diff --git a/cgosqlite/logcallback.go b/cgosqlite/logcallback.go new file mode 100644 index 0000000..44aee0f --- /dev/null +++ b/cgosqlite/logcallback.go @@ -0,0 +1,55 @@ +package cgosqlite + +// #include +// +// void logCallbackGo(void* userData, int errCode, char* msgC); +// +// static void log_callback_into_go(void *userData, int errCode, const char *msg) { +// logCallbackGo(userData, errCode, (char*)msg); +// } +// +// static int ts_sqlite3_config_log(void) { +// // TODO(raggi): if the library gains new uses of sqlite3_config they need to +// // share a mutex. +// return sqlite3_config(SQLITE_CONFIG_LOG, log_callback_into_go, NULL); +// } +import "C" +import ( + "sync" + "unsafe" + + "github.com/tailscale/sqlite/sqliteh" +) + +// LogCallback receives SQLite log messages. +type LogCallback func(code sqliteh.Code, msg string) + +var ( + logCallbackMu sync.Mutex + logCallback LogCallback +) + +//export logCallbackGo +func logCallbackGo(userData unsafe.Pointer, errCode C.int, msgC *C.char) { + logCallbackMu.Lock() + cb := logCallback + logCallbackMu.Unlock() + + if cb == nil { + return + } + + msg := C.GoString(msgC) + cb(sqliteh.Code(errCode), msg) +} + +// SetLogCallback sets the global SQLite log callback. +// If callback is nil, logs are discarded. +func SetLogCallback(callback LogCallback) error { + logCallbackMu.Lock() + logCallback = callback + logCallbackMu.Unlock() + + res := C.ts_sqlite3_config_log() + return errCode(res) +} diff --git a/sqlite_cgo.go b/sqlite_cgo.go index c7471d2..876a751 100644 --- a/sqlite_cgo.go +++ b/sqlite_cgo.go @@ -3,8 +3,20 @@ package sqlite -import "github.com/tailscale/sqlite/cgosqlite" +import ( + "github.com/tailscale/sqlite/cgosqlite" + "github.com/tailscale/sqlite/sqliteh" +) func init() { Open = cgosqlite.Open } + +// LogCallback receives SQLite log messages. +type LogCallback func(code sqliteh.Code, msg string) + +// SetLogCallback sets the global SQLite log callback. +// If callback is nil, logs are discarded. +func SetLogCallback(callback LogCallback) error { + return cgosqlite.SetLogCallback(cgosqlite.LogCallback(callback)) +} diff --git a/sqlite_cgo_test.go b/sqlite_cgo_test.go new file mode 100644 index 0000000..7ee3ef0 --- /dev/null +++ b/sqlite_cgo_test.go @@ -0,0 +1,45 @@ +//go:build cgo +// +build cgo + +package sqlite + +import ( + "sync" + "testing" + + "github.com/tailscale/sqlite/cgosqlite" + "github.com/tailscale/sqlite/sqliteh" +) + +// ensure LogCallback is convertible to cgosqlite.LogCallback +var _ cgosqlite.LogCallback = cgosqlite.LogCallback(LogCallback(func(code sqliteh.Code, msg string) {})) + +func TestSetLogCallback(t *testing.T) { + var mu sync.Mutex + var logs []string + + err := SetLogCallback(func(code sqliteh.Code, msg string) { + mu.Lock() + defer mu.Unlock() + logs = append(logs, msg) + }) + if err != nil { + t.Fatal(err) + } + defer SetLogCallback(nil) + + db := openTestDB(t) + + _, err = db.Exec("SELECT * FROM nonexistent_table") + if err == nil { + t.Fatal("expected error from invalid SQL") + } + + mu.Lock() + gotLogs := len(logs) > 0 + mu.Unlock() + + if !gotLogs { + t.Fatal("expected to receive log messages") + } +}