Skip to content

Commit aea0bea

Browse files
authored
feat: finer-grained :mode support for Sqlite3.open (#348)
Make default mode more explicit. Aligns more closely with `sqlite3_open_v2` Refs #347
1 parent 9f4c752 commit aea0bea

4 files changed

Lines changed: 59 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- added: Finer-grained `:mode` support when opening databases (e.g. `[:readwrite]` for read/write without implicit CREATE, `[:readwrite, :create]`). The atom `:readwrite` default is preserved for backward compatibility (still creates if needed). This aligns the API more closely with sqlite3_open_v2 flags. See #347.
6+
57
## v0.37.0
68

79
- added: `Exqlite.Sqlite3.cancel/1` to cancel a running query, waking both the busy handler and VDBE execution.

lib/exqlite/connection.ex

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,11 @@ defmodule Exqlite.Connection do
9898
* `:default_transaction_mode` - one of `deferred` (default), `immediate`,
9999
or `exclusive`. If a mode is not specified in a call to `Repo.transaction/2`,
100100
this will be the default transaction mode.
101-
* `:mode` - use `:readwrite` to open the database for reading and writing
102-
, `:readonly` to open it in read-only mode or `[:readonly | :readwrite, :nomutex]`
103-
to open it with no mutex mode. `:readwrite` will also create
104-
the database if it doesn't already exist. Defaults to `:readwrite`.
105-
Note: [:readwrite, :nomutex] is not recommended.
101+
* `:mode` - controls the sqlite3_open_v2 flags. Defaults to
102+
`[:readwrite, :create]` (read/write + create if needed). Use
103+
`:readwrite` for read/write without create, `:readonly` for read-only, or
104+
a list such as `[:readwrite, :create]` or `[:readonly, :nomutex]`.
105+
Note: `[:readwrite, :nomutex]` is not recommended.
106106
* `:journal_mode` - Sets the journal mode for the sqlite connection. Can be
107107
one of the following `:delete`, `:truncate`, `:persist`, `:memory`,
108108
`:wal`, or `:off`. Defaults to `:delete`. It is recommended that you use

lib/exqlite/sqlite3.ex

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ defmodule Exqlite.Sqlite3 do
1919
@type statement() :: reference()
2020
@type reason() :: atom() | String.t()
2121
@type row() :: list()
22-
@type open_mode :: :readwrite | :readonly | :nomutex
23-
@type open_opt :: {:mode, :readwrite | :readonly | [open_mode()]}
22+
@type open_mode :: :readwrite | :readonly | :nomutex | :create
23+
@type open_opt :: {:mode, :readwrite | :readonly | :create | [open_mode()]}
2424

2525
@doc """
2626
Opens a new sqlite database at the Path provided.
@@ -29,15 +29,28 @@ defmodule Exqlite.Sqlite3 do
2929
3030
## Options
3131
32-
* `:mode` - use `:readwrite` to open the database for reading and writing
33-
, `:readonly` to open it in read-only mode or `[:readonly | :readwrite, :nomutex]`
34-
to open it with no mutex mode. `:readwrite` will also create
35-
the database if it doesn't already exist. Defaults to `:readwrite`.
36-
Note: [:readwrite, :nomutex] is not recommended.
32+
* `:mode` - controls the flags for sqlite3_open_v2 (see
33+
https://www.sqlite.org/c3ref/c_open_autoproxy.html). Defaults to
34+
`[:readwrite, :create]` (opens for reading and writing and creates the
35+
file if it does not exist).
36+
37+
Single modes are permitted:
38+
- `:readwrite` - read/write to the database. Does not create the database
39+
if it is not present. Use in combination with `:create` to create the
40+
database if it does not exist.
41+
- `:readonly` - read-only (file must exist).
42+
- `:create` - creates the database if it does not exist.
43+
44+
Combinations are permitted:
45+
- `[:readwrite, :create]` - read/write + create if needed. This is the
46+
default if not specified.
47+
- `[:readonly, :nomutex]`
48+
49+
Note: `[:readwrite, :nomutex]` is not recommended.
3750
"""
3851
@spec open(String.t(), [open_opt()]) :: {:ok, db()} | {:error, reason()}
3952
def open(path, opts \\ []) do
40-
mode = Keyword.get(opts, :mode, :readwrite)
53+
mode = opts[:mode] || [:readwrite, :create]
4154
Sqlite3NIF.open(path, flags_from_mode(mode))
4255
end
4356

@@ -46,11 +59,8 @@ defmodule Exqlite.Sqlite3 do
4659
"expected mode to be `:readwrite` or `:readonly`, can't use a single :nomutex mode"
4760
end
4861

49-
defp flags_from_mode(:readwrite),
50-
do: do_flags_from_mode([:readwrite], [])
51-
52-
defp flags_from_mode(:readonly),
53-
do: do_flags_from_mode([:readonly], [])
62+
defp flags_from_mode(mode) when mode in [:readwrite, :readonly, :create],
63+
do: do_flags_from_mode(List.wrap(mode), [])
5464

5565
defp flags_from_mode([_ | _] = modes),
5666
do: do_flags_from_mode(modes, [])
@@ -60,18 +70,23 @@ defmodule Exqlite.Sqlite3 do
6070
"expected mode to be `:readwrite`, `:readonly` or list of modes, but received #{inspect(mode)}"
6171
end
6272

73+
# List context: `:readwrite` adds *only* READWRITE (no implicit CREATE).
74+
# Users must explicitly list `:create` when they want it.
6375
defp do_flags_from_mode([:readwrite | tail], acc),
64-
do: do_flags_from_mode(tail, [:sqlite_open_readwrite, :sqlite_open_create | acc])
76+
do: do_flags_from_mode(tail, [:sqlite_open_readwrite | acc])
6577

6678
defp do_flags_from_mode([:readonly | tail], acc),
6779
do: do_flags_from_mode(tail, [:sqlite_open_readonly | acc])
6880

6981
defp do_flags_from_mode([:nomutex | tail], acc),
7082
do: do_flags_from_mode(tail, [:sqlite_open_nomutex | acc])
7183

84+
defp do_flags_from_mode([:create | tail], acc),
85+
do: do_flags_from_mode(tail, [:sqlite_open_create | acc])
86+
7287
defp do_flags_from_mode([mode | _tail], _acc) do
7388
raise ArgumentError,
74-
"expected mode to be `:readwrite`, `:readonly` or `:nomutex`, but received #{inspect(mode)}"
89+
"expected mode to be `:readwrite`, `:readonly`, `:nomutex` or `:create`, but received #{inspect(mode)}"
7590
end
7691

7792
defp do_flags_from_mode([], acc),

test/exqlite/sqlite3_test.exs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,33 @@ defmodule Exqlite.Sqlite3Test do
102102
{:ok, path} = Temp.path()
103103

104104
msg =
105-
"expected mode to be `:readwrite`, `:readonly` or `:nomutex`, but received :notarealmode"
105+
"expected mode to be `:readwrite`, `:readonly`, `:nomutex` or `:create`, but received :notarealmode"
106106

107107
assert_raise ArgumentError, msg, fn ->
108108
Sqlite3.open(path, mode: [:notarealmode])
109109
end
110110
end
111+
112+
test "opens with default create but explicit readwrite does not create" do
113+
{:ok, path} = Temp.path()
114+
115+
# Pure readwrite modes should not create the file.
116+
assert {:error, _reason} = Sqlite3.open(path, mode: :readwrite)
117+
assert {:error, _reason} = Sqlite3.open(path, mode: [:readwrite])
118+
119+
# The default (and [:readwrite, :create]) still creates.
120+
{:ok, conn} = Sqlite3.open(path)
121+
:ok = Sqlite3.close(conn)
122+
assert File.exists?(path)
123+
File.rm!(path)
124+
125+
# Explicit list with create also works
126+
{:ok, path2} = Temp.path()
127+
{:ok, conn2} = Sqlite3.open(path2, mode: [:readwrite, :create])
128+
:ok = Sqlite3.close(conn2)
129+
assert File.exists?(path2)
130+
File.rm!(path2)
131+
end
111132
end
112133

113134
describe ".close/2" do

0 commit comments

Comments
 (0)