Skip to content

Commit 7ed9631

Browse files
authored
add set_log_hook/1 (#266)
1 parent 7de7634 commit 7ed9631

File tree

4 files changed

+149
-1
lines changed

4 files changed

+149
-1
lines changed

c_src/sqlite3_nif.c

+65
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ static ErlNifResourceType* connection_type = NULL;
1717
static ErlNifResourceType* statement_type = NULL;
1818
static sqlite3_mem_methods default_alloc_methods = {0};
1919

20+
ErlNifPid* log_hook_pid = NULL;
21+
ErlNifMutex* log_hook_mutex = NULL;
22+
2023
typedef struct connection
2124
{
2225
sqlite3* db;
@@ -956,6 +959,11 @@ on_load(ErlNifEnv* env, void** priv, ERL_NIF_TERM info)
956959
return -1;
957960
}
958961

962+
log_hook_mutex = enif_mutex_create("exqlite:log_hook");
963+
if (!log_hook_mutex) {
964+
return -1;
965+
}
966+
959967
return 0;
960968
}
961969

@@ -965,6 +973,7 @@ on_unload(ErlNifEnv* caller_env, void* priv_data)
965973
assert(caller_env);
966974

967975
sqlite3_config(SQLITE_CONFIG_MALLOC, &default_alloc_methods);
976+
enif_mutex_destroy(log_hook_mutex);
968977
}
969978

970979
//
@@ -1065,6 +1074,61 @@ exqlite_set_update_hook(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
10651074
return make_atom(env, "ok");
10661075
}
10671076

1077+
//
1078+
// Log Notifications
1079+
//
1080+
1081+
void
1082+
log_callback(void* arg, int iErrCode, const char* zMsg)
1083+
{
1084+
if (log_hook_pid == NULL) {
1085+
return;
1086+
}
1087+
1088+
ErlNifEnv* msg_env = enif_alloc_env();
1089+
ERL_NIF_TERM error = make_binary(msg_env, zMsg, strlen(zMsg));
1090+
ERL_NIF_TERM msg = enif_make_tuple3(msg_env, make_atom(msg_env, "log"), enif_make_int(msg_env, iErrCode), error);
1091+
1092+
if (!enif_send(NULL, log_hook_pid, msg_env, msg)) {
1093+
enif_mutex_lock(log_hook_mutex);
1094+
sqlite3_config(SQLITE_CONFIG_LOG, NULL, NULL);
1095+
enif_free(log_hook_pid);
1096+
log_hook_pid = NULL;
1097+
enif_mutex_unlock(log_hook_mutex);
1098+
}
1099+
1100+
enif_free_env(msg_env);
1101+
}
1102+
1103+
static ERL_NIF_TERM
1104+
exqlite_set_log_hook(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
1105+
{
1106+
assert(env);
1107+
1108+
if (argc != 1) {
1109+
return enif_make_badarg(env);
1110+
}
1111+
1112+
ErlNifPid* pid = (ErlNifPid*)enif_alloc(sizeof(ErlNifPid));
1113+
if (!enif_get_local_pid(env, argv[0], pid)) {
1114+
enif_free(pid);
1115+
return make_error_tuple(env, "invalid_pid");
1116+
}
1117+
1118+
enif_mutex_lock(log_hook_mutex);
1119+
1120+
if (log_hook_pid) {
1121+
enif_free(log_hook_pid);
1122+
}
1123+
1124+
log_hook_pid = pid;
1125+
sqlite3_config(SQLITE_CONFIG_LOG, log_callback, NULL);
1126+
1127+
enif_mutex_unlock(log_hook_mutex);
1128+
1129+
return make_atom(env, "ok");
1130+
}
1131+
10681132
//
10691133
// Most of our nif functions are going to be IO bounded
10701134
//
@@ -1086,6 +1150,7 @@ static ErlNifFunc nif_funcs[] = {
10861150
{"release", 2, exqlite_release, ERL_NIF_DIRTY_JOB_IO_BOUND},
10871151
{"enable_load_extension", 2, exqlite_enable_load_extension, ERL_NIF_DIRTY_JOB_IO_BOUND},
10881152
{"set_update_hook", 2, exqlite_set_update_hook, ERL_NIF_DIRTY_JOB_IO_BOUND},
1153+
{"set_log_hook", 1, exqlite_set_log_hook, ERL_NIF_DIRTY_JOB_IO_BOUND},
10891154
};
10901155

10911156
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

+56
Original file line numberDiff line numberDiff line change
@@ -491,4 +491,60 @@ 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+
on_exit(fn -> Sqlite3.close(conn2) end)
536+
537+
assert {:error, _reason} = Sqlite3.prepare(conn1, "some invalid sql 1")
538+
assert_receive {:log, rc, msg}
539+
assert rc == 1
540+
assert msg == "near \"some\": syntax error in \"some invalid sql 1\""
541+
refute_receive _anything_else
542+
543+
assert {:error, _reason} = Sqlite3.prepare(conn2, "some invalid sql 2")
544+
assert_receive {:log, rc, msg}
545+
assert rc == 1
546+
assert msg == "near \"some\": syntax error in \"some invalid sql 2\""
547+
refute_receive _anything_else
548+
end
549+
end
494550
end

0 commit comments

Comments
 (0)