@@ -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
206255end
0 commit comments