Skip to content

Commit 7a78463

Browse files
committed
PR feedback
1 parent 2800a25 commit 7a78463

File tree

2 files changed

+63
-7
lines changed
  • packages/sync-service

2 files changed

+63
-7
lines changed

packages/sync-service/lib/electric/shape_cache/shape_status/shape_db/sqlite3.ex

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,21 @@ defmodule Electric.ShapeCache.ShapeStatus.ShapeDb.Sqlite3 do
2424
| multi_step(conn, stmt) | step loop; returns {:rows, rows}/{:done, rows} |
2525
| enable_load_extension(conn, bool) | not supported – always returns error |
2626
| bind_parameter_count(stmt) | column_names heuristic (explain only) |
27+
28+
## How esqlite manages prepared statements
29+
30+
When `prepare` is called, the NIF allocates an `esqlite3_stmt` resource,
31+
immediately calls `enif_release_resource` to drop the C-side reference, and
32+
returns the resource wrapped in an Erlang term. From that point the BEAM
33+
garbage collector is the sole owner: when no Erlang process holds a reference
34+
to the term, the GC calls the registered destructor which runs
35+
`sqlite3_finalize`. The NIF also holds an `enif_keep_resource` reference from the
36+
statement back to its connection, ensuring the connection is never finalized
37+
before all its statements are. There is no explicit finalize or release call
38+
exposed — lifetime is entirely GC- driven.
39+
40+
Hence the `release/1` function is a no-op.
41+
2742
"""
2843

2944
# ── Types ──────────────────────────────────────────────────────────────────
@@ -80,7 +95,7 @@ defmodule Electric.ShapeCache.ShapeStatus.ShapeDb.Sqlite3 do
8095
"""
8196
@spec bind(statement(), list()) :: :ok | {:error, term()}
8297
def bind(stmt, binds) do
83-
converted = Enum.map(binds, &convert_bind/1)
98+
converted = convert_binds(binds)
8499
:esqlite3.bind(stmt, converted)
85100
end
86101

@@ -176,12 +191,31 @@ defmodule Electric.ShapeCache.ShapeStatus.ShapeDb.Sqlite3 do
176191
0
177192
end
178193

179-
# ── Private helpers ────────────────────────────────────────────────────────
194+
@doc """
195+
Build a file: URI from a path with the given opts as query params
196+
197+
See: https://sqlite.org/uri.html#uri_filenames_in_sqlite
198+
199+
## Examples
200+
201+
iex> build_uri(":memory:", [])
202+
"file:memory?mode=memory&cache=shared"
203+
204+
iex> build_uri("/my/path/here", [])
205+
"file:/my/path/here?mode=rwc"
180206
181-
# Build a SQLite URI from a file path and exqlite-style opts.
182-
defp build_uri(":memory:", _opts), do: "file:memory?mode=memory&cache=shared"
207+
iex> build_uri("/my/path/here", mode: :readonly)
208+
"file:/my/path/here?mode=ro"
183209
184-
defp build_uri(path, opts) do
210+
iex> build_uri("/my/#path?/is-here", mode: :readonly)
211+
"file:/my/%23path%3F/is-here?mode=ro"
212+
213+
iex> build_uri("/my//path//here", mode: :readwrite)
214+
"file:/my/path/here?mode=rwc"
215+
"""
216+
def build_uri(":memory:", _opts), do: "file:memory?mode=memory&cache=shared"
217+
218+
def build_uri(path, opts) do
185219
mode =
186220
case Keyword.get(opts, :mode, []) do
187221
modes when is_list(modes) ->
@@ -194,13 +228,28 @@ defmodule Electric.ShapeCache.ShapeStatus.ShapeDb.Sqlite3 do
194228
"rwc"
195229
end
196230

197-
"file:#{URI.encode(path)}?mode=#{mode}"
231+
"file:#{URI.encode(Path.absname(path), &unescaped?/1)}?mode=#{mode}"
232+
end
233+
234+
defp unescaped?(?/), do: true
235+
defp unescaped?(char), do: URI.char_unreserved?(char)
236+
237+
# Maps are used for named binds in the form `%{name => bind}`
238+
defp convert_binds(binds) when is_map(binds) do
239+
Map.new(binds, fn {name, value} -> {name, convert_bind(value)} end)
240+
end
241+
242+
defp convert_binds(binds) when is_list(binds) do
243+
Enum.map(binds, &convert_bind/1)
198244
end
199245

200246
# Convert an exqlite bind value to an esqlite bind value.
201247
# esqlite's bind/2 supports: integers, floats, binaries (text), and
202248
# {:blob, binary} tuples for BLOBs. nil/null map to undefined.
203249
defp convert_bind(nil), do: :undefined
204250
defp convert_bind(:null), do: :undefined
205-
defp convert_bind(value), do: value
251+
defp convert_bind({:blob, _} = blob), do: blob
252+
# Deliberately being conservative with the types of binds we support
253+
defp convert_bind(value) when is_integer(value) or is_binary(value) or is_float(value),
254+
do: value
206255
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
defmodule Electric.ShapeCache.ShapeStatus.ShapeDb.Sqlite3Test do
2+
use ExUnit.Case, async: true
3+
4+
alias Electric.ShapeCache.ShapeStatus.ShapeDb.Sqlite3
5+
6+
doctest Sqlite3, import: true
7+
end

0 commit comments

Comments
 (0)