Skip to content

Commit 57019dd

Browse files
mikestokMike Stok
andauthored
Allow the use of a file: uri for a database (#225)
This change allows SQLite3 to load data from files on a read only file system using Ecto and ecto_sqlite3 which use exqlite. Looking at https://sqlite.org/uri.html I saw that I could use a file url to pass in the mode=ro and immutable=1 parameters to sqlite3 to prevent the creation of `-wal` and `-shm` files. I noticed that there were still some empty directories with names starting with `file:` appearing on my laptop. This is the least intrusive change I could make to the code to allow for `file:` uris to work. I chose to use `URI.parse/1` rather than `URI.new!/1` or `URI.new/1` as the last two functions need Elixir 1.13. Co-authored-by: Mike Stok <mstok@kineticcafe.com>
1 parent adbe5a2 commit 57019dd

File tree

2 files changed

+70
-6
lines changed

2 files changed

+70
-6
lines changed

lib/exqlite/connection.ex

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ defmodule Exqlite.Connection do
3131

3232
defstruct [
3333
:db,
34+
:directory,
3435
:path,
3536
:transaction_status,
3637
:status,
@@ -39,6 +40,7 @@ defmodule Exqlite.Connection do
3940

4041
@type t() :: %__MODULE__{
4142
db: Sqlite3.db(),
43+
directory: String.t() | nil,
4244
path: String.t(),
4345
transaction_status: :idle | :transaction,
4446
status: :idle | :busy
@@ -437,9 +439,10 @@ defmodule Exqlite.Connection do
437439
set_pragma(db, "busy_timeout", Pragma.busy_timeout(options))
438440
end
439441

440-
defp do_connect(path, options) do
441-
with :ok <- mkdir_p(path),
442-
{:ok, db} <- Sqlite3.open(path, options),
442+
defp do_connect(database, options) do
443+
with {:ok, directory} <- resolve_directory(database),
444+
:ok <- mkdir_p(directory),
445+
{:ok, db} <- Sqlite3.open(database, options),
443446
:ok <- set_key(db, options),
444447
:ok <- set_journal_mode(db, options),
445448
:ok <- set_temp_store(db, options),
@@ -458,7 +461,8 @@ defmodule Exqlite.Connection do
458461
:ok <- set_hard_heap_limit(db, options) do
459462
state = %__MODULE__{
460463
db: db,
461-
path: path,
464+
directory: directory,
465+
path: database,
462466
transaction_status: :idle,
463467
status: :idle,
464468
chunk_size: Keyword.get(options, :chunk_size)
@@ -605,10 +609,24 @@ defmodule Exqlite.Connection do
605609
end
606610
end
607611

612+
defp resolve_directory(":memory:"), do: {:ok, nil}
613+
614+
defp resolve_directory("file:" <> _ = uri) do
615+
case URI.parse(uri) do
616+
%{path: path} when is_binary(path) ->
617+
{:ok, Path.dirname(path)}
618+
619+
_ ->
620+
{:error, "No path in #{inspect(uri)}"}
621+
end
622+
end
623+
624+
defp resolve_directory(path), do: {:ok, Path.dirname(path)}
625+
608626
# SQLITE_OPEN_CREATE will create the DB file if not existing, but
609627
# will not create intermediary directories if they are missing.
610628
# So let's preemptively create the intermediate directories here
611629
# before trying to open the DB file.
612-
defp mkdir_p(":memory:"), do: :ok
613-
defp mkdir_p(path), do: File.mkdir_p(Path.dirname(path))
630+
defp mkdir_p(nil), do: :ok
631+
defp mkdir_p(directory), do: File.mkdir_p(directory)
614632
end

test/exqlite/connection_test.exs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,52 @@ defmodule Exqlite.ConnectionTest do
3737
File.rm(path)
3838
end
3939

40+
test "connects to a file from URL" do
41+
path = Temp.path!()
42+
43+
{:ok, state} = Connection.connect(database: "file:#{path}?mode=rwc")
44+
45+
assert state.directory == Path.dirname(path)
46+
assert state.db
47+
end
48+
49+
test "fails to write a file from URL with mode=ro" do
50+
path = Temp.path!()
51+
52+
{:ok, db} = Sqlite3.open(path)
53+
54+
:ok =
55+
Sqlite3.execute(db, "create table test (id ingeger primary key, stuff text)")
56+
57+
:ok =
58+
Sqlite3.execute(db, "insert into test (id, stuff) values (999, 'Some stuff')")
59+
60+
:ok = Sqlite3.close(db)
61+
62+
{:ok, conn} = Connection.connect(database: "file:#{path}?mode=ro")
63+
64+
assert conn.directory == Path.dirname(path)
65+
assert conn.db
66+
67+
assert match?(
68+
{:ok, _, %{rows: [[1]]}, _},
69+
%Query{statement: "select count(*) from test"}
70+
|> Connection.handle_execute([], [], conn)
71+
)
72+
73+
{:error, %{message: message}, _} =
74+
%Query{
75+
statement: "insert into test (id, stuff) values (888, 'some more stuff')"
76+
}
77+
|> Connection.handle_execute([], [], conn)
78+
79+
# In most of the test matrix the message is "attempt to write a readonly database",
80+
# but in Elixir 1.13, OTP 23, OS windows-2019 it is "not an error".
81+
assert message in ["attempt to write a readonly database", "not an error"]
82+
83+
File.rm(path)
84+
end
85+
4086
test "setting journal_size_limit" do
4187
path = Temp.path!()
4288
size_limit = 20 * 1024 * 1024

0 commit comments

Comments
 (0)