Skip to content

Commit d645ab1

Browse files
committed
add set_log_hook/1
1 parent 7de7634 commit d645ab1

File tree

4 files changed

+141
-1
lines changed

4 files changed

+141
-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

+62
Original file line numberDiff line numberDiff line change
@@ -491,4 +491,66 @@ defmodule Exqlite.Sqlite3Test do
491491
refute_receive {{:insert, "main", "test", 1}, _}, 1000
492492
end
493493
end
494+
495+
describe "set_log_hook/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_log_hook(self())
510+
511+
assert {:error, reason} = Sqlite3.prepare(conn, "some invalid sql")
512+
assert reason == "near \"some\": syntax error"
513+
514+
assert_receive {:log, 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_log_hook(test)
523+
524+
task =
525+
Task.async(fn ->
526+
:ok = Sqlite3.set_log_hook(self())
527+
assert {:error, reason} = Sqlite3.prepare(conn, "some invalid sql")
528+
assert reason == "near \"some\": syntax error"
529+
assert_receive {:log, rc, msg}
530+
assert rc == 1
531+
assert msg == "near \"some\": syntax error in \"some invalid sql\""
532+
refute_receive _anything_else
533+
end)
534+
535+
Task.await(task)
536+
refute_receive _anything_else
537+
end
538+
539+
test "receives notifications from all connections", %{conn: conn1, path: path} do
540+
assert :ok = Sqlite3.set_log_hook(self())
541+
assert {:ok, conn2} = Sqlite3.open(path)
542+
543+
assert {:error, _reason} = Sqlite3.prepare(conn1, "some invalid sql 1")
544+
assert_receive {:log, rc, msg}
545+
assert rc == 1
546+
assert msg == "near \"some\": syntax error in \"some invalid sql 1\""
547+
refute_receive _anything_else
548+
549+
assert {:error, _reason} = Sqlite3.prepare(conn2, "some invalid sql 2")
550+
assert_receive {:log, rc, msg}
551+
assert rc == 1
552+
assert msg == "near \"some\": syntax error in \"some invalid sql 2\""
553+
refute_receive _anything_else
554+
end
555+
end
494556
end

0 commit comments

Comments
 (0)