From 20ad5138a15316b126d1e122e7a874b79cd57ef5 Mon Sep 17 00:00:00 2001 From: Maas-Maarten Zeeman Date: Mon, 23 May 2022 22:39:19 +0200 Subject: [PATCH] Re-added higher level api --- c_src/esqlite3_nif.c | 2 +- src/esqlite3.erl | 154 ++++++++++++++++++---------------- src/esqlite3_nif.erl | 31 +------ test/esqlite_test.erl | 186 ++++++++++++++++++++---------------------- 4 files changed, 177 insertions(+), 196 deletions(-) diff --git a/c_src/esqlite3_nif.c b/c_src/esqlite3_nif.c index 10755fa..409c251 100644 --- a/c_src/esqlite3_nif.c +++ b/c_src/esqlite3_nif.c @@ -815,7 +815,7 @@ esqlite_bind_text(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) return enif_make_badarg(env); } - if(!enif_inspect_iolist_as_binary(env, argv[1], &text)) { + if(!enif_inspect_iolist_as_binary(env, argv[2], &text)) { return enif_make_badarg(env); } diff --git a/src/esqlite3.erl b/src/esqlite3.erl index 2c5aafb..d6f5f0d 100644 --- a/src/esqlite3.erl +++ b/src/esqlite3.erl @@ -48,11 +48,12 @@ bind_null/2, step/1, - reset/1 + reset/1, + q/2, q/3, -% fetchone/1, -% fetchall/1, fetchall/2, fetchall/3, + fetchone/1, + fetchall/1 % % backup_init/4, backup_init/5, % backup_finish/1, backup_finish/2, @@ -160,44 +161,38 @@ error_info(#esqlite3{db=Connection}) -> set_update_hook(#esqlite3{db=Connection}, MaybePid) when is_pid(MaybePid) orelse MaybePid =:= undefined -> esqlite3_nif:set_update_hook(Connection, MaybePid). - %%% %%% q %%% -% + %%% @doc Execute a sql statement, returns a list with tuples. %-spec q(sql(), connection()) -> list(row()) | {error, _}. -%q(Sql, Connection) -> -% q(Sql, [], Connection, ?DEFAULT_TIMEOUT). -% -%%% @doc Execute statement, bind args and return a list with tuples as result. -%-spec q(sql(), list(), connection()) -> list(row()) | {error, _}. -%%q(Sql, Args, Connection) -> -% q(Sql, Args, Connection, ?DEFAULT_TIMEOUT). -% +q(Connection, Sql) -> + q(Connection, Sql, []). %% @doc Execute statement, bind args and return a list with tuples as result restricted by timeout. %-spec q(sql(), list(), connection(), timeout()) -> list(row()) | {error, _}. -%q(Sql, [], Connection, Timeout) -> -% case prepare(Sql, Connection, Timeout) of -% {ok, Statement} -> -% fetchall(Statement, ?DEFAULT_CHUNK_SIZE, Timeout); -% {error, _Msg}=Error -> -% Error -% end; -%q(Sql, Args, Connection, Timeout) -> -% case prepare(Sql, Connection, Timeout) of -% {ok, Statement} -> -% case bind(Statement, Args, Timeout) of -% ok -> -% fetchall(Statement, ?DEFAULT_CHUNK_SIZE, Timeout); -% {error, _}=Error -> -% Error -% end; -% {error, _Msg}=Error -> -% Error -% end. -% +q(Connection, Sql, []) -> + case prepare(Connection, Sql) of + {ok, Statement} -> + fetchall(Statement); + {error, _Msg}=Error -> + Error + end; + +q(Connection, Sql, Args) -> + case prepare(Connection, Sql) of + {ok, Statement} -> + case bind(Statement, Args) of + ok -> + fetchall(Statement); + {error, _}=Error -> + Error + end; + {error, _Msg}=Error -> + Error + end. + %%% %% map %%% @@ -280,45 +275,63 @@ set_update_hook(#esqlite3{db=Connection}, MaybePid) when is_pid(MaybePid) orelse %% %-spec fetchone(statement()) -> tuple(). -%fetchone(Statement) -> -% case try_multi_step(Statement, 1, [], 0) of -% {'$done', []} -> ok; -% {error, _} = E -> E; -% {rows, [Row | []]} -> Row -% end. +fetchone(Statement) -> + case step(Statement) of + Row when is_list(Row) -> + Row; + '$done' -> + ok; + {error, _} = E -> + E + end. % %%% @doc Fetch all records %%% @param Statement is prepared sql statement %-spec fetchall(statement()) -> list(row()) | {error, _}. -%fetchall(Statement) -> -% fetchall(Statement, ?DEFAULT_CHUNK_SIZE, ?DEFAULT_TIMEOUT). -% -%% @doc Fetch all records -%% @param Statement is prepared sql statement -%% @param ChunkSize is a count of rows to read from sqlite and send to erlang process in one bulk. -%% Decrease this value if rows are heavy. Default value is 5000 (DEFAULT_CHUNK_SIZE). -%-spec fetchall(statement(), pos_integer()) -> list(row()) | {error, _}. -%fetchall(Statement, ChunkSize) -> -% fetchall(Statement, ChunkSize, ?DEFAULT_TIMEOUT). -% -%%% @doc Fetch all records -%%% @param Statement is prepared sql statement -%% @param ChunkSize is a count of rows to read from sqlite and send to erlang process in one bulk. -%% Decrease this value if rows are heavy. Default value is 5000 (DEFAULT_CHUNK_SIZE). -%% @param Timeout is timeout per each request of the one bulk -%-spec fetchall(statement(), pos_integer(), timeout()) -> list(row()) | {error, _}. -%fetchall(Statement, ChunkSize, Timeout) -> -% case fetchall_internal(Statement, ChunkSize, [], Timeout) of -% {'$done', Rows} -> lists:reverse(Rows); -% {error, _} = E -> E -% end. +fetchall(Statement) -> + fetchall1(Statement, []). + +fetchall1(Statement, Acc) -> + case step(Statement) of + Row when is_list(Row) -> + fetchall1(Statement, [Row|Acc]); + '$done' -> + lists:reverse(Acc); + {error, _} = E -> + E + end. + +bind(Statement, Args) -> + bind1(Statement, 1, Args). + +bind1(_Statement, _Column, []) -> + ok; +bind1(Statement, Column, [Arg | Args]) -> + bind_arg(Statement, Column, Arg), + bind1(Statement, Column + 1, Args). + +% Do automatic conversion +bind_arg(Statement, Column, undefined) -> + bind_null(Statement, Column); +bind_arg(Statement, Column, null) -> + bind_null(Statement, Column); +bind_arg(Statement, Column, Atom) when is_atom(Atom) -> + bind_text(Statement, Column, atom_to_binary(Atom, utf8)); +bind_arg(Statement, Column, Int) when is_integer(Int) -> + bind_int64(Statement, Column, Int); +bind_arg(Statement, Column, Float) when is_float(Float) -> + bind_double(Statement, Column, Float); +bind_arg(Statement, Column, Bin) when is_binary(Bin) -> + bind_text(Statement, Column, Bin); +bind_arg(Statement, Column, String) when is_list(String) -> + bind_text(Statement, Column, String). %% @doc Get the last insert rowid. %% --spec last_insert_rowid(Connection) -> RowidResult - when Connection :: esqlite3(), - RowidResult :: integer() | {error, closed}. +-spec last_insert_rowid(Connection) -> RowidResult when + Connection :: esqlite3(), + RowidResult :: integer() | {error, closed}. last_insert_rowid(#esqlite3{db=Connection}) -> esqlite3_nif:last_insert_rowid(Connection). @@ -400,6 +413,14 @@ bind_int64(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> bind_double(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> esqlite3_nif:bind_double(Stmt, Index, Value). +-spec bind_text(Statement, Index, Value) -> BindResult + when Statement :: esqlite3_stmt(), + Index :: integer(), + Value :: iodata(), + BindResult :: ok | {error, _}. +bind_text(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> + esqlite3_nif:bind_text(Stmt, Index, Value). + -spec bind_blob(Statement, Index, Value) -> BindResult when Statement :: esqlite3_stmt(), Index :: integer(), @@ -631,8 +652,3 @@ props_to_prepare_flag(Props) -> end. - - - - - diff --git a/src/esqlite3_nif.erl b/src/esqlite3_nif.erl index 3dcbff4..c49dabf 100644 --- a/src/esqlite3_nif.erl +++ b/src/esqlite3_nif.erl @@ -141,10 +141,10 @@ bind_int64(_Statement, _Index, _Value) -> bind_double(_Statement, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -bind_blob(_Statement, _Index, _Value) -> +bind_text(_Statement, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -bind_text(_Statement, _Index, _Value) -> +bind_blob(_Statement, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). bind_null(_Statement, _Index) -> @@ -156,27 +156,6 @@ step(_Statement) -> reset(_Statement) -> erlang:nif_error(nif_library_not_loaded). - -%% @doc Exec the query. -%% -%% Sends an asynchronous exec command over the connection and returns -%% ok immediately. -%% -%% When the statement is executed Dest will receive message {Ref, answer()} -%% with answer() integer | {error, reason()} -%% -%-spec exec(esqlite3(), reference(), pid(), sql()) -> ok | {error, _}. -%%exec(_Db, _Ref, _Dest, _Sql) -> -% erlang:nif_error(nif_library_not_loaded). - -%% @doc Get the number of affected rows of last statement -%% -%% When the statement is executed Dest will receive message {Ref, answer()} -%% with answer() integer | {error, reason()} -%-spec changes(esqlite3(), reference(), pid()) -> ok | {error, _}. -%changes(_Db, _Ref, _Dest) -> -% erlang:nif_error(nif_library_not_loaded). - %% @doc %% %-spec multi_step(esqlite3(), esqlite3_stmt(), pos_integer(), reference(), pid()) -> ok | {error, _}. @@ -195,12 +174,6 @@ reset(_Statement) -> %finalize(_Db, _Stmt, _Ref, _Dest) -> % erlang:nif_error(nif_library_not_loaded). -%% @doc Bind parameters to a prepared statement. -%% -%-spec bind(esqlite3(), esqlite3_stmt(), reference(), pid(), list(any())) -> ok | {error, _}. -%bind(_Db, _Stmt, _Ref, _Dest, _Args) -> -% erlang:nif_error(nif_library_not_loaded). - %% @doc Retrieve the column names of the prepared statement %% -spec column_names(esqlite3_stmt()) -> list(binary()) | {error, _}. diff --git a/test/esqlite_test.erl b/test/esqlite_test.erl index c36b017..847d73e 100644 --- a/test/esqlite_test.erl +++ b/test/esqlite_test.erl @@ -64,13 +64,9 @@ column_names_test() -> column_decltypes_test() -> {ok, C} = esqlite3:open(":memory:"), - {ok, Stmt} = esqlite3:prepare(C, "select 1, 2, 3"), - ?assertEqual([undefined, undefined, undefined], esqlite3:column_decltypes(Stmt)), - %% Need to be able to define tables. - ok. step_test() -> @@ -197,7 +193,7 @@ prepare2_test() -> ok = esqlite3:exec(Db, "insert into test_table values('hello4', 13);"), %% Check if the values are there. - %% [{<<"one">>, 2}, {<<"hello4">>, 13}] = esqlite3:q(Db, "select * from test_table order by two"), + [[<<"one">>, 2], [<<"hello4">>, 13]] = esqlite3:q(Db, "select * from test_table order by two"), esqlite3:exec(Db, "commit;"), esqlite3:close(Db), @@ -266,94 +262,91 @@ prepare2_test() -> % % ok. -%bind_for_queries_test() -> -% {ok, Db} = esqlite3:open(":memory:"), -% -% ok = esqlite3:exec("begin;", Db), -% ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db), -% ok = esqlite3:exec("commit;", Db), +bind_for_queries_test() -> + {ok, Db} = esqlite3:open(":memory:"), -% ?assertEqual([{1}], esqlite3:q(<<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, -% [test_table], Db)), -% ?assertEqual([{1}], esqlite3:q(<<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, -% ["test_table"], Db)), -% ?assertEqual([{1}], esqlite3:q(<<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, -% [<<"test_table">>], Db)), -% ?assertEqual([{1}], esqlite3:q(<<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, -% [[<<"test_table">>]], Db)), -% -% ok. + ok = esqlite3:exec(Db, "begin;"), + ok = esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"), + ok = esqlite3:exec(Db, "commit;"), -%column_names_test() -> -% {ok, Db} = esqlite3:open(":memory:"), -% ok = esqlite3:exec("begin;", Db), -% ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db), -% ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db), -% ok = esqlite3:exec("insert into test_table values('hello2', 20);", Db), -% ok = esqlite3:exec("commit;", Db), + ?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, + [test_table])), + ?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, + ["test_table"])), + ?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, + [<<"test_table">>])), + ?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>, + [[<<"test_table">>]])), + + ok. + +column_names2_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, "insert into test_table values('hello1', 10);"), + ok = esqlite3:exec(Db, "insert into test_table values('hello2', 20);"), + ok = esqlite3:exec(Db, "commit;"), %% All columns -% {ok, Stmt} = esqlite3:prepare("select * from test_table", Db), -% {one, two} = esqlite3:column_names(Stmt), -% {row, {<<"hello1">>, 10}} = esqlite3:step(Stmt), -% {one, two} = esqlite3:column_names(Stmt), -% {row, {<<"hello2">>, 20}} = esqlite3:step(Stmt), -% {one, two} = esqlite3:column_names(Stmt), -% '$done' = esqlite3:step(Stmt), -% {one, two} = esqlite3:column_names(Stmt), + {ok, Stmt} = esqlite3:prepare(Db, "select * from test_table"), + [<<"one">>, <<"two">>] = esqlite3:column_names(Stmt), + [<<"hello1">>, 10] = esqlite3:step(Stmt), + [<<"one">>, <<"two">>] = esqlite3:column_names(Stmt), + [<<"hello2">>, 20] = esqlite3:step(Stmt), + [<<"one">>, <<"two">>] = esqlite3:column_names(Stmt), + '$done' = esqlite3:step(Stmt), + [<<"one">>, <<"two">>] = esqlite3:column_names(Stmt), %% One column -% {ok, Stmt2} = esqlite3:prepare("select two from test_table", Db), -% {two} = esqlite3:column_names(Stmt2), -% {row, {10}} = esqlite3:step(Stmt2), -% {two} = esqlite3:column_names(Stmt2), -% {row, {20}} = esqlite3:step(Stmt2), -% {two} = esqlite3:column_names(Stmt2), -% '$done' = esqlite3:step(Stmt2), -% {two} = esqlite3:column_names(Stmt2), + {ok, Stmt2} = esqlite3:prepare(Db, "select two from test_table"), + [<<"two">>] = esqlite3:column_names(Stmt2), + [10] = esqlite3:step(Stmt2), + [<<"two">>] = esqlite3:column_names(Stmt2), + [20] = esqlite3:step(Stmt2), + [<<"two">>] = esqlite3:column_names(Stmt2), + '$done' = esqlite3:step(Stmt2), + [<<"two">>] = esqlite3:column_names(Stmt2), %% No columns -% {ok, Stmt3} = esqlite3:prepare("values(1);", Db), -% {column1} = esqlite3:column_names(Stmt3), -% {row, {1}} = esqlite3:step(Stmt3), -% {column1} = esqlite3:column_names(Stmt3), + {ok, Stmt3} = esqlite3:prepare(Db, "values(1);"), + [<<"column1">>] = esqlite3:column_names(Stmt3), + [1] = esqlite3:step(Stmt3), + [<<"column1">>] = esqlite3:column_names(Stmt3), %% Things get a bit weird when you retrieve the column name %% when calling an aggragage function. -%% {ok, Stmt4} = esqlite3:prepare("select date('now');", Db), -% {'date(\'now\')'} = esqlite3:column_names(Stmt4), -% {row, {Date}} = esqlite3:step(Stmt4), -% true = is_binary(Date), + {ok, Stmt4} = esqlite3:prepare(Db, "select date('now');"), + [<<"date(\'now\')">>] = esqlite3:column_names(Stmt4), + [Date] = esqlite3:step(Stmt4), + true = is_binary(Date), %% Some statements have no column names -% {ok, Stmt5} = esqlite3:prepare("create table dummy(a, b, c);", Db), -% {} = esqlite3:column_names(Stmt5), -% -% ok. + {ok, Stmt5} = esqlite3:prepare(Db, "create table dummy(a, b, c);"), + [] = esqlite3:column_names(Stmt5), + + ok. + +column_types_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, "insert into test_table values('hello1', 10);"), + ok = esqlite3:exec(Db, "insert into test_table values('hello2', 20);"), + ok = esqlite3:exec(Db, "commit;"), -%column_types_test() -> -% {ok, Db} = esqlite3:open(":memory:"), -% ok = esqlite3:exec("begin;", Db), -% ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db), -% ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db), -% ok = esqlite3:exec("insert into test_table values('hello2', 20);", Db), -% ok = esqlite3:exec("commit;", Db), -% %% All columns -% {ok, Stmt} = esqlite3:prepare("select * from test_table", Db), -% ?assertEqual({'varchar(10)', 'INT'}, esqlite3:column_types(Stmt)), -% {row, {<<"hello1">>, 10}} = esqlite3:step(Stmt), -% {'varchar(10)', 'INT'} = esqlite3:column_types(Stmt), -% {row, {<<"hello2">>, 20}} = esqlite3:step(Stmt), -% {'varchar(10)', 'INT'} = esqlite3:column_types(Stmt), -% '$done' = esqlite3:step(Stmt), -% {'varchar(10)', 'INT'} = esqlite3:column_types(Stmt), -% -% %% Some statements have no column types -% {ok, Stmt2} = esqlite3:prepare("create table dummy(a, b, c);", Db), -% {} = esqlite3:column_types(Stmt2), -% -% ok. + {ok, Stmt} = esqlite3:prepare(Db, "select * from test_table"), + ?assertEqual([<<"varchar(10)">>, <<"INT">>], esqlite3:column_decltypes(Stmt)), + + %% Some statements have no column types + {ok, Stmt2} = esqlite3:prepare(Db, "create table dummy(a, b, c);"), + [] = esqlite3:column_decltypes(Stmt2), + + {ok, Stmt3} = esqlite3:prepare(Db, "select 1, 2, 3;"), + [undefined, undefined, undefined] = esqlite3:column_decltypes(Stmt3), + + ok. nil_column_decltypes_test() -> {ok, Db} = esqlite3:open(":memory:"), @@ -636,27 +629,26 @@ sqlite_source_id_test() -> % end % end. -%garbage_collect_test() -> -% F = fun() -> -% {ok, Db} = esqlite3:open(":memory:"), -% [] = esqlite3:q("create table test(one, two, three)", Db), -% [] = esqlite3:q("insert into test values(1, '2', 3.0)", Db), -% {ok, Stmt} = esqlite3:prepare("select * from test", Db), -% {row, {1, <<"2">>, 3.0}} = esqlite3:step(Stmt), -% '$done' = esqlite3:step(Stmt), -% ok = esqlite3:close(Db) -% end, -% -% [spawn(F) || _X <- lists:seq(0,30)], -% receive after 500 -> ok end, -% erlang:garbage_collect(), -% -% [spawn(F) || _X <- lists:seq(0,30)], -% receive after 500 -> ok end, -% erlang:garbage_collect(), +garbage_collect_test() -> + F = fun() -> + {ok, Db} = esqlite3:open(":memory:"), + [] = esqlite3:q(Db, "create table test(one, two, three)"), + [] = esqlite3:q(Db, "insert into test values(1, '2', 3.0)"), + {ok, Stmt} = esqlite3:prepare(Db, "select * from test"), + [1, <<"2">>, 3.0] = esqlite3:step(Stmt), + '$done' = esqlite3:step(Stmt), + ok = esqlite3:close(Db) + end, + [spawn(F) || _X <- lists:seq(0,30)], + receive after 500 -> ok end, + erlang:garbage_collect(), -% ok. + [spawn(F) || _X <- lists:seq(0,30)], + receive after 500 -> ok end, + erlang:garbage_collect(), + + ok. %% %% Helpers