From d1dffa1e8d1c4276cde44c6553b7a60ddfaf2cef Mon Sep 17 00:00:00 2001 From: Maas-Maarten Zeeman Date: Sat, 21 May 2022 09:30:56 +0200 Subject: [PATCH] Added step function in favour of multi_step --- c_src/esqlite3_nif.c | 207 ++++++++++++++++++++++++++---------------- src/esqlite3.erl | 83 ++++++++++++++++- src/esqlite3_nif.erl | 22 ++++- test/esqlite_test.erl | 13 +++ 4 files changed, 238 insertions(+), 87 deletions(-) diff --git a/c_src/esqlite3_nif.c b/c_src/esqlite3_nif.c index 941675c..9249312 100644 --- a/c_src/esqlite3_nif.c +++ b/c_src/esqlite3_nif.c @@ -40,6 +40,8 @@ typedef struct { /* prepared statement */ typedef struct { sqlite3_stmt *statement; + + int column_count; } esqlite3_stmt; /* data associated with ongoing backup */ @@ -590,7 +592,6 @@ make_binary(ErlNifEnv *env, const void *bytes, unsigned int size) return term; } -/* static ERL_NIF_TERM make_cell(ErlNifEnv *env, sqlite3_stmt *statement, unsigned int i) { @@ -610,78 +611,10 @@ make_cell(ErlNifEnv *env, sqlite3_stmt *statement, unsigned int i) case SQLITE_TEXT: return make_binary(env, sqlite3_column_text(statement, i), sqlite3_column_bytes(statement, i)); - default: - return make_atom(env, "should_not_happen"); } + return enif_raise_exception(env, make_atom(env, "internal_error")); } -*/ -/* -static ERL_NIF_TERM -make_row(ErlNifEnv *env, sqlite3_stmt *statement, ERL_NIF_TERM *array, int size) -{ - if(!array) - return make_error_tuple(env, "no_memory"); - - for(int i = 0; i < size; i++) - array[i] = make_cell(env, statement, i); - - return enif_make_tuple_from_array(env, array, size); -} -*/ - -/* -static ERL_NIF_TERM -do_multi_step(ErlNifEnv *env, sqlite3 *db, sqlite3_stmt *stmt, const ERL_NIF_TERM arg) -{ - ERL_NIF_TERM status; - ERL_NIF_TERM rows = enif_make_list_from_array(env, NULL, 0); - ERL_NIF_TERM *rowBuffer = NULL; - int rowBufferSize = 0; - - int chunk_size = 0; - enif_get_int(env, arg, &chunk_size); - - int rc = sqlite3_step(stmt); - while (rc == SQLITE_ROW && chunk_size-- > 0) - { - if (!rowBufferSize) - rowBufferSize = sqlite3_column_count(stmt); - if (rowBuffer == NULL) - rowBuffer = (ERL_NIF_TERM *) enif_alloc(sizeof(ERL_NIF_TERM)*rowBufferSize); - - rows = enif_make_list_cell(env, make_row(env, stmt, rowBuffer, rowBufferSize), rows); - - if (chunk_size > 0) - rc = sqlite3_step(stmt); - } - - switch(rc) { - case SQLITE_ROW: - status = make_atom(env, "rows"); - break; - case SQLITE_BUSY: - status = make_atom(env, "$busy"); - break; - case SQLITE_DONE: - // - // Automatically reset the statement after a done so - // column_names will work after the statement is done. - // - // Not resetting the statement can lead to vm crashes. - // - sqlite3_reset(stmt); - status = make_atom(env, "$done"); - break; - default: - // We use prepare_v2, so any error code can be returned. - return make_sqlite3_error_tuple(env, rc, db); - } - - enif_free(rowBuffer); - return enif_make_tuple2(env, status, rows); -} -*/ /* static ERL_NIF_TERM @@ -940,11 +873,13 @@ esqlite_prepare(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { esqlite3 *conn; - if(argc != 2) + if(argc != 3) { return enif_make_badarg(env); + } - if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) + if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) { return enif_make_badarg(env); + } ErlNifBinary bin; esqlite3_stmt *stmt; @@ -957,17 +892,24 @@ esqlite_prepare(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) return enif_make_badarg(env); } + unsigned int prep_flags; + if(!enif_get_uint(env, argv[2], &prep_flags)) { + return make_error_tuple(env, "invalid_chunk_size"); + } + stmt = enif_alloc_resource(esqlite3_stmt_type, sizeof(esqlite3_stmt)); if(!stmt) { return enif_raise_exception(env, make_atom(env, "no_memory")); } - rc = sqlite3_prepare_v2(conn->db, (char *) bin.data, bin.size, &(stmt->statement), &tail); + rc = sqlite3_prepare_v3(conn->db, (char *) bin.data, bin.size, prep_flags, &(stmt->statement), &tail); if(rc != SQLITE_OK) { enif_release_resource(stmt); return make_sqlite3_error_tuple(env, rc); } + stmt->column_count = sqlite3_column_count(stmt->statement); + esqlite_stmt = enif_make_resource(env, stmt); enif_release_resource(stmt); @@ -1400,6 +1342,115 @@ esqlite_bind_int(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) return make_atom(env, "ok"); } +static ERL_NIF_TERM +esqlite_bind_int64(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + int index; + ErlNifSInt64 value; + + if(argc != 3) { + 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_get_int(env, argv[1], &index)) { + return enif_make_badarg(env); + } + + if(!enif_get_int64(env, argv[2], &value)) { + return enif_make_badarg(env); + } + + int rc = sqlite3_bind_int64(stmt->statement, index, value); + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + return make_atom(env, "ok"); +} + +static ERL_NIF_TERM +esqlite_bind_double(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + int index; + double value; + + if(argc != 3) { + 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_get_int(env, argv[1], &index)) { + return enif_make_badarg(env); + } + + if(!enif_get_double(env, argv[2], &value)) { + return enif_make_badarg(env); + } + + int rc = sqlite3_bind_double(stmt->statement, index, value); + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc); + } + + return make_atom(env, "ok"); +} + +static ERL_NIF_TERM +esqlite_step(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3_stmt *stmt; + + if(argc != 1) { + 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")); + } + + int rc = sqlite3_step(stmt->statement); + switch(rc) { + case SQLITE_ROW: + { + ERL_NIF_TERM row = enif_make_list(env, 0); + for(int i=stmt->column_count; i-- > 0; ) { + row = enif_make_list_cell(env, make_cell(env, stmt->statement, i), row); + } + return row; + } + case SQLITE_DONE: + /* since 3.6.23.1 it is no longer required to do an explict reset. + */ + return make_atom(env, "done"); + case SQLITE_BUSY: + return make_atom(env, "busy"); + } + + return make_sqlite3_error_tuple(env, rc); +} + + /* * Backup functions @@ -1643,16 +1694,19 @@ static ErlNifFunc nif_funcs[] = { {"open", 1, esqlite_open, ERL_NIF_DIRTY_JOB_IO_BOUND}, {"close", 1, esqlite_close, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"prepare", 2, esqlite_prepare}, + {"prepare", 3, esqlite_prepare}, {"column_names", 1, esqlite_column_names}, {"column_decltypes", 1, esqlite_column_decltypes}, {"bind_int", 3, esqlite_bind_int}, + {"bind_int64", 3, esqlite_bind_int64}, + {"bind_double", 3, esqlite_bind_double}, + + {"step", 1, esqlite_step}, + /* - {"bind_int64", 3, esqlite_bind_blob}, - {"bind_double", 3, esqlite_bind_blob}, {"bind_text", 3, esqlite_bind_blob}, {"bind_blob", 3, esqlite_bind_blob}, {"bind_null", 2, esqlite_bind_blob}, @@ -1668,7 +1722,6 @@ static ErlNifFunc nif_funcs[] = { {"insert", 4, esqlite_insert}, {"last_insert_rowid", 3, esqlite_last_insert_rowid}, {"get_autocommit", 3, esqlite_get_autocommit}, - {"multi_step", 5, esqlite_multi_step, ERL_NIF_DIRTY_JOB_IO_BOUND}, {"reset", 4, esqlite_reset}, // TODO: {"esqlite_bind", 3, esqlite_bind_named}, diff --git a/src/esqlite3.erl b/src/esqlite3.erl index 0f929f3..b826300 100644 --- a/src/esqlite3.erl +++ b/src/esqlite3.erl @@ -20,30 +20,41 @@ %% higher-level export -export([ open/1, close/1, + prepare/2, + prepare/3, column_names/1, column_decltypes/1, - bind_int/3 + bind_int/3, + bind_int64/3, + bind_double/3, + + step/1 % set_update_hook/2, set_update_hook/3, % exec/2, exec/3, exec/4, % changes/1, changes/2, % insert/2, insert/3, % last_insert_rowid/1, +% % get_autocommit/1, get_autocommit/2, +% % step/1, step/2, +% % reset/1, % bind/2, bind/3, % fetchone/1, % fetchall/1, fetchall/2, fetchall/3, -% column_types/1, column_types/2, +% +% % backup_init/4, backup_init/5, % backup_finish/1, backup_finish/2, % backup_remaining/1, backup_remaining/2, % backup_pagecount/1, backup_pagecount/2, % backup_step/2, backup_step/3, +% % flush/0 ]). @@ -52,6 +63,10 @@ -define(DEFAULT_TIMEOUT, infinity). -define(DEFAULT_CHUNK_SIZE, 5000). + +-define(SQLITE_PREPARE_PERSISTENT, 16#01). +-define(SQLITE_PREPARE_NO_VTAB, 16#04). + -record(esqlite3, { db :: esqlite3_nif:esqlite3() }). @@ -71,6 +86,8 @@ %-type esqlite3_backup() :: #esqlite3_backup{}. -type sql() :: esqlite3_nif:sql(). +-type prepare_flags() :: persistent | no_vtab. + %% erlang -> sqlite type conversions %% %% 'undefined' -> null @@ -85,7 +102,7 @@ -type row() :: tuple(). % tuple of cell_type -type cell_type() :: undefined | integer() | binary() | float(). --export_type([esqlite3/0, esqlite3_stmt/0, sql/0, row/0, rowid/0, cell_type/0]). +-export_type([esqlite3/0, esqlite3_stmt/0, prepare_flags/0, sql/0, row/0, rowid/0, cell_type/0]). %% @doc Opens a sqlite3 database mentioned in Filename. %% @@ -389,8 +406,19 @@ close(#esqlite3{db=Connection}) -> when Connection :: esqlite3(), Sql :: sql(), PrepareResult :: {ok, esqlite3_stmt()} | {error, _}. -prepare(#esqlite3{db=Connection}, Sql) -> - case esqlite3_nif:prepare(Connection, Sql) of +prepare(Connection, Sql) -> + prepare(Connection, Sql, []). + +%% @doc Compile a SQL statement. Returns a cached compiled statement which can be used in +%% queries. +%% +-spec prepare(Connection, Sql, PrepareFlags) -> PrepareResult + when Connection :: esqlite3(), + Sql :: sql(), + PrepareFlags :: list(prepare_flags()), + PrepareResult :: {ok, esqlite3_stmt()} | {error, _}. +prepare(#esqlite3{db=Connection}, Sql, PrepareFlags) -> + case esqlite3_nif:prepare(Connection, Sql, props_to_prepare_flag(PrepareFlags)) of {ok, Stmt} -> {ok, #esqlite3_stmt{db=Connection, stmt=Stmt}}; {error, _}=Error -> @@ -405,6 +433,29 @@ prepare(#esqlite3{db=Connection}, Sql) -> bind_int(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> esqlite3_nif:bind_int(Stmt, Index, Value). +-spec bind_int64(Statement, Index, Value) -> BindResult + when Statement :: esqlite3_stmt(), + Index :: integer(), + Value :: integer(), + BindResult :: ok | {error, _}. +bind_int64(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> + esqlite3_nif:bind_int64(Stmt, Index, Value). + +-spec bind_double(Statement, Index, Value) -> BindResult + when Statement :: esqlite3_stmt(), + Index :: integer(), + Value :: float(), + BindResult :: ok | {error, _}. +bind_double(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> + esqlite3_nif:bind_double(Stmt, Index, Value). + +-spec step(Statement) -> StepResult + when Statement :: esqlite3_stmt(), + StepResult:: ok | {error, _}. +step(#esqlite3_stmt{stmt=Stmt}) -> + esqlite3_nif:step(Stmt). + + %% @doc Like prepare/2, but with an extra timeout value. %-spec prepare(sql(), connection(), timeout()) -> {ok, statement()} | {error, _}. %prepare(Sql, #connection{raw_connection=RawConnection}, Timeout) -> @@ -673,3 +724,25 @@ column_decltypes(#esqlite3_stmt{stmt=Stmt}) -> % after % 0 -> ok % end. + +%% +%% Helpers +%% + + +props_to_prepare_flag(Props) -> + Flag = case proplists:get_value(no_vtab, Props, false) of + true -> ?SQLITE_PREPARE_NO_VTAB; + false -> 0 + end, + case proplists:get_value(persistent, Props, false) of + true -> Flag bor ?SQLITE_PREPARE_PERSISTENT; + false -> Flag + end. + + + + + + + diff --git a/src/esqlite3_nif.erl b/src/esqlite3_nif.erl index f5b9f1c..89601d0 100644 --- a/src/esqlite3_nif.erl +++ b/src/esqlite3_nif.erl @@ -22,12 +22,16 @@ -export([ open/1, close/1, - prepare/2, + prepare/3, column_names/1, column_decltypes/1, bind_int/3, + bind_int64/3, + bind_double/3, + + step/1, interrupt/1 % set_update_hook/4, @@ -87,18 +91,26 @@ close(_Db) -> %% @doc Compile a sql statement. %% --spec prepare(Connection, Sql) -> PrepareResult +-spec prepare(Connection, Sql, PrepareFlags) -> PrepareResult when Connection :: esqlite3(), Sql :: sql(), + PrepareFlags :: non_neg_integer(), PrepareResult :: {ok, esqlite3_stmt()} | {error, _}. -prepare(_Connection, _Sql) -> +prepare(_Connection, _Sql, _PrepareFlags) -> erlang:nif_error(nif_library_not_loaded). - - bind_int(_Statement, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). +bind_int64(_Statement, _Index, _Value) -> + erlang:nif_error(nif_library_not_loaded). + +bind_double(_Statement, _Index, _Value) -> + erlang:nif_error(nif_library_not_loaded). + +step(_Statement) -> + erlang:nif_error(nif_library_not_loaded). + % -spec set_update_hook((), pid(), pid()) -> ok | {error, _}. %set_update_hook(_Db, _Ref, _Dest, _Pid) -> % erlang:nif_error(nif_library_not_loaded). diff --git a/test/esqlite_test.erl b/test/esqlite_test.erl index 3d37fba..2ecef5c 100644 --- a/test/esqlite_test.erl +++ b/test/esqlite_test.erl @@ -78,6 +78,19 @@ column_decltypes_test() -> ok. +step_test() -> + {ok, C} = esqlite3:open(":memory:"), + {ok, Stmt} = esqlite3:prepare(C, "select 1, 2, 3;" ), + + ?assertEqual([1,2,3], esqlite3:step(Stmt)), + ?assertEqual(done, esqlite3:step(Stmt)), + + %% After the done, the statement is reset and + ?assertEqual([1,2,3], esqlite3:step(Stmt)), + ?assertEqual(done, esqlite3:step(Stmt)), + + ok. + %iodata_test() -> % {ok, C} = esqlite3:open(":memory:"),