Skip to content

Commit 2edb69d

Browse files
greg-rychlewskiGreg Rychlewski
andauthored
Use sqlite3_open_v2 (#211)
Co-authored-by: Greg Rychlewski <grychle@LLL7WPM64V5M.ngco.com>
1 parent 931039c commit 2edb69d

File tree

5 files changed

+106
-8
lines changed

5 files changed

+106
-8
lines changed

c_src/sqlite3_nif.c

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,12 @@ exqlite_open(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
184184

185185
int rc = 0;
186186
int size = 0;
187+
int flags;
187188
connection_t* conn = NULL;
188189
char filename[MAX_PATHNAME];
189190
ERL_NIF_TERM result;
190191

191-
if (argc != 1) {
192+
if (argc != 2) {
192193
return enif_make_badarg(env);
193194
}
194195

@@ -202,7 +203,11 @@ exqlite_open(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
202203
return make_error_tuple(env, "out_of_memory");
203204
}
204205

205-
rc = sqlite3_open(filename, &conn->db);
206+
if (!enif_get_int(env, argv[1], &flags)) {
207+
return make_error_tuple(env, "invalid flags");
208+
}
209+
210+
rc = sqlite3_open_v2(filename, &conn->db, flags, NULL);
206211
if (rc != SQLITE_OK) {
207212
enif_release_resource(conn);
208213
return make_error_tuple(env, "database_open_failed");
@@ -944,7 +949,7 @@ exqlite_enable_load_extension(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[
944949
//
945950

946951
static ErlNifFunc nif_funcs[] = {
947-
{"open", 1, exqlite_open, ERL_NIF_DIRTY_JOB_IO_BOUND},
952+
{"open", 2, exqlite_open, ERL_NIF_DIRTY_JOB_IO_BOUND},
948953
{"close", 1, exqlite_close, ERL_NIF_DIRTY_JOB_IO_BOUND},
949954
{"execute", 2, exqlite_execute, ERL_NIF_DIRTY_JOB_IO_BOUND},
950955
{"changes", 1, exqlite_changes, ERL_NIF_DIRTY_JOB_IO_BOUND},

lib/exqlite/flags.ex

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
defmodule Exqlite.Flags do
2+
@moduledoc false
3+
4+
import Bitwise
5+
6+
# https://www.sqlite.org/c3ref/c_open_autoproxy.html
7+
@file_open_flags [
8+
sqlite_open_readonly: 0x00000001,
9+
sqlite_open_readwrite: 0x00000002,
10+
sqlite_open_create: 0x00000004,
11+
sqlite_open_deleteonclos: 0x00000008,
12+
sqlite_open_exclusive: 0x00000010,
13+
sqlite_open_autoproxy: 0x00000020,
14+
sqlite_open_uri: 0x00000040,
15+
sqlite_open_memory: 0x00000080,
16+
sqlite_open_main_db: 0x00000100,
17+
sqlite_open_temp_db: 0x00000200,
18+
sqlite_open_transient_db: 0x00000400,
19+
sqlite_open_main_journal: 0x00000800,
20+
sqlite_open_temp_journal: 0x00001000,
21+
sqlite_open_subjournal: 0x00002000,
22+
sqlite_open_super_journal: 0x00004000,
23+
sqlite_open_nomutex: 0x00008000,
24+
sqlite_open_fullmutex: 0x00010000,
25+
sqlite_open_sharedcache: 0x00020000,
26+
sqlite_open_privatecache: 0x00040000,
27+
sqlite_open_wal: 0x00080000,
28+
sqlite_open_nofollow: 0x01000000,
29+
sqlite_open_exrescode: 0x02000000
30+
]
31+
32+
def put_file_open_flags(current_flags \\ 0, flags) do
33+
Enum.reduce(flags, current_flags, &(&2 ||| Keyword.fetch!(@file_open_flags, &1)))
34+
end
35+
end

lib/exqlite/sqlite3.ex

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,42 @@ defmodule Exqlite.Sqlite3 do
1212
# Need to figure out if we can just stream results where we use this
1313
# module as a sink.
1414

15+
alias Exqlite.Flags
1516
alias Exqlite.Sqlite3NIF
1617

1718
@type db() :: reference()
1819
@type statement() :: reference()
1920
@type reason() :: atom() | String.t()
2021
@type row() :: list()
22+
@type open_opt :: {:mode, :readwrite | :readonly}
2123

2224
@doc """
2325
Opens a new sqlite database at the Path provided.
2426
25-
If `path` can be `":memory"` to keep the sqlite database in memory.
27+
`path` can be `":memory"` to keep the sqlite database in memory.
28+
29+
## Options
30+
31+
* `:mode` - use `:readwrite` to open the database for reading and writing
32+
or `:readonly` to open it in read-only mode. `:readwrite` will also create
33+
the database if it doesn't already exist. Defaults to `:readwrite`.
2634
"""
27-
@spec open(String.t()) :: {:ok, db()} | {:error, reason()}
28-
def open(path), do: Sqlite3NIF.open(String.to_charlist(path))
35+
@spec open(String.t(), [open_opt()]) :: {:ok, db()} | {:error, reason()}
36+
def open(path, opts \\ []) do
37+
mode = Keyword.get(opts, :mode, :readwrite)
38+
Sqlite3NIF.open(String.to_charlist(path), flags_from_mode(mode))
39+
end
40+
41+
defp flags_from_mode(:readwrite),
42+
do: Flags.put_file_open_flags([:sqlite_open_readwrite, :sqlite_open_create])
43+
44+
defp flags_from_mode(:readonly),
45+
do: Flags.put_file_open_flags([:sqlite_open_readonly])
46+
47+
defp flags_from_mode(mode) do
48+
raise ArgumentError,
49+
"expected mode to be `:readwrite` or `:readonly`, but received #{inspect(mode)}"
50+
end
2951

3052
@spec close(nil) :: :ok
3153
def close(nil), do: :ok

lib/exqlite/sqlite3_nif.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ defmodule Exqlite.Sqlite3NIF do
1717
:erlang.load_nif(path, 0)
1818
end
1919

20-
@spec open(String.Chars.t()) :: {:ok, db()} | {:error, reason()}
21-
def open(_path), do: :erlang.nif_error(:not_loaded)
20+
@spec open(String.Chars.t(), integer()) :: {:ok, db()} | {:error, reason()}
21+
def open(_path, _flags), do: :erlang.nif_error(:not_loaded)
2222

2323
@spec close(db()) :: :ok | {:error, reason()}
2424
def close(_conn), do: :erlang.nif_error(:not_loaded)

test/exqlite/sqlite3_test.exs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,42 @@ defmodule Exqlite.Sqlite3Test do
2727

2828
File.rm(path)
2929
end
30+
31+
test "opens a database in readonly mode" do
32+
# Create database with readwrite connection
33+
{:ok, path} = Temp.path()
34+
{:ok, rw_conn} = Sqlite3.open(path)
35+
36+
create_table_query = "create table test (id integer primary key, stuff text)"
37+
:ok = Sqlite3.execute(rw_conn, create_table_query)
38+
39+
insert_value_query = "insert into test (stuff) values ('This is a test')"
40+
:ok = Sqlite3.execute(rw_conn, insert_value_query)
41+
42+
# Read from database with a readonly connection
43+
{:ok, ro_conn} = Sqlite3.open(path, mode: :readonly)
44+
45+
select_query = "select id, stuff from test order by id asc"
46+
{:ok, statement} = Sqlite3.prepare(ro_conn, select_query)
47+
{:row, columns} = Sqlite3.step(ro_conn, statement)
48+
49+
assert [1, "This is a test"] == columns
50+
51+
# Readonly connection cannot insert
52+
assert {:error, "attempt to write a readonly database"} ==
53+
Sqlite3.execute(ro_conn, insert_value_query)
54+
end
55+
56+
test "opens a database with invalid mode" do
57+
{:ok, path} = Temp.path()
58+
59+
msg =
60+
"expected mode to be `:readwrite` or `:readonly`, but received :notarealmode"
61+
62+
assert_raise ArgumentError, msg, fn ->
63+
Sqlite3.open(path, mode: :notarealmode)
64+
end
65+
end
3066
end
3167

3268
describe ".close/2" do

0 commit comments

Comments
 (0)