Add named parameters support

SQLite supports named parameters in either of these forms:
- $NAME
- :NAME
- @NAME

These parameters should be supplied to the query either in a form of a map
or as a list, the key is always iodata:
- #{":myparam" => "value"}
- [{"$myparam", 1}]
- [{text, "@myparam", "value"}]
This commit is contained in:
Dmitry Matveyev
2023-01-08 13:54:27 +06:00
parent c71ad3eb6b
commit 16580ce11d
5 changed files with 99 additions and 1 deletions

View File

@@ -10,3 +10,4 @@
Aleph Archives
Qing Liang <qing.liang.cn@gmail.com>
Dmitry Matveyev <public@greenfork.me>

View File

@@ -753,6 +753,38 @@ esqlite_bind_null(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
return make_atom(env, "ok");
}
static ERL_NIF_TERM
esqlite_bind_parameter_index(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
esqlite3_stmt *stmt;
ErlNifBinary bin;
int index;
ERL_NIF_TERM eos = enif_make_int(env, 0);
if(argc != 2) {
return enif_make_badarg(env);
}
if(!enif_get_resource(env, argv[0], esqlite3_stmt_type, (void **) &stmt)) {
return enif_make_badarg(env);
}
if(!stmt->statement) {
return enif_raise_exception(env, make_atom(env, "no_prepared_statement"));
}
if(!enif_inspect_iolist_as_binary(env, enif_make_list2(env, argv[1], eos), &bin)) {
return enif_make_badarg(env);
}
index = sqlite3_bind_parameter_index(stmt->statement, bin.data);
if(index == 0) {
return enif_make_atom(env, "error");
}
return make_ok_tuple(env, enif_make_int(env, index));
}
static ERL_NIF_TERM
esqlite_step(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
@@ -1202,6 +1234,7 @@ static ErlNifFunc nif_funcs[] = {
{"bind_text", 3, esqlite_bind_text},
{"bind_blob", 3, esqlite_bind_blob},
{"bind_null", 2, esqlite_bind_null},
{"bind_parameter_index", 2, esqlite_bind_parameter_index},
{"step", 1, esqlite_step, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"reset", 1, esqlite_reset},

View File

@@ -295,8 +295,17 @@ prepare(#esqlite3{db=Connection}, Sql, PrepareFlags) ->
%% @doc Bind an array of values as parameters of a prepared statement
-spec bind(Statement, Args) -> Result when
Statement :: esqlite3_stmt(),
Args :: list(),
Args :: list() | map(),
Result :: ok | {error, _}.
%% Named parameters
bind(#esqlite3_stmt{}=Statement, Args) when is_map(Args) ->
bind(Statement, maps:to_list(Args));
bind(#esqlite3_stmt{}=Statement, [{_Type, _ParameterName, _Value} | _] = Args) ->
bind2(Statement, Args);
bind(#esqlite3_stmt{}=Statement, [{ParameterName, _Value} | _] = Args)
when is_binary(ParameterName); is_list(ParameterName) ->
bind2(Statement, Args);
%% Anonymous parameters
bind(#esqlite3_stmt{}=Statement, Args) when is_list(Args) ->
bind1(Statement, 1, Args).
@@ -310,6 +319,33 @@ bind1(Statement, Column, [Arg | Args]) ->
Error
end.
bind2(_Statement, []) ->
ok;
bind2(#esqlite3_stmt{stmt=Stmt}=Statement, [{Type, ParameterName, Value} | Args]) ->
case esqlite3_nif:bind_parameter_index(Stmt, ParameterName) of
{ok, Column} ->
case bind_arg(Statement, Column, {Type, Value}) of
ok ->
bind2(Statement, Args);
{error, _}=Error ->
Error
end;
error ->
{error, named_parameter_not_found}
end;
bind2(#esqlite3_stmt{stmt=Stmt}=Statement, [{ParameterName, Value} | Args]) ->
case esqlite3_nif:bind_parameter_index(Stmt, ParameterName) of
{ok, Column} ->
case bind_arg(Statement, Column, Value) of
ok ->
bind2(Statement, Args);
{error, _}=Error ->
Error
end;
error ->
{error, named_parameter_not_found}
end.
% Bind with automatic tyoe conversion
bind_arg(Statement, Column, undefined) ->
bind_null(Statement, Column);

View File

@@ -42,6 +42,7 @@
bind_text/3,
bind_blob/3,
bind_null/2,
bind_parameter_index/2,
step/1,
@@ -194,6 +195,13 @@ bind_blob(_Statement, _Index, _Value) ->
bind_null(_Statement, _Index) ->
erlang:nif_error(nif_library_not_loaded).
-spec bind_parameter_index(Statement, ParameterName) -> Result when
Statement :: esqlite3_stmt_ref(),
ParameterName :: iodata(),
Result :: {ok, integer()} | error.
bind_parameter_index(_Statement, _ParameterName) ->
erlang:nif_error(nif_library_not_loaded).
-spec step(Statement) -> StepResult when
Statement :: esqlite3_stmt_ref(),
StepResult :: row() | '$done' | error().

View File

@@ -281,6 +281,26 @@ bind_for_queries_test() ->
ok.
named_bind_for_queries_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec(Db, "begin;"),
ok = esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"),
ok = esqlite3:exec(Db, "commit;"),
?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=:name;">>,
[{<<":name">>, "test_table"}])),
?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=$name;">>,
[{"$name", test_table}])),
?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=@name;">>,
[{"@name", <<"test_table">>}])),
?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=:name;">>,
#{":name" => test_table})),
?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=:name;">>,
[{text, ":name", "test_table"}])),
ok.
column_names2_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec(Db, "begin;"),