diff --git a/c_src/esqlite3_nif.c b/c_src/esqlite3_nif.c index d1f14f0..fd335b1 100644 --- a/c_src/esqlite3_nif.c +++ b/c_src/esqlite3_nif.c @@ -1219,6 +1219,24 @@ esqlite_column_types(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) return push_command(env, conn, cmd); } +/* + * Interrupt currently active query. + */ + +static ERL_NIF_TERM +esqlite_interrupt(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite_connection *conn; + + if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) + return enif_make_badarg(env); + + esqlite_connection *db = (esqlite_connection *) conn; + sqlite3_interrupt(db->db); + + return enif_make_atom(env, "ok"); +} + /* * Close the database */ @@ -1297,6 +1315,7 @@ static ErlNifFunc nif_funcs[] = { {"bind", 5, esqlite_bind}, {"column_names", 4, esqlite_column_names}, {"column_types", 4, esqlite_column_types}, + {"interrupt", 1, esqlite_interrupt, ERL_NIF_DIRTY_JOB_IO_BOUND}, {"close", 3, esqlite_close} }; diff --git a/src/esqlite.app.src b/src/esqlite.app.src index 5c5aca3..5331c4c 100644 --- a/src/esqlite.app.src +++ b/src/esqlite.app.src @@ -1,7 +1,7 @@ {application, esqlite, [ {description, "sqlite nif interface"}, - {vsn, "0.5.1"}, + {vsn, "0.5.2"}, {modules, [esqlite3, esqlite3_nif]}, {registered, []}, {licenses, ["Apache"]}, diff --git a/src/esqlite3.erl b/src/esqlite3.erl index cceb763..81b247e 100644 --- a/src/esqlite3.erl +++ b/src/esqlite3.erl @@ -23,7 +23,7 @@ %% higher-level export -export([open/1, open/2, set_update_hook/2, set_update_hook/3, - exec/2, exec/3, + exec/2, exec/3, exec/4, changes/1, changes/2, insert/2, get_autocommit/1, @@ -96,7 +96,7 @@ open(Filename, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:open(Connection, Ref, self(), Filename), - case receive_answer(Ref, Timeout) of + case receive_answer(Connection, Ref, Timeout) of ok -> {ok, {connection, make_ref(), Connection}}; {error, _Msg}=Error -> @@ -121,7 +121,7 @@ set_update_hook(Pid, Connection) -> set_update_hook(Pid, {connection, _Ref, Connection}, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:set_update_hook(Connection, Ref, self(), Pid), - receive_answer(Ref, Timeout). + receive_answer(Connection, Ref, Timeout). %% @doc Execute a sql statement, returns a list with tuples. -spec q(sql(), connection()) -> list(tuple()) | {error, term()}. @@ -339,7 +339,9 @@ try_multi_step(Statement, ChunkSize, Rest, Tries, Timeout) -> exec(Sql, Connection) -> exec(Sql, [], Connection, ?DEFAULT_TIMEOUT). --spec exec(sql(), list(cell_type()), connection()) -> ok | {error, _}. +-spec exec(sql(), list(cell_type()) | connection(), connection() | timeout()) -> ok | {error, _}. +exec(Sql, {connection, _,_}=Connection, Timeout) -> + exec(Sql, [], Connection, Timeout); exec(Sql, Params, Connection) -> exec(Sql, Params, Connection, ?DEFAULT_TIMEOUT). @@ -347,7 +349,7 @@ exec(Sql, Params, Connection) -> exec(Sql, [], {connection, _Ref, Connection}, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:exec(Connection, Ref, self(), Sql), - receive_answer(Ref, Timeout); + receive_answer(Connection, Ref, Timeout); exec(Sql, Params, Connection, Timeout) -> {ok, Statement} = prepare(Sql, Connection, Timeout), bind(Statement, Params), @@ -363,7 +365,7 @@ changes(Connection) -> changes({connection, _Ref, Connection}, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:changes(Connection, Ref, self()), - receive_answer(Ref, Timeout). + receive_answer(Connection, Ref, Timeout). %% @doc Insert records, returns the last rowid. %% @@ -376,7 +378,7 @@ insert(Sql, Connection) -> insert(Sql, {connection, _Ref, Connection}, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:insert(Connection, Ref, self(), Sql), - receive_answer(Ref, Timeout). + receive_answer(Connection, Ref, Timeout). %% @doc Check if the connection is in auto-commit mode. %% See: [https://sqlite.org/c3ref/get_autocommit.html] for more details. @@ -390,7 +392,7 @@ get_autocommit(Connection) -> get_autocommit({connection, _Ref, Connection}, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:get_autocommit(Connection, Ref, self()), - receive_answer(Ref, Timeout). + receive_answer(Connection, Ref, Timeout). %% @doc Compile a SQL statement. Returns a cached compiled statement which can be used in %% queries. @@ -404,7 +406,7 @@ prepare(Sql, Connection) -> prepare(Sql, {connection, _Ref, Connection}=C, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:prepare(Connection, Ref, self(), Sql), - case receive_answer(Ref, Timeout) of + case receive_answer(Connection, Ref, Timeout) of {ok, Stmt} -> {ok, {statement, Stmt, C}}; Else -> Else end. @@ -421,7 +423,7 @@ step(Stmt) -> step({statement, Stmt, {connection, _, Conn}}, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:multi_step(Conn, Stmt, 1, Ref, self()), - case receive_answer(Ref, Timeout) of + case receive_answer(Conn, Ref, Timeout) of {rows, [Row | []]} -> {row, Row}; {'$done', []} -> '$done'; {'$busy', []} -> '$busy'; @@ -438,7 +440,7 @@ step({statement, Stmt, {connection, _, Conn}}, Timeout) -> multi_step({statement, Stmt, {connection, _, Conn}}, ChunkSize, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:multi_step(Conn, Stmt, ChunkSize, Ref, self()), - receive_answer(Ref, Timeout). + receive_answer(Conn, Ref, Timeout). %% @doc Reset the prepared statement back to its initial state. %% @@ -446,7 +448,7 @@ multi_step({statement, Stmt, {connection, _, Conn}}, ChunkSize, Timeout) -> reset({statement, Stmt, {connection, _, Conn}}) -> Ref = make_ref(), ok = esqlite3_nif:reset(Conn, Stmt, Ref, self()), - receive_answer(Ref, ?DEFAULT_TIMEOUT). + receive_answer(Conn, Ref, ?DEFAULT_TIMEOUT). %% @doc Bind values to prepared statements %% @@ -459,7 +461,7 @@ bind(Stmt, Args) -> bind({statement, Stmt, {connection, _, Conn}}, Args, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:bind(Conn, Stmt, Ref, self(), Args), - receive_answer(Ref, Timeout). + receive_answer(Conn, Ref, Timeout). %% @doc Return the column names of the prepared statement. %% @@ -471,7 +473,7 @@ column_names(Stmt) -> column_names({statement, Stmt, {connection, _, Conn}}, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:column_names(Conn, Stmt, Ref, self()), - receive_answer(Ref, Timeout). + receive_answer(Conn, Ref, Timeout). %% @doc Return the column types of the prepared statement. %% @@ -483,7 +485,7 @@ column_types(Stmt) -> column_types({statement, Stmt, {connection, _, Conn}}, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:column_types(Conn, Stmt, Ref, self()), - receive_answer(Ref, Timeout). + receive_answer(Conn, Ref, Timeout). %% @doc Close the database -spec close(connection()) -> ok | {error, _}. @@ -495,7 +497,7 @@ close(Connection) -> close({connection, _Ref, Connection}, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:close(Connection, Ref, self()), - receive_answer(Ref, Timeout). + receive_answer(Connection, Ref, Timeout). %% @doc Flush any stale answers left in the mailbox of the current process. @@ -509,11 +511,13 @@ flush() -> %% Internal functions -receive_answer(Ref, Timeout) -> +receive_answer(Connection, Ref, Timeout) -> receive {esqlite3, Ref, Resp} -> Resp after - Timeout -> throw({error, timeout, Ref}) + Timeout -> + ok = esqlite3_nif:interrupt(Connection), + throw({error, timeout, Ref}) end. flush_answers() -> diff --git a/src/esqlite3_nif.erl b/src/esqlite3_nif.erl index 2b00f63..3f2503a 100644 --- a/src/esqlite3_nif.erl +++ b/src/esqlite3_nif.erl @@ -35,6 +35,7 @@ bind/5, column_names/4, column_types/4, + interrupt/1, close/3 ]). @@ -128,6 +129,10 @@ column_names(_Db, _Stmt, _Ref, _Dest) -> column_types(_Db, _Stmt, _Ref, _Dest) -> erlang:nif_error(nif_library_not_loaded). +%% @doc Interrupt all active queries. +interrupt(_Db) -> + erlang:nif_error(nif_library_not_loaded). + %% @doc Close the connection. %% -spec close(esqlite:connection(), reference(), pid()) -> ok | {error, any()}. diff --git a/test/esqlite_test.erl b/test/esqlite_test.erl index da39676..a60a814 100644 --- a/test/esqlite_test.erl +++ b/test/esqlite_test.erl @@ -420,6 +420,28 @@ sqlite_source_id_test() -> ?assertEqual({row, {<<"2021-12-30 15:30:28 378629bf2ea546f73eee84063c5358439a12f7300e433f18c9e1bddd948dea62">>}}, esqlite3:step(Stmt)), ok. +interrupt_on_timeout_test() -> + {ok, Db} = esqlite3:open(":memory:"), + CreateTableQuery = "CREATE TABLE all_numbers_in_the_world (number int not null);", + ok = esqlite3:exec(CreateTableQuery, Db), + VeryLongQuery = " + WITH RECURSIVE + for(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM for WHERE i < 10000000) + INSERT INTO all_numbers_in_the_world SELECT i FROM for; + ", + try + ok = esqlite3:exec(VeryLongQuery, [], Db, 10) + catch + {error, timeout, _} -> + ?assertMatch([{0}], esqlite3:q("SELECT COUNT(*) FROM all_numbers_in_the_world", Db)), + %% There is now a stale answer, because the recursive query was interrupted. + receive + {esqlite3, _, {error, {interrupt, "interrupted"}}} -> + ok + end + end. + + garbage_collect_test() -> F = fun() -> {ok, Db} = esqlite3:open(":memory:"),