Skip to content

Commit 3742b8f

Browse files
committed
add set_error_log/2
1 parent 7de7634 commit 3742b8f

File tree

4 files changed

+129
-1
lines changed

4 files changed

+129
-1
lines changed

c_src/sqlite3_nif.c

+51
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ typedef struct connection
2222
sqlite3* db;
2323
ErlNifMutex* mutex;
2424
ErlNifPid update_hook_pid;
25+
ErlNifPid error_log_pid;
2526
} connection_t;
2627

2728
typedef struct statement
@@ -1065,6 +1066,55 @@ exqlite_set_update_hook(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
10651066
return make_atom(env, "ok");
10661067
}
10671068

1069+
//
1070+
// Error Notifications
1071+
//
1072+
1073+
void
1074+
error_callback(void* arg, int iErrCode, const char* zMsg)
1075+
{
1076+
connection_t* conn = (connection_t*)arg;
1077+
1078+
if (conn == NULL) {
1079+
return;
1080+
}
1081+
1082+
ErlNifEnv* msg_env = enif_alloc_env();
1083+
ERL_NIF_TERM error = make_binary(msg_env, zMsg, strlen(zMsg));
1084+
ERL_NIF_TERM msg = enif_make_tuple3(msg_env, make_atom(msg_env, "error"), enif_make_int(msg_env, iErrCode), error);
1085+
1086+
if (!enif_send(NULL, &conn->error_log_pid, msg_env, msg)) {
1087+
sqlite3_config(SQLITE_CONFIG_LOG, NULL, NULL);
1088+
}
1089+
1090+
enif_free_env(msg_env);
1091+
}
1092+
1093+
static ERL_NIF_TERM
1094+
exqlite_set_error_log(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
1095+
{
1096+
assert(env);
1097+
connection_t* conn = NULL;
1098+
1099+
if (argc != 2) {
1100+
return enif_make_badarg(env);
1101+
}
1102+
1103+
if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) {
1104+
return make_error_tuple(env, "invalid_connection");
1105+
}
1106+
1107+
if (!enif_get_local_pid(env, argv[1], &conn->error_log_pid)) {
1108+
return make_error_tuple(env, "invalid_pid");
1109+
}
1110+
1111+
// Passing the connection as the third argument causes it to be
1112+
// passed as the first argument to error_callback. This allows us
1113+
// to extract the hook pid and reset the hook if the pid is not alive.
1114+
sqlite3_config(SQLITE_CONFIG_LOG, error_callback, conn);
1115+
return make_atom(env, "ok");
1116+
}
1117+
10681118
//
10691119
// Most of our nif functions are going to be IO bounded
10701120
//
@@ -1086,6 +1136,7 @@ static ErlNifFunc nif_funcs[] = {
10861136
{"release", 2, exqlite_release, ERL_NIF_DIRTY_JOB_IO_BOUND},
10871137
{"enable_load_extension", 2, exqlite_enable_load_extension, ERL_NIF_DIRTY_JOB_IO_BOUND},
10881138
{"set_update_hook", 2, exqlite_set_update_hook, ERL_NIF_DIRTY_JOB_IO_BOUND},
1139+
{"set_error_log", 2, exqlite_set_error_log, ERL_NIF_DIRTY_JOB_IO_BOUND},
10891140
};
10901141

10911142
ERL_NIF_INIT(Elixir.Exqlite.Sqlite3NIF, nif_funcs, on_load, NULL, NULL, on_unload)

lib/exqlite/sqlite3.ex

+11-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ defmodule Exqlite.Sqlite3 do
223223
* Only one pid can listen to the changes on a given database connection at a time.
224224
If this function is called multiple times for the same connection, only the last pid will
225225
receive the notifications
226-
* Updates only happen for the connection that is opened. For example, there
226+
* Updates only happen for the connection that is opened. For example, there
227227
are two connections A and B. When an update happens on connection B, the
228228
hook set for connection A will not receive the update, but the hook for
229229
connection B will receive the update.
@@ -233,6 +233,16 @@ defmodule Exqlite.Sqlite3 do
233233
Sqlite3NIF.set_update_hook(conn, pid)
234234
end
235235

236+
@doc """
237+
Send error messages to a process.
238+
239+
Similar to `set_update_hook/2`, see `SQLITE_CONFIG_LOG` for [more details.](https://www.sqlite.org/c3ref/c_config_covering_index_scan.html)
240+
"""
241+
@spec set_error_log(db(), pid()) :: :ok | {:error, reason()}
242+
def set_error_log(conn, pid) do
243+
Sqlite3NIF.set_error_log(conn, pid)
244+
end
245+
236246
defp convert(%Date{} = val), do: Date.to_iso8601(val)
237247
defp convert(%Time{} = val), do: Time.to_iso8601(val)
238248
defp convert(%NaiveDateTime{} = val), do: NaiveDateTime.to_iso8601(val)

lib/exqlite/sqlite3_nif.ex

+3
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,8 @@ defmodule Exqlite.Sqlite3NIF do
6767
@spec set_update_hook(db(), pid()) :: :ok | {:error, reason()}
6868
def set_update_hook(_conn, _pid), do: :erlang.nif_error(:not_loaded)
6969

70+
@spec set_error_log(db(), pid()) :: :ok | {:error, reason()}
71+
def set_error_log(_conn, _pid), do: :erlang.nif_error(:not_loaded)
72+
7073
# add statement inspection tooling https://sqlite.org/c3ref/expanded_sql.html
7174
end

test/exqlite/sqlite3_test.exs

+64
Original file line numberDiff line numberDiff line change
@@ -491,4 +491,68 @@ defmodule Exqlite.Sqlite3Test do
491491
refute_receive {{:insert, "main", "test", 1}, _}, 1000
492492
end
493493
end
494+
495+
describe "set_error_log/2" do
496+
setup do
497+
{:ok, path} = Temp.path()
498+
{:ok, conn} = Sqlite3.open(path)
499+
500+
on_exit(fn ->
501+
Sqlite3.close(conn)
502+
File.rm(path)
503+
end)
504+
505+
{:ok, conn: conn, path: path}
506+
end
507+
508+
test "can receive errors", %{conn: conn} do
509+
assert :ok = Sqlite3.set_error_log(conn, self())
510+
511+
assert {:error, reason} = Sqlite3.prepare(conn, "some invalid sql")
512+
assert reason == "near \"some\": syntax error"
513+
514+
assert_receive {:error, rc, msg}
515+
assert rc == 1
516+
assert msg == "near \"some\": syntax error in \"some invalid sql\""
517+
refute_receive _anything_else
518+
end
519+
520+
test "only one pid can listen at a time", %{conn: conn} do
521+
test = self()
522+
assert :ok = Sqlite3.set_error_log(conn, test)
523+
524+
spawn(fn ->
525+
:ok = Sqlite3.set_error_log(conn, self())
526+
assert {:error, reason} = Sqlite3.prepare(conn, "some invalid sql")
527+
assert reason == "near \"some\": syntax error"
528+
assert_receive {:error, rc, msg}
529+
assert rc == 1
530+
assert msg == "near \"some\": syntax error in \"some invalid sql\""
531+
refute_receive _anything_else
532+
send(test, :done)
533+
end)
534+
535+
assert_receive :done, 1000
536+
refute_receive _anything_else
537+
end
538+
539+
test "notifications don't cross connections", %{conn: conn1, path: path} do
540+
assert {:ok, conn2} = Sqlite3.open(path)
541+
542+
assert :ok = Sqlite3.set_error_log(conn1, self())
543+
assert :ok = Sqlite3.set_error_log(conn2, self())
544+
545+
assert {:error, _reason} = Sqlite3.prepare(conn1, "some invalid sql 1")
546+
assert_receive {:error, rc, msg}
547+
assert rc == 1
548+
assert msg == "near \"some\": syntax error in \"some invalid sql 1\""
549+
refute_receive _anything_else
550+
551+
assert {:error, _reason} = Sqlite3.prepare(conn2, "some invalid sql 2")
552+
assert_receive {:error, rc, msg}
553+
assert rc == 1
554+
assert msg == "near \"some\": syntax error in \"some invalid sql 2\""
555+
refute_receive _anything_else
556+
end
557+
end
494558
end

0 commit comments

Comments
 (0)