Skip to content

Commit 4dd538f

Browse files
committed
add set_log_hook/1
1 parent 7de7634 commit 4dd538f

File tree

4 files changed

+134
-1
lines changed

4 files changed

+134
-1
lines changed

c_src/sqlite3_nif.c

+51
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,56 @@ exqlite_set_update_hook(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
10651065
return make_atom(env, "ok");
10661066
}
10671067

1068+
//
1069+
// Log Notifications
1070+
//
1071+
1072+
ErlNifPid* log_hook_pid = NULL;
1073+
1074+
void
1075+
log_callback(void* arg, int iErrCode, const char* zMsg)
1076+
{
1077+
if (log_hook_pid == NULL) {
1078+
return;
1079+
}
1080+
1081+
ErlNifEnv* msg_env = enif_alloc_env();
1082+
ERL_NIF_TERM error = make_binary(msg_env, zMsg, strlen(zMsg));
1083+
ERL_NIF_TERM msg = enif_make_tuple3(msg_env, make_atom(msg_env, "log"), enif_make_int(msg_env, iErrCode), error);
1084+
1085+
if (!enif_send(NULL, log_hook_pid, msg_env, msg)) {
1086+
sqlite3_config(SQLITE_CONFIG_LOG, NULL, NULL);
1087+
log_hook_pid = NULL;
1088+
enif_free(log_hook_pid);
1089+
}
1090+
1091+
enif_free_env(msg_env);
1092+
}
1093+
1094+
static ERL_NIF_TERM
1095+
exqlite_set_log_hook(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
1096+
{
1097+
assert(env);
1098+
1099+
if (argc != 1) {
1100+
return enif_make_badarg(env);
1101+
}
1102+
1103+
ErlNifPid* pid = (ErlNifPid*)enif_alloc(sizeof(ErlNifPid));
1104+
if (!enif_get_local_pid(env, argv[0], pid)) {
1105+
enif_free(pid);
1106+
return make_error_tuple(env, "invalid_pid");
1107+
}
1108+
1109+
if (log_hook_pid) {
1110+
enif_free(log_hook_pid);
1111+
}
1112+
1113+
log_hook_pid = pid;
1114+
sqlite3_config(SQLITE_CONFIG_LOG, log_callback, NULL);
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_log_hook", 1, exqlite_set_log_hook, 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

+25-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,30 @@ defmodule Exqlite.Sqlite3 do
233233
Sqlite3NIF.set_update_hook(conn, pid)
234234
end
235235

236+
@doc """
237+
Send log messages to a process.
238+
239+
Each time a message is logged in SQLite a message will be sent to the pid provided as the argument.
240+
241+
The message is of the form: `{:log, rc, message}`, where:
242+
243+
* `rc` is an integer [result code](https://www.sqlite.org/rescode.html) or an [extended result code](https://www.sqlite.org/rescode.html#extrc)
244+
* `message` is a string representing the log message
245+
246+
See [`SQLITE_CONFIG_LOG`](https://www.sqlite.org/c3ref/c_config_covering_index_scan.html) and
247+
["The Error And Warning Log"](https://www.sqlite.org/errlog.html) for more details.
248+
249+
## Restrictions
250+
251+
* Only one pid can listen to the log messages at a time.
252+
If this function is called multiple times, only the last pid will
253+
receive the notifications
254+
"""
255+
@spec set_log_hook(pid()) :: :ok | {:error, reason()}
256+
def set_log_hook(pid) do
257+
Sqlite3NIF.set_log_hook(pid)
258+
end
259+
236260
defp convert(%Date{} = val), do: Date.to_iso8601(val)
237261
defp convert(%Time{} = val), do: Time.to_iso8601(val)
238262
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_log_hook(pid()) :: :ok | {:error, reason()}
71+
def set_log_hook(_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

+55
Original file line numberDiff line numberDiff line change
@@ -491,4 +491,59 @@ defmodule Exqlite.Sqlite3Test do
491491
refute_receive {{:insert, "main", "test", 1}, _}, 1000
492492
end
493493
end
494+
495+
describe "set_log_hook/1" do
496+
setup do
497+
{:ok, conn} = Sqlite3.open(":memory:")
498+
on_exit(fn -> Sqlite3.close(conn) end)
499+
{:ok, conn: conn}
500+
end
501+
502+
test "can receive errors", %{conn: conn} do
503+
assert :ok = Sqlite3.set_log_hook(self())
504+
505+
assert {:error, reason} = Sqlite3.prepare(conn, "some invalid sql")
506+
assert reason == "near \"some\": syntax error"
507+
508+
assert_receive {:log, rc, msg}
509+
assert rc == 1
510+
assert msg == "near \"some\": syntax error in \"some invalid sql\""
511+
refute_receive _anything_else
512+
end
513+
514+
test "only one pid can listen at a time", %{conn: conn} do
515+
assert :ok = Sqlite3.set_log_hook(self())
516+
517+
task =
518+
Task.async(fn ->
519+
:ok = Sqlite3.set_log_hook(self())
520+
assert {:error, reason} = Sqlite3.prepare(conn, "some invalid sql")
521+
assert reason == "near \"some\": syntax error"
522+
assert_receive {:log, rc, msg}
523+
assert rc == 1
524+
assert msg == "near \"some\": syntax error in \"some invalid sql\""
525+
refute_receive _anything_else
526+
end)
527+
528+
Task.await(task)
529+
refute_receive _anything_else
530+
end
531+
532+
test "receives notifications from all connections", %{conn: conn1} do
533+
assert :ok = Sqlite3.set_log_hook(self())
534+
assert {:ok, conn2} = Sqlite3.open(":memory:")
535+
536+
assert {:error, _reason} = Sqlite3.prepare(conn1, "some invalid sql 1")
537+
assert_receive {:log, rc, msg}
538+
assert rc == 1
539+
assert msg == "near \"some\": syntax error in \"some invalid sql 1\""
540+
refute_receive _anything_else
541+
542+
assert {:error, _reason} = Sqlite3.prepare(conn2, "some invalid sql 2")
543+
assert_receive {:log, rc, msg}
544+
assert rc == 1
545+
assert msg == "near \"some\": syntax error in \"some invalid sql 2\""
546+
refute_receive _anything_else
547+
end
548+
end
494549
end

0 commit comments

Comments
 (0)