From 16580ce11d947d0939e29d9373317dd826cc1ced Mon Sep 17 00:00:00 2001 From: Dmitry Matveyev Date: Sun, 8 Jan 2023 13:54:27 +0600 Subject: [PATCH] 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"}] --- CONTRIBUTERS | 1 + c_src/esqlite3_nif.c | 33 +++++++++++++++++++++++++++++++++ src/esqlite3.erl | 38 +++++++++++++++++++++++++++++++++++++- src/esqlite3_nif.erl | 8 ++++++++ test/esqlite_test.erl | 20 ++++++++++++++++++++ 5 files changed, 99 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTERS b/CONTRIBUTERS index 358788b..fcae24f 100644 --- a/CONTRIBUTERS +++ b/CONTRIBUTERS @@ -10,3 +10,4 @@ Aleph Archives Qing Liang +Dmitry Matveyev diff --git a/c_src/esqlite3_nif.c b/c_src/esqlite3_nif.c index 2b1a7c3..f604109 100644 --- a/c_src/esqlite3_nif.c +++ b/c_src/esqlite3_nif.c @@ -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}, diff --git a/src/esqlite3.erl b/src/esqlite3.erl index 8a4cdf0..a2d3e94 100644 --- a/src/esqlite3.erl +++ b/src/esqlite3.erl @@ -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); diff --git a/src/esqlite3_nif.erl b/src/esqlite3_nif.erl index e341f82..d757c0b 100644 --- a/src/esqlite3_nif.erl +++ b/src/esqlite3_nif.erl @@ -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(). diff --git a/test/esqlite_test.erl b/test/esqlite_test.erl index 0e25ff0..54948df 100644 --- a/test/esqlite_test.erl +++ b/test/esqlite_test.erl @@ -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;"),