Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- 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.

## v0.37.0

- added: `Exqlite.Sqlite3.cancel/1` to cancel a running query, waking both the busy handler and VDBE execution.
Expand Down
10 changes: 5 additions & 5 deletions lib/exqlite/connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ defmodule Exqlite.Connection do
* `:default_transaction_mode` - one of `deferred` (default), `immediate`,
or `exclusive`. If a mode is not specified in a call to `Repo.transaction/2`,
this will be the default transaction mode.
* `:mode` - use `:readwrite` to open the database for reading and writing
, `:readonly` to open it in read-only mode or `[:readonly | :readwrite, :nomutex]`
to open it with no mutex mode. `:readwrite` will also create
the database if it doesn't already exist. Defaults to `:readwrite`.
Note: [:readwrite, :nomutex] is not recommended.
* `:mode` - controls the sqlite3_open_v2 flags. Defaults to
`[:readwrite, :create]` (read/write + create if needed). Use
`:readwrite` for read/write without create, `:readonly` for read-only, or
a list such as `[:readwrite, :create]` or `[:readonly, :nomutex]`.
Note: `[:readwrite, :nomutex]` is not recommended.
* `:journal_mode` - Sets the journal mode for the sqlite connection. Can be
one of the following `:delete`, `:truncate`, `:persist`, `:memory`,
`:wal`, or `:off`. Defaults to `:delete`. It is recommended that you use
Expand Down
45 changes: 30 additions & 15 deletions lib/exqlite/sqlite3.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ defmodule Exqlite.Sqlite3 do
@type statement() :: reference()
@type reason() :: atom() | String.t()
@type row() :: list()
@type open_mode :: :readwrite | :readonly | :nomutex
@type open_opt :: {:mode, :readwrite | :readonly | [open_mode()]}
@type open_mode :: :readwrite | :readonly | :nomutex | :create
@type open_opt :: {:mode, :readwrite | :readonly | :create | [open_mode()]}

@doc """
Opens a new sqlite database at the Path provided.
Expand All @@ -29,15 +29,28 @@ defmodule Exqlite.Sqlite3 do

## Options

* `:mode` - use `:readwrite` to open the database for reading and writing
, `:readonly` to open it in read-only mode or `[:readonly | :readwrite, :nomutex]`
to open it with no mutex mode. `:readwrite` will also create
the database if it doesn't already exist. Defaults to `:readwrite`.
Note: [:readwrite, :nomutex] is not recommended.
* `:mode` - controls the flags for sqlite3_open_v2 (see
https://www.sqlite.org/c3ref/c_open_autoproxy.html). Defaults to
`[:readwrite, :create]` (opens for reading and writing and creates the
file if it does not exist).

Single modes are permitted:
- `:readwrite` - read/write to the database. Does not create the database
if it is not present. Use in combination with `:create` to create the
database if it does not exist.
- `:readonly` - read-only (file must exist).
- `:create` - creates the database if it does not exist.

Combinations are permitted:
- `[:readwrite, :create]` - read/write + create if needed. This is the
default if not specified.
- `[:readonly, :nomutex]`

Note: `[:readwrite, :nomutex]` is not recommended.
"""
@spec open(String.t(), [open_opt()]) :: {:ok, db()} | {:error, reason()}
def open(path, opts \\ []) do
mode = Keyword.get(opts, :mode, :readwrite)
mode = opts[:mode] || [:readwrite, :create]
Sqlite3NIF.open(path, flags_from_mode(mode))
end

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

defp flags_from_mode(:readwrite),
do: do_flags_from_mode([:readwrite], [])

defp flags_from_mode(:readonly),
do: do_flags_from_mode([:readonly], [])
defp flags_from_mode(mode) when mode in [:readwrite, :readonly, :create],
do: do_flags_from_mode(List.wrap(mode), [])

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

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

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

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

defp do_flags_from_mode([:create | tail], acc),
do: do_flags_from_mode(tail, [:sqlite_open_create | acc])

defp do_flags_from_mode([mode | _tail], _acc) do
raise ArgumentError,
"expected mode to be `:readwrite`, `:readonly` or `:nomutex`, but received #{inspect(mode)}"
"expected mode to be `:readwrite`, `:readonly`, `:nomutex` or `:create`, but received #{inspect(mode)}"
end

defp do_flags_from_mode([], acc),
Expand Down
23 changes: 22 additions & 1 deletion test/exqlite/sqlite3_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,33 @@ defmodule Exqlite.Sqlite3Test do
{:ok, path} = Temp.path()

msg =
"expected mode to be `:readwrite`, `:readonly` or `:nomutex`, but received :notarealmode"
"expected mode to be `:readwrite`, `:readonly`, `:nomutex` or `:create`, but received :notarealmode"

assert_raise ArgumentError, msg, fn ->
Sqlite3.open(path, mode: [:notarealmode])
end
end

test "opens with default create but explicit readwrite does not create" do
{:ok, path} = Temp.path()

# Pure readwrite modes should not create the file.
assert {:error, _reason} = Sqlite3.open(path, mode: :readwrite)
assert {:error, _reason} = Sqlite3.open(path, mode: [:readwrite])

# The default (and [:readwrite, :create]) still creates.
{:ok, conn} = Sqlite3.open(path)
:ok = Sqlite3.close(conn)
assert File.exists?(path)
File.rm!(path)

# Explicit list with create also works
{:ok, path2} = Temp.path()
{:ok, conn2} = Sqlite3.open(path2, mode: [:readwrite, :create])
:ok = Sqlite3.close(conn2)
assert File.exists?(path2)
File.rm!(path2)
end
end

describe ".close/2" do
Expand Down
Loading