Skip to content

Commit 79b782b

Browse files
authored
Add named params (#332)
1 parent 364d019 commit 79b782b

File tree

6 files changed

+132
-7
lines changed

6 files changed

+132
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- added: Added named params support
56
- added: Custom type extensions. See: `Exqlite.TypeExtensions`.
67
- changed: Update sqlite to `3.50.1`.
78

c_src/sqlite3_nif.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,30 @@ exqlite_bind_parameter_count(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]
561561
return enif_make_int(env, bind_parameter_count);
562562
}
563563

564+
///
565+
/// Get the bind parameter index
566+
///
567+
ERL_NIF_TERM
568+
exqlite_bind_parameter_index(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
569+
{
570+
statement_t* statement;
571+
if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) {
572+
return raise_badarg(env, argv[0]);
573+
}
574+
575+
ERL_NIF_TERM eos = enif_make_int(env, 0);
576+
ErlNifBinary name;
577+
578+
if (!enif_inspect_iolist_as_binary(env, enif_make_list2(env, argv[1], eos), &name)) {
579+
return raise_badarg(env, argv[1]);
580+
}
581+
582+
statement_acquire_lock(statement);
583+
int index = sqlite3_bind_parameter_index(statement->statement, (const char*)name.data);
584+
statement_release_lock(statement);
585+
return enif_make_int(env, index);
586+
}
587+
564588
///
565589
/// Binds a text parameter
566590
///
@@ -1423,6 +1447,7 @@ static ErlNifFunc nif_funcs[] = {
14231447
{"prepare", 2, exqlite_prepare, ERL_NIF_DIRTY_JOB_IO_BOUND},
14241448
{"reset", 1, exqlite_reset, ERL_NIF_DIRTY_JOB_CPU_BOUND},
14251449
{"bind_parameter_count", 1, exqlite_bind_parameter_count},
1450+
{"bind_parameter_index", 2, exqlite_bind_parameter_index},
14261451
{"bind_text", 3, exqlite_bind_text},
14271452
{"bind_blob", 3, exqlite_bind_blob},
14281453
{"bind_integer", 3, exqlite_bind_integer},

lib/exqlite/sqlite3.ex

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ defmodule Exqlite.Sqlite3 do
158158
iex> Sqlite3.step(conn, stmt)
159159
{:row, [42, 3.14, "Alice", <<0, 0, 0>>, nil]}
160160
161+
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
162+
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT :42, @pi, $name, @blob, :null")
163+
iex> Sqlite3.bind(stmt, %{":42" => 42, "@pi" => 3.14, "$name" => "Alice", :"@blob" => {:blob, <<0, 0, 0>>}, ~c":null" => nil})
164+
iex> Sqlite3.step(conn, stmt)
165+
{:row, [42, 3.14, "Alice", <<0, 0, 0>>, nil]}
166+
161167
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
162168
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?")
163169
iex> Sqlite3.bind(stmt, [42, 3.14, "Alice"])
@@ -174,10 +180,13 @@ defmodule Exqlite.Sqlite3 do
174180
** (ArgumentError) unsupported type: #PID<0.0.0>
175181
176182
"""
177-
@spec bind(statement, [bind_value] | nil) :: :ok
183+
@spec bind(
184+
statement,
185+
[bind_value] | %{optional(String.t()) => bind_value} | nil
186+
) :: :ok
178187
def bind(stmt, nil), do: bind(stmt, [])
179188

180-
def bind(stmt, args) do
189+
def bind(stmt, args) when is_list(args) do
181190
params_count = bind_parameter_count(stmt)
182191
args_count = length(args)
183192

@@ -188,8 +197,40 @@ defmodule Exqlite.Sqlite3 do
188197
end
189198
end
190199

191-
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
200+
def bind(stmt, args) when is_map(args) do
201+
params_count = bind_parameter_count(stmt)
202+
args_count = map_size(args)
203+
204+
if args_count == params_count do
205+
bind_all_named(Map.to_list(args), stmt)
206+
else
207+
raise ArgumentError,
208+
"expected #{params_count} named arguments, got #{args_count}: #{inspect(Map.keys(args))}"
209+
end
210+
end
211+
192212
defp bind_all([param | params], stmt, idx) do
213+
do_bind(stmt, idx, param)
214+
bind_all(params, stmt, idx + 1)
215+
end
216+
217+
defp bind_all([], _stmt, _idx), do: :ok
218+
219+
defp bind_all_named([{name, param} | named_params], stmt) do
220+
idx = Sqlite3NIF.bind_parameter_index(stmt, to_string(name))
221+
222+
if idx == 0 do
223+
raise ArgumentError, "unknown named parameter: #{inspect(name)}"
224+
end
225+
226+
do_bind(stmt, idx, param)
227+
bind_all_named(named_params, stmt)
228+
end
229+
230+
defp bind_all_named([], _stmt), do: :ok
231+
232+
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
233+
defp do_bind(stmt, idx, param) do
193234
case convert(param) do
194235
i when is_integer(i) -> bind_integer(stmt, idx, i)
195236
f when is_float(f) -> bind_float(stmt, idx, f)
@@ -202,12 +243,8 @@ defmodule Exqlite.Sqlite3 do
202243
{:blob, b} when is_list(b) -> bind_blob(stmt, idx, IO.iodata_to_binary(b))
203244
_other -> raise ArgumentError, "unsupported type: #{inspect(param)}"
204245
end
205-
206-
bind_all(params, stmt, idx + 1)
207246
end
208247

209-
defp bind_all([], _stmt, _idx), do: :ok
210-
211248
@spec columns(db(), statement()) :: {:ok, [binary()]} | {:error, reason()}
212249
def columns(conn, statement), do: Sqlite3NIF.columns(conn, statement)
213250

lib/exqlite/sqlite3_nif.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ defmodule Exqlite.Sqlite3NIF do
7272
@spec bind_parameter_count(statement) :: integer
7373
def bind_parameter_count(_stmt), do: :erlang.nif_error(:not_loaded)
7474

75+
@spec bind_parameter_index(statement, String.t()) :: integer
76+
def bind_parameter_index(_stmt, _name), do: :erlang.nif_error(:not_loaded)
77+
7578
@spec bind_text(statement, non_neg_integer, String.t()) :: integer()
7679
def bind_text(_stmt, _index, _text), do: :erlang.nif_error(:not_loaded)
7780

test/exqlite/query_test.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ defmodule Exqlite.QueryTest do
3131
assert Enum.to_list(columns["y"]) == ["a", "b", "c"]
3232
end
3333

34+
test "named params", %{conn: conn} do
35+
assert Exqlite.query!(conn, "select :a, @b, $c", %{":a" => 1, "@b" => 2, "$c" => 3}) ==
36+
%Exqlite.Result{
37+
command: :execute,
38+
columns: [":a", "@b", "$c"],
39+
rows: [[1, 2, 3]],
40+
num_rows: 1
41+
}
42+
end
43+
3444
defp create_conn!(_) do
3545
opts = [database: "#{Temp.path!()}.db"]
3646

test/exqlite/sqlite3_test.exs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,55 @@ defmodule Exqlite.Sqlite3Test do
298298
Sqlite3.bind(statement, [other_tz])
299299
end
300300
end
301+
302+
test "binds named parameters" do
303+
{:ok, conn} = Sqlite3.open(":memory:")
304+
305+
{:ok, statement} =
306+
Sqlite3.prepare(conn, "select :42, @pi, :name, $👋, :blob, :null")
307+
308+
:ok =
309+
Sqlite3.bind(statement, %{
310+
":42" => 42,
311+
"@pi" => 3.14,
312+
:":name" => "Alice",
313+
"$👋" => "👋",
314+
":blob" => {:blob, <<0, 1, 2>>},
315+
~c":null" => nil
316+
})
317+
318+
assert {:row, [42, 3.14, "Alice", "👋", <<0, 1, 2>>, nil]} =
319+
Sqlite3.step(conn, statement)
320+
end
321+
322+
test "handles repeating named parameters" do
323+
{:ok, conn} = Sqlite3.open(":memory:")
324+
325+
{:ok, statement} =
326+
Sqlite3.prepare(conn, "select :name, :name, :name")
327+
328+
:ok =
329+
Sqlite3.bind(statement, %{
330+
":name" => "Alice"
331+
})
332+
333+
assert {:row, ["Alice", "Alice", "Alice"]} = Sqlite3.step(conn, statement)
334+
end
335+
336+
test "raises an error when too few or too many named parameters" do
337+
{:ok, conn} = Sqlite3.open(":memory:")
338+
339+
{:ok, statement} =
340+
Sqlite3.prepare(conn, "select :name, :age")
341+
342+
assert_raise ArgumentError, ~r"expected 2 named arguments, got 1", fn ->
343+
Sqlite3.bind(statement, %{":name" => "Alice"})
344+
end
345+
346+
assert_raise ArgumentError, ~r"expected 2 named arguments, got 3", fn ->
347+
Sqlite3.bind(statement, %{":name" => "Alice", ":age" => 30, ":extra" => "value"})
348+
end
349+
end
301350
end
302351

303352
describe ".bind_text/3" do

0 commit comments

Comments
 (0)