diff --git a/Makefile b/Makefile index 053b3538..0d0a04c5 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ EXTENSION_FUNCTIONS_URL = https://www.sqlite.org/contrib/download/extension-func EXTENSION_FUNCTIONS_SHA3 = ee39ddf5eaa21e1d0ebcbceeab42822dd0c4f82d8039ce173fd4814807faabfa # source files +# TODO: maybe use libtrace only in debug mode CFILES = \ sqlite3.c \ extension-functions.c \ @@ -16,6 +17,7 @@ CFILES = \ libhook.c \ libprogress.c \ libvfs.c \ + libtrace.c \ $(CFILES_EXTRA) JSFILES = \ @@ -23,7 +25,8 @@ JSFILES = \ src/libfunction.js \ src/libhook.js \ src/libprogress.js \ - src/libvfs.js + src/libvfs.js \ + src/libtrace.js vpath %.c src vpath %.c deps @@ -77,7 +80,8 @@ EMFLAGS_LIBRARIES = \ --post-js src/libfunction.js \ --post-js src/libhook.js \ --post-js src/libprogress.js \ - --post-js src/libvfs.js + --post-js src/libvfs.js \ + --post-js src/libtrace.js EMFLAGS_ASYNCIFY_COMMON = \ -s ASYNCIFY \ diff --git a/src/libadapters.h b/src/libadapters.h index 90c88327..f8c819a8 100644 --- a/src/libadapters.h +++ b/src/libadapters.h @@ -22,6 +22,7 @@ DECLARE(void, vppp, P, P, P); DECLARE(I, ipppj, P, P, P, J); DECLARE(I, ipppi, P, P, P, I); DECLARE(I, ipppp, P, P, P, P); +DECLARE(I, ippipp, P, P, I, P, P); DECLARE(I, ipppip, P, P, P, I, P); DECLARE(void, vpppip, P, P, P, I, P); DECLARE(I, ippppi, P, P, P, P, I); diff --git a/src/libadapters.js b/src/libadapters.js index 6aabcf32..68318b49 100644 --- a/src/libadapters.js +++ b/src/libadapters.js @@ -6,6 +6,7 @@ const SIGNATURES = [ 'ipppj', // xTruncate 'ipppi', // xSleep, xSync, xLock, xUnlock, xShmUnmap 'ipppp', // xFileSize, xCheckReservedLock, xCurrentTime, xCurrentTimeInt64 + 'ippipp', // xTrace 'ipppip', // xFileControl, xRandomness, xGetLastError 'vpppip', // xFunc, xStep 'ippppi', // xDelete diff --git a/src/libtrace.c b/src/libtrace.c new file mode 100644 index 00000000..8f6407f7 --- /dev/null +++ b/src/libtrace.c @@ -0,0 +1,19 @@ +#include +#include +#include + +#include "libadapters.h" + +#define CALL_JS(SIGNATURE, KEY, ...) \ + (asyncFlags ? SIGNATURE##_async(KEY, __VA_ARGS__) \ + : SIGNATURE(KEY, __VA_ARGS__)) + +static int libtrace_xTrace(unsigned opCode, void *pApp, void *P, void *X) { + const int asyncFlags = pApp ? *(int *)pApp : 0; + return CALL_JS(ippipp, pApp, pApp, opCode, P, X); +} + +void EMSCRIPTEN_KEEPALIVE libtrace_trace(sqlite3 *db, unsigned mTrace, + int xTrace, void *pApp) { + sqlite3_trace_v2(db, mTrace, xTrace ? &libtrace_xTrace : NULL, pApp); +} diff --git a/src/libtrace.js b/src/libtrace.js new file mode 100644 index 00000000..bc0fcbc2 --- /dev/null +++ b/src/libtrace.js @@ -0,0 +1,28 @@ +// Copyright 2024 Roy T. Hashimoto. All Rights Reserved. +// This file should be included in the build with --post-js. +(function() { + const AsyncFunction = Object.getPrototypeOf(async function() { }).constructor; + let pAsyncFlags = 0; + + Module['trace'] = function(db, mTrace, xTrace) { + if (pAsyncFlags) { + Module['deleteCallback'](pAsyncFlags); + Module['_sqlite3_free'](pAsyncFlags); + pAsyncFlags = 0; + } + + pAsyncFlags = Module['_sqlite3_malloc'](4); + setValue(pAsyncFlags, xTrace instanceof AsyncFunction ? 1 : 0, 'i32'); + + ccall( + 'libtrace_trace', + 'void', + ['number', 'number', 'number', 'number'], + [db, mTrace, xTrace ? 1 : 0, pAsyncFlags]); + if (xTrace) { + Module['setCallback'](pAsyncFlags, (_, opCode, pP, pX) => { + return xTrace(opCode, pP, pX) + }); + } + }; +})(); diff --git a/src/sqlite-api.js b/src/sqlite-api.js index 2b3c04f7..142f105e 100644 --- a/src/sqlite-api.js +++ b/src/sqlite-api.js @@ -6,7 +6,7 @@ export * from './sqlite-constants.js'; const MAX_INT64 = 0x7fffffffffffffffn; const MIN_INT64 = -0x8000000000000000n; -const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; +const AsyncFunction = Object.getPrototypeOf(async function() { }).constructor; export class SQLiteError extends Error { constructor(message, code) { @@ -248,7 +248,7 @@ export function Factory(Module) { return check(fname, result, mapStmtToDB.get(stmt)); }; })(); - + sqlite3.close = (function() { const fname = 'sqlite3_close'; const f = Module.cwrap(fname, ...decl('n:n'), { async }); @@ -388,7 +388,7 @@ export function Factory(Module) { sqlite3.create_function = function(db, zFunctionName, nArg, eTextRep, pApp, xFunc, xStep, xFinal) { verifyDatabase(db); - + // Convert SQLite callback arguments to JavaScript-friendly arguments. function adapt(f) { return f instanceof AsyncFunction ? @@ -640,7 +640,7 @@ export function Factory(Module) { const result = Module.set_authorizer(db, adapt(xAuth), pApp); return check('sqlite3_set_authorizer', result, db); };; - + sqlite3.sql = (function() { const fname = 'sqlite3_sql'; const f = Module.cwrap(fname, ...decl('n:s')); @@ -673,7 +673,7 @@ export function Factory(Module) { onFinally.push(() => Module._sqlite3_free(pzHead)); Module.HEAPU8.set(utf8, pzHead); Module.HEAPU8[pzEnd - 1] = 0; - + // Use extra space for the statement handle and SQL tail pointer. const pStmt = pzHead + allocSize - 8; const pzTail = pzHead + allocSize - 4; @@ -687,7 +687,7 @@ export function Factory(Module) { stmt = 0; } onFinally.push(maybeFinalize); - + // Loop over statements. Module.setValue(pzTail, pzHead, '*'); do { @@ -710,7 +710,7 @@ export function Factory(Module) { if (rc !== SQLite.SQLITE_OK) { check('sqlite3_prepare_v3', rc, db); } - + stmt = Module.getValue(pStmt, '*'); if (stmt) { mapStmtToDB.set(stmt, db); @@ -752,7 +752,7 @@ export function Factory(Module) { iUpdateType, Module.UTF8ToString(dbName), Module.UTF8ToString(tblName), - cvt32x2ToBigInt(lo32, hi32) + cvt32x2ToBigInt(lo32, hi32) ]; }; function adapt(f) { @@ -762,7 +762,39 @@ export function Factory(Module) { } Module.update_hook(db, adapt(xUpdateHook)); - };; + }; + + sqlite3.trace = function(db, mTrace, xTrace) { + verifyDatabase(db) + + function cvtArgs(opCode, _pP, pX) { + // NOTE: only SQLITE_TRACE_STMT is currently implemented + switch (opCode) { + case SQLite.SQLITE_TRACE_STMT: + return [ + opCode, + "SQLITE_TRACE_STMT", + Module.UTF8ToString(pX) + ] + default: + // TODO: implement other variants + return [ + opCode, + "UNSUPPORTED_OP", + null + ] + } + } + + function adapt(f) { + return f instanceof AsyncFunction ? + (async (opCode, pP, pX) => f(...cvtArgs(opCode, pP, pX))) : + ((opCode, pP, pX) => f(...cvtArgs(opCode, pP, pX))) + } + + Module.trace(db, mTrace, adapt(xTrace)) + } + sqlite3.value = function(pValue) { const type = sqlite3.value_type(pValue); @@ -876,7 +908,7 @@ export function Factory(Module) { await Promise.all(Module.retryOps); Module.retryOps = []; } - + rc = await f(); // Retry on failure with new pending retry operations. diff --git a/src/sqlite-constants.js b/src/sqlite-constants.js index 3878b169..9d8375a7 100644 --- a/src/sqlite-constants.js +++ b/src/sqlite-constants.js @@ -272,4 +272,9 @@ export const SQLITE_LIMIT_WORKER_THREADS = 11; export const SQLITE_PREPARE_PERSISTENT = 0x01; export const SQLITE_PREPARE_NORMALIZED = 0x02; -export const SQLITE_PREPARE_NO_VTAB = 0x04; \ No newline at end of file +export const SQLITE_PREPARE_NO_VTAB = 0x04; + +export const SQLITE_TRACE_STMT = 0x01; +export const SQLITE_TRACE_PROFILE = 0x02; +export const SQLITE_TRACE_ROW = 0x04; +export const SQLITE_TRACE_CLOSE = 0x08; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 3eaa73a0..e178f69f 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -849,6 +849,11 @@ declare interface SQLiteAPI { * @returns `SQLITE_OK` (throws exception on error) */ vfs_register(vfs: SQLiteVFS, makeDefault?: boolean): number; + + trace( + db: number, mTrace: 1 | 2 | 3 | 4, + xTrace: (opCode: 1 | 2 | 3 | 4, opStr: string, sql?: string) => number + ): void } /** @ignore */