From 8aae31abdaa46249c5aff1ad712ab9f0fe6d7570 Mon Sep 17 00:00:00 2001 From: Maas-Maarten Zeeman Date: Sun, 22 May 2022 13:07:57 +0200 Subject: [PATCH] Added update hook --- c_src/esqlite3_nif.c | 240 +++++++++++++++--------------------------- src/esqlite3.erl | 44 ++++---- src/esqlite3_nif.erl | 19 ++-- test/esqlite_test.erl | 34 ++++-- 4 files changed, 137 insertions(+), 200 deletions(-) diff --git a/c_src/esqlite3_nif.c b/c_src/esqlite3_nif.c index b76c9d2..dd289e9 100644 --- a/c_src/esqlite3_nif.c +++ b/c_src/esqlite3_nif.c @@ -1,5 +1,5 @@ /* - * Copyright 2011 - 2017 Maas-Maarten Zeeman + * Copyright 2011 - 2022 Maas-Maarten Zeeman * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,8 @@ static ErlNifResourceType *esqlite3_backup_type = NULL; /* database connection context */ typedef struct { sqlite3 *db; + + ErlNifPid *update_hook_pid; } esqlite3; /* prepared statement */ @@ -279,9 +281,8 @@ make_extended_error_tuple(ErlNifEnv *env, int code) { case SQLITE_OK_SYMLINK: /* internal use only */ return make_two_atom_tuple(env, "ok", "symlink"); - default: - return make_two_atom_tuple(env, "error", enif_make_int(env, code)); } + return enif_make_tuple2(env, make_atom(env, "error"), enif_make_int(env, code)); } static const char * @@ -335,45 +336,6 @@ destruct_esqlite3_backup(ErlNifEnv *env, void *arg) backup->backup = NULL; } -/* -void -update_callback(void *arg, int sqlite_operation_type, char const *sqlite_database, char const *sqlite_table, sqlite3_int64 sqlite_rowid) -{ - esqlite3 *db = (esqlite3 *)arg; - esqlite_command *cmd = NULL; - ERL_NIF_TERM type, table, rowid; - - if(db == NULL) - return; - - cmd = command_create(); - if(!cmd) - return; - - rowid = enif_make_int64(cmd->env, sqlite_rowid); - table = enif_make_string(cmd->env, sqlite_table, ERL_NIF_LATIN1); - - switch(sqlite_operation_type) { - case SQLITE_INSERT: - type = make_atom(cmd->env, "insert"); - break; - case SQLITE_DELETE: - type = make_atom(cmd->env, "delete"); - break; - case SQLITE_UPDATE: - type = make_atom(cmd->env, "update"); - break; - default: - return; - } - - cmd->type = cmd_notification; - cmd->arg = enif_make_tuple3(cmd->env, type, table, rowid); - - push_command(cmd->env, db, cmd); -} -*/ - /* static ERL_NIF_TERM do_set_update_hook(ErlNifEnv *env, esqlite3 *conn, const ERL_NIF_TERM arg) @@ -392,80 +354,6 @@ do_set_update_hook(ErlNifEnv *env, esqlite3 *conn, const ERL_NIF_TERM arg) } */ -/* -static ERL_NIF_TERM -do_exec(ErlNifEnv *env, esqlite3 *conn, const ERL_NIF_TERM arg) -{ - ErlNifBinary bin; - int rc; - ERL_NIF_TERM eos = enif_make_int(env, 0); - - if(!enif_inspect_iolist_as_binary(env, enif_make_list2(env, arg, eos), &bin)) { - return make_error_tuple(env, "no_iodata"); - } - - rc = sqlite3_exec(conn->db, (char *) bin.data, NULL, NULL, NULL); - if(rc != SQLITE_OK) - return make_sqlite3_error_tuple(env, rc, conn->db); - - return make_atom(env, "ok"); -} - */ - -/* - * Nr of changes -static ERL_NIF_TERM -do_changes(ErlNifEnv *env, esqlite3 *conn, const ERL_NIF_TERM arg) -{ - if(!conn->db) { - return make_error_tuple(env, "closed"); - } - - sqlite3_int64 changes = sqlite3_changes64(conn->db); - ERL_NIF_TERM changes_term = enif_make_int64(env, changes); - - return make_ok_tuple(env, changes_term); -} - */ - -/* -* insert action -static ERL_NIF_TERM -do_insert(ErlNifEnv *env, esqlite3 *conn, const ERL_NIF_TERM arg) -{ - ErlNifBinary bin; - int rc; - ERL_NIF_TERM eos = enif_make_int(env, 0); - - if(!enif_inspect_iolist_as_binary(env, enif_make_list2(env, arg, eos), &bin)) { - return make_error_tuple(env, "no_iodata"); - } - - rc = sqlite3_exec(conn->db, (char *) bin.data, NULL, NULL, NULL); - if(rc != SQLITE_OK) - return make_sqlite3_error_tuple(env, rc, conn->db); - sqlite3_int64 last_rowid = sqlite3_last_insert_rowid(conn->db); - ERL_NIF_TERM last_rowid_term = enif_make_int64(env, last_rowid); - return make_ok_tuple(env, last_rowid_term); -} -*/ - -/* - * Return the last inserted rowid -static ERL_NIF_TERM -do_last_insert_rowid(ErlNifEnv *env, esqlite3 *conn) -{ - if(!conn->db) { - return make_error_tuple(env, "closed"); - } - - sqlite3_int64 last_rowid = sqlite3_last_insert_rowid(conn->db); - ERL_NIF_TERM last_rowid_term = enif_make_int64(env, last_rowid); - - return make_ok_tuple(env, last_rowid_term); -} - - */ /* static int @@ -854,17 +742,92 @@ esqlite_close(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) 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); + } rc = sqlite3_close_v2(conn->db); - if(rc != SQLITE_OK) + if(rc != SQLITE_OK) { return make_sqlite3_error_tuple(env, rc); + } conn->db = NULL; return make_atom(env, "ok"); } +void +update_callback(void *arg, int sqlite_operation_type, char const *sqlite_database, char const *sqlite_table, sqlite3_int64 sqlite_rowid) +{ + esqlite3 *conn = (esqlite3 *)arg; + + if(conn == NULL) { + return; + } + + /* Create a message environment */ + ErlNifEnv *msg_env = enif_alloc_env(); + + ERL_NIF_TERM type; + + switch(sqlite_operation_type) { + case SQLITE_INSERT: + type = make_atom(msg_env, "insert"); + break; + case SQLITE_DELETE: + type = make_atom(msg_env, "delete"); + break; + case SQLITE_UPDATE: + type = make_atom(msg_env, "update"); + break; + default: + return; + } + ERL_NIF_TERM rowid = enif_make_int64(msg_env, sqlite_rowid); + ERL_NIF_TERM database = make_binary(msg_env, sqlite_database, strlen(sqlite_database)); + ERL_NIF_TERM table = make_binary(msg_env, sqlite_table, strlen(sqlite_table)); + + ERL_NIF_TERM msg = enif_make_tuple4(msg_env, type, database, table, rowid); + + if(!enif_send(NULL, &conn->update_hook_pid, msg_env, msg)) { + sqlite3_update_hook(conn->db, NULL, NULL); + } +} + + +static ERL_NIF_TERM +esqlite_set_update_hook(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite3 *conn; + int rc; + + if(argc != 2) { + return enif_make_badarg(env); + } + + if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) { + return enif_make_badarg(env); + } + + if(!conn->db) { + return make_error_tuple(env, "closed"); + } + + if(enif_is_atom(env, argv[1])) { + /* Assume this is undefined, reset the connection */ + sqlite3_update_hook(conn->db, NULL, NULL); + } else { + /* [todo] passing undefined resets the hook? */ + if(!enif_get_local_pid(env, argv[1], &conn->update_hook_pid)) { + return enif_make_badarg(env); + } + + sqlite3_update_hook(conn->db, update_callback, conn); + } + + return make_atom(env, "ok"); +} + + /* * Exec a sql statement */ @@ -984,43 +947,6 @@ set_update_hook(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) */ -/* - * Bind a variable to a prepared statement -static ERL_NIF_TERM -esqlite_bind(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) -{ - esqlite3 *conn; - esqlite3_stmt *stmt; - esqlite_command *cmd = NULL; - ErlNifPid pid; - - if(argc != 5) - return enif_make_badarg(env); - - if(!enif_get_resource(env, argv[0], esqlite3_type, (void **) &conn)) - return enif_make_badarg(env); - if(!enif_get_resource(env, argv[1], esqlite3_stmt_type, (void **) &stmt)) - return enif_make_badarg(env); - if(!enif_is_ref(env, argv[2])) - return make_error_tuple(env, "invalid_ref"); - if(!enif_get_local_pid(env, argv[3], &pid)) - return make_error_tuple(env, "invalid_pid"); - - cmd = command_create(); - if(!cmd) - return make_error_tuple(env, "command_create_failed"); - - cmd->type = cmd_bind; - cmd->ref = enif_make_copy(cmd->env, argv[2]); - cmd->pid = pid; - cmd->stmt = enif_make_copy(cmd->env, argv[1]); - cmd->arg = enif_make_copy(cmd->env, argv[4]); - - return push_command(env, conn, cmd); -} - */ - - /* * Get the column names of the prepared statement. */ @@ -1241,7 +1167,6 @@ esqlite_bind_null(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) return make_atom(env, "ok"); } - static ERL_NIF_TERM esqlite_step(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { @@ -1607,6 +1532,8 @@ static ErlNifFunc nif_funcs[] = { {"open", 1, esqlite_open, ERL_NIF_DIRTY_JOB_IO_BOUND}, {"close", 1, esqlite_close, ERL_NIF_DIRTY_JOB_IO_BOUND}, + {"set_update_hook", 2, esqlite_set_update_hook}, + {"exec", 2, esqlite_exec}, {"prepare", 3, esqlite_prepare}, @@ -1640,7 +1567,6 @@ static ErlNifFunc nif_funcs[] = { {"changes", 1, esqlite_changes}, /* - {"set_update_hook", 4, set_update_hook}, {"exec", 4, esqlite_exec, ERL_NIF_DIRTY_JOB_IO_BOUND}, diff --git a/src/esqlite3.erl b/src/esqlite3.erl index 7e74d29..3fd624f 100644 --- a/src/esqlite3.erl +++ b/src/esqlite3.erl @@ -22,12 +22,13 @@ open/1, close/1, %% db connection functions + + set_update_hook/2, + get_autocommit/1, last_insert_rowid/1, changes/1, -% set_update_hook/2, set_update_hook/3, - exec/2, prepare/2, prepare/3, @@ -46,7 +47,6 @@ step/1, reset/1 -% insert/2, insert/3, % % fetchone/1, % fetchall/1, fetchall/2, fetchall/3, @@ -138,27 +138,20 @@ close(#esqlite3{db=Connection}) -> esqlite3_nif:close(Connection). -%%% @doc Subscribe to database notifications. When rows are inserted deleted -%%% or updates, the process will receive messages: -%%% ```{insert, string(), rowid()}''' -%%% When a new row has been inserted. -%%% ```{delete, string(), rowid()}''' -%%% When a new row has been deleted. -%%% ```{update, string(), rowid()}''' -%%% When a row has been updated. -%%% -%-spec set_update_hook(pid(), connection()) -> ok | {error, term()}. -%set_update_hook(Pid, Connection) -> -% set_update_hook(Pid, Connection, ?DEFAULT_TIMEOUT). -% -%%% @doc Same as set_update_hook/2, but with an additional timeout parameter. -%%% -%-spec set_update_hook(pid(), connection(), timeout()) -> ok | {error, term()}. -%set_update_hook(Pid, #connection{raw_connection=RawConnection}, Timeout) -> -% Ref = make_ref(), -% ok = esqlite3_nif:set_update_hook(RawConnection, Ref, self(), Pid), -% receive_answer(RawConnection, Ref, Timeout). -% +%% @doc Subscribe to database notifications. When rows are inserted deleted +%% or updates, the process will receive messages: +%% ```{insert, binary(), binary(), rowid()}''' +%% When a new row has been inserted. +%% ```{delete, binary(), binary(), rowid()}''' +%% When a new row has been deleted. +%% ```{update, binary(), binary(), rowid()}''' +%% When a row has been updated. +%% +-spec set_update_hook(esqlite3(), pid() | undefined) -> ok | {error, term()}. +set_update_hook(#esqlite3{db=Connection}, MaybePid) when is_pid(MaybePid) orelse MaybePid =:= undefined -> + esqlite3_nif:set_update_hook(Connection, MaybePid). + + %%% %%% q %%% @@ -202,7 +195,7 @@ close(#esqlite3{db=Connection}) -> % %%% @doc Execute statement and return a list with the result of F for each row. %-spec map(Fun, sql(), connection()) -> list(Type) when -%% Fun :: fun((Row) -> Type) | fun((ColumnNames, Row) -> Type), +% Fun :: fun((Row) -> Type) | fun((ColumnNames, Row) -> Type), % Row :: row(), % ColumnNames :: tuple(), % Type :: any(). @@ -401,7 +394,6 @@ get_autocommit(#esqlite3{db=Connection}) -> exec(#esqlite3{db=Connection}, Sql) -> esqlite3_nif:exec(Connection, Sql). - %% @doc Compile a SQL statement. Returns a cached compiled statement which can be used in %% queries. %% diff --git a/src/esqlite3_nif.erl b/src/esqlite3_nif.erl index 65b8fc9..0e5c21f 100644 --- a/src/esqlite3_nif.erl +++ b/src/esqlite3_nif.erl @@ -23,10 +23,11 @@ open/1, close/1, + set_update_hook/2, + get_autocommit/1, last_insert_rowid/1, changes/1, - exec/2, prepare/3, @@ -45,7 +46,6 @@ reset/1, interrupt/1 -% set_update_hook/4, % exec/4, % changes/3, % @@ -94,10 +94,20 @@ open(_Filename) -> %% -spec close(Connection) -> CloseResult when Connection :: esqlite3(), - CloseResult :: ok | {error, _}. + CloseResult :: ok | {error, _}. close(_Db) -> erlang:nif_error(nif_library_not_loaded). +%% @doc Set an update hook +%% +-spec set_update_hook(Connection, Pid) -> Result + when Connection :: esqlite3(), + Pid :: pid(), + Result :: ok | {error, _}. +set_update_hook(_Db, _Pid) -> + erlang:nif_error(nif_library_not_loaded). + + %% @doc Execute a sql statement %% -spec exec(Connection, Sql) -> ExecResult @@ -136,9 +146,6 @@ step(_Statement) -> reset(_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). %% @doc Exec the query. %% diff --git a/test/esqlite_test.erl b/test/esqlite_test.erl index 1dfc544..1611cda 100644 --- a/test/esqlite_test.erl +++ b/test/esqlite_test.erl @@ -139,17 +139,29 @@ last_insert_rowid_test() -> 2 = esqlite3:last_insert_rowid(Db), ok. -%update_hook_test() -> -% {ok, Db} = esqlite3:open(":memory:"), -% ok = esqlite3:set_update_hook(self(), Db), -% ok = esqlite3:exec("CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);", Db), -% ok = esqlite3:exec("INSERT INTO test (val) VALUES ('this is a test');", Db), -% ok = receive {insert, "test", 1} -> ok after 150 -> no_message end, -% ok = esqlite3:exec("UPDATE test SET val = 'a new test' WHERE id = 1;", Db), -% ok = receive {update, "test", 1} -> ok after 150 -> no_message end, -% ok = esqlite3:exec("DELETE FROM test WHERE id = 1;", Db), -% ok = receive {delete, "test", 1} -> ok after 150 -> no_message end, -% ok. +update_hook_test() -> + {ok, Db} = esqlite3:open(":memory:"), + ok = esqlite3:set_update_hook(Db, self()), + + ok = esqlite3:exec(Db, "CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);"), + ok = esqlite3:exec(Db, "INSERT INTO test (val) VALUES ('this is a test');"), + + ok = receive {insert, <<"main">>, <<"test">>, 1} -> ok after 150 -> no_message end, + + ok = esqlite3:exec(Db, "UPDATE test SET val = 'a new test' WHERE id = 1;"), + + ok = receive {update, <<"main">>, <<"test">>, 1} -> ok after 150 -> no_message end, + + ok = esqlite3:exec(Db, "DELETE FROM test WHERE id = 1;"), + + ok = receive {delete, <<"main">>, <<"test">>, 1} -> ok after 150 -> no_message end, + + ok = esqlite3:set_update_hook(Db, undefined), + + ok = esqlite3:exec(Db, "INSERT INTO test (val) VALUES ('this is a test');"), + no_message = receive {insert, <<"main">>, <<"test">>, 1} -> ok after 150 -> no_message end, + + ok. simple_query_test() -> {ok, Db} = esqlite3:open(":memory:"),