quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

commit e714497143b635564ea1df807ee52bc6bc0d0643
parent b9db78e6eb55a36f348348e9932f2a82c4283732
Author: Florian Dold <dold@taler.net>
Date:   Sun,  9 Aug 2026 22:10:19 +0200

sqlite: expose structured errors

Diffstat:
Mtart_module.c | 189++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mtests/test_sqlite3_error.js | 41+++++++++++++++++++++++++++++++++++++++--
2 files changed, 203 insertions(+), 27 deletions(-)

diff --git a/tart_module.c b/tart_module.c @@ -1575,6 +1575,7 @@ static JSValue js_talercrypto_hash_state_finish(JSContext *ctx, JSValue this_val static JSClassID js_sqlite3_database_class_id; static JSClassID js_sqlite3_statement_class_id; +static JSClassID js_sqlite3_error_class_id; static void js_sqlite3_database_finalizer(JSRuntime *rt, JSValue val) { @@ -1604,6 +1605,15 @@ static JSClassDef js_sqlite3_statement_class = { .finalizer = js_sqlite3_statement_finalizer, }; +/* + * Sqlite3Error instances use QuickJS' built-in Error class so that they get + * normal Error behaviour and stack traces. This otherwise-unused class ID + * gives each context a place to retain the Sqlite3Error prototype. + */ +static JSClassDef js_sqlite3_error_class = { + .class_name = "Sqlite3Error", +}; + #define ERRCASE(c) case c: return #c const char *translate_sqlite3_err_to_string(int errcode) @@ -1676,6 +1686,9 @@ const char *translate_sqlite3_err_to_string(int errcode) ERRCASE(SQLITE_IOERR_ROLLBACK_ATOMIC); ERRCASE(SQLITE_IOERR_DATA); ERRCASE(SQLITE_IOERR_CORRUPTFS); +#ifdef SQLITE_IOERR_IN_PAGE + ERRCASE(SQLITE_IOERR_IN_PAGE); +#endif ERRCASE(SQLITE_LOCKED_SHAREDCACHE); ERRCASE(SQLITE_LOCKED_VTAB); ERRCASE(SQLITE_BUSY_RECOVERY); @@ -1724,32 +1737,101 @@ const char *translate_sqlite3_err_to_string(int errcode) } -static JSValue throw_sqlite3_error(JSContext *ctx, sqlite3 *db) +static JSValue new_sqlite3_error_with_proto(JSContext *ctx, + JSValueConst proto) { JSValue obj; obj = JS_NewError(ctx); if (JS_IsException(obj)) { - /* out of memory: throw JS_NULL to avoid recursing */ - obj = JS_NULL; - goto done; + return obj; + } + if (JS_SetPrototype(ctx, obj, proto) < 0) { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + return obj; +} + +static JSValue new_sqlite3_error(JSContext *ctx) +{ + JSValue obj; + JSValue proto; + + proto = JS_GetClassProto(ctx, js_sqlite3_error_class_id); + if (JS_IsException(proto)) { + return proto; + } + obj = new_sqlite3_error_with_proto(ctx, proto); + JS_FreeValue(ctx, proto); + return obj; +} + +static JSValue js_sqlite3_error_constructor(JSContext *ctx, + JSValueConst new_target, + int argc, + JSValueConst *argv) +{ + JSValue obj; + JSValue proto; + + proto = JS_GetPropertyStr(ctx, new_target, "prototype"); + if (JS_IsException(proto)) { + return proto; + } + obj = new_sqlite3_error_with_proto(ctx, proto); + JS_FreeValue(ctx, proto); + if (JS_IsException(obj)) { + return obj; + } + if (argc > 0 && !JS_IsUndefined(argv[0])) { + JSValue message = JS_ToString(ctx, argv[0]); + if (JS_IsException(message) || + JS_DefinePropertyValueStr( + ctx, + obj, + "message", + message, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0) { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + } + return obj; +} + +static JSValue throw_sqlite3_error(JSContext *ctx, sqlite3 *db) +{ + JSValue obj; + int error_code; + + error_code = sqlite3_extended_errcode(db); + obj = new_sqlite3_error(ctx); + if (JS_IsException(obj)) { + return obj; } - JS_DefinePropertyValueStr( + if (JS_DefinePropertyValueStr( ctx, obj, "message", JS_NewString(ctx, sqlite3_errmsg(db)), - JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); - - JS_DefinePropertyValueStr( + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0 || + JS_DefinePropertyValueStr( ctx, obj, "code", - JS_NewString(ctx, translate_sqlite3_err_to_string(sqlite3_errcode(db))), - JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); - -done: + JS_NewString(ctx, translate_sqlite3_err_to_string(error_code)), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0 || + JS_DefinePropertyValueStr( + ctx, + obj, + "errno", + JS_NewInt32(ctx, error_code), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0) { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } return JS_Throw(ctx, obj); } @@ -1856,7 +1938,7 @@ static JSValue js_sqlite3_prepare(JSContext *ctx, JSValue this_val, ret = sqlite3_prepare_v3(sqlite3_db, stmt_cstr, (int) strlen(stmt_cstr), 0, &stmt, &tail); if (SQLITE_OK != ret) { - ret_val = JS_ThrowTypeError(ctx, "unable to prepare"); + ret_val = throw_sqlite3_error(ctx, sqlite3_db); goto done; } @@ -2084,12 +2166,12 @@ static JSValue js_sqlite3_stmt_run(JSContext *ctx, JSValue this_val, sqlret = sqlite3_reset(stmt); if (SQLITE_OK != sqlret) { fprintf(stderr, "sqlite3_reset failed (in stmt_run): %s\n", sqlite3_errmsg(db)); - ret_val = JS_ThrowTypeError(ctx, "failed to reset"); + ret_val = throw_sqlite3_error(ctx, db); goto done; } sqlret = sqlite3_clear_bindings(stmt); if (SQLITE_OK != sqlret) { - ret_val = JS_ThrowTypeError(ctx, "failed to clear bindings"); + ret_val = throw_sqlite3_error(ctx, db); goto done; } if (argc > 1) { @@ -2117,7 +2199,7 @@ static JSValue js_sqlite3_stmt_run(JSContext *ctx, JSValue this_val, if (SQLITE_OK != sqlret) { fprintf(stderr, "sqlite3_reset failed (in stmt_run after SQLITE_DONE): %s\n", sqlite3_errmsg(db)); JS_FreeValue(ctx, ret_val); - ret_val = JS_ThrowTypeError(ctx, "failed to reset"); + ret_val = throw_sqlite3_error(ctx, db); goto done; } goto done; @@ -2204,12 +2286,12 @@ static JSValue js_sqlite3_stmt_get_all(JSContext *ctx, JSValue this_val, sqlret = sqlite3_reset(stmt); if (SQLITE_OK != sqlret) { fprintf(stderr, "sqlite3_reset failed (in stmt_get_all): %s\n", sqlite3_errmsg(db)); - ret_val = JS_ThrowTypeError(ctx, "failed to reset"); + ret_val = throw_sqlite3_error(ctx, db); goto done; } sqlret = sqlite3_clear_bindings(stmt); if (SQLITE_OK != sqlret) { - ret_val = JS_ThrowTypeError(ctx, "failed to clear bindings"); + ret_val = throw_sqlite3_error(ctx, db); goto done; } if (argc > 1) { @@ -2234,7 +2316,7 @@ static JSValue js_sqlite3_stmt_get_all(JSContext *ctx, JSValue this_val, sqlret = sqlite3_reset(stmt); if (SQLITE_OK != sqlret) { fprintf(stderr, "sqlite3_reset failed (in stmt_get_all after SQLITE_DONE): %s\n", sqlite3_errmsg(db)); - ret_val = JS_ThrowTypeError(ctx, "failed to reset"); + ret_val = throw_sqlite3_error(ctx, db); } else { ret_val = JS_DupValue(ctx, rows_array); } @@ -2272,12 +2354,12 @@ static JSValue js_sqlite3_stmt_get_first(JSContext *ctx, JSValue this_val, sqlret = sqlite3_reset(stmt); if (SQLITE_OK != sqlret) { fprintf(stderr, "sqlite3_reset failed (in stmt_get_first): %s\n", sqlite3_errmsg(db)); - ret_val = JS_ThrowTypeError(ctx, "failed to reset"); + ret_val = throw_sqlite3_error(ctx, db); goto done; } sqlret = sqlite3_clear_bindings(stmt); if (SQLITE_OK != sqlret) { - ret_val = JS_ThrowTypeError(ctx, "failed to clear bindings"); + ret_val = throw_sqlite3_error(ctx, db); goto done; } if (argc > 1) { @@ -2312,7 +2394,7 @@ reset: if (SQLITE_OK != sqlret) { fprintf(stderr, "sqlite3_reset failed (in stmt_get_first): %s\n", sqlite3_errmsg(db)); JS_FreeValue(ctx, ret_val); - ret_val = JS_ThrowTypeError(ctx, "failed to reset"); + ret_val = throw_sqlite3_error(ctx, db); } done: return ret_val; @@ -2362,10 +2444,17 @@ static void tart_init_class_ids(void) JS_NewClassID(&js_hash_state_class_id); JS_NewClassID(&js_sqlite3_database_class_id); JS_NewClassID(&js_sqlite3_statement_class_id); + JS_NewClassID(&js_sqlite3_error_class_id); } static int tart_talercrypto_init(JSContext *ctx, JSModuleDef *m) { + JSValue error_class; + JSValue error_base_proto; + JSValue error_constructor; + JSValue error_proto; + JSValue global; + if (0 != pthread_once(&tart_class_ids_once, tart_init_class_ids)) { return -1; } @@ -2374,12 +2463,61 @@ static int tart_talercrypto_init(JSContext *ctx, JSModuleDef *m) 0 != JS_NewClass(JS_GetRuntime(ctx), js_sqlite3_database_class_id, &js_sqlite3_database_class) || 0 != JS_NewClass(JS_GetRuntime(ctx), js_sqlite3_statement_class_id, - &js_sqlite3_statement_class)) { + &js_sqlite3_statement_class) || + 0 != JS_NewClass(JS_GetRuntime(ctx), js_sqlite3_error_class_id, + &js_sqlite3_error_class)) { + return -1; + } + + global = JS_GetGlobalObject(ctx); + error_class = JS_GetPropertyStr(ctx, global, "Error"); + JS_FreeValue(ctx, global); + if (JS_IsException(error_class)) { + return -1; + } + error_base_proto = JS_GetPropertyStr(ctx, error_class, "prototype"); + JS_FreeValue(ctx, error_class); + if (JS_IsException(error_base_proto)) { + return -1; + } + error_proto = JS_NewObjectProto(ctx, error_base_proto); + JS_FreeValue(ctx, error_base_proto); + if (JS_IsException(error_proto)) { + return -1; + } + if (JS_DefinePropertyValueStr( + ctx, + error_proto, + "name", + JS_NewString(ctx, "Sqlite3Error"), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0) { + JS_FreeValue(ctx, error_proto); return -1; } - return JS_SetModuleExportList(ctx, m, tart_talercrypto_funcs, - countof(tart_talercrypto_funcs)); + error_constructor = JS_NewCFunction2(ctx, + js_sqlite3_error_constructor, + "Sqlite3Error", + 1, + JS_CFUNC_constructor, + 0); + if (JS_IsException(error_constructor)) { + JS_FreeValue(ctx, error_proto); + return -1; + } + if (JS_SetConstructor(ctx, error_constructor, error_proto) < 0) { + JS_FreeValue(ctx, error_constructor); + JS_FreeValue(ctx, error_proto); + return -1; + } + JS_SetClassProto(ctx, js_sqlite3_error_class_id, error_proto); + + if (JS_SetModuleExportList(ctx, m, tart_talercrypto_funcs, + countof(tart_talercrypto_funcs)) < 0) { + JS_FreeValue(ctx, error_constructor); + return -1; + } + return JS_SetModuleExport(ctx, m, "Sqlite3Error", error_constructor); } JSModuleDef *tart_init_module_talercrypto(JSContext *ctx, const char *module_name) @@ -2391,5 +2529,6 @@ JSModuleDef *tart_init_module_talercrypto(JSContext *ctx, const char *module_nam } JS_AddModuleExportList(ctx, m, tart_talercrypto_funcs, countof(tart_talercrypto_funcs)); + JS_AddModuleExport(ctx, m, "Sqlite3Error"); return m; } diff --git a/tests/test_sqlite3_error.js b/tests/test_sqlite3_error.js @@ -1,15 +1,52 @@ import * as os from "os"; import * as tart from "tart"; -const dbPath = `/tmp/qtart-sqlite3-get-first-${Date.now()}.sqlite3`; +const dbPath = `/tmp/qtart-sqlite3-error-${Date.now()}.sqlite3`; let db; let stmt; +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function expectSqlite3Error(fn, code, messagePart) { + try { + fn(); + } catch (error) { + assert(error instanceof tart.Sqlite3Error, + `expected Sqlite3Error, got ${error?.constructor?.name}`); + assert(error instanceof Error, "Sqlite3Error must inherit from Error"); + assert(error.name === "Sqlite3Error", + `unexpected error name: ${error.name}`); + assert(error.code === code, + `expected ${code}, got ${error.code}`); + assert(Number.isInteger(error.errno) && error.errno !== 0, + `unexpected SQLite error number: ${error.errno}`); + assert(error.message.includes(messagePart), + `unexpected SQLite error message: ${error.message}`); + return; + } + throw new Error(`expected ${code} Sqlite3Error`); +} + try { db = tart.sqlite3Open(dbPath); - tart.sqlite3Exec(db, "CREATE TABLE entries (value TEXT)"); + tart.sqlite3Exec(db, "CREATE TABLE entries (value TEXT UNIQUE)"); tart.sqlite3Exec(db, "INSERT INTO entries VALUES ('one')"); + expectSqlite3Error( + () => tart.sqlite3Prepare(db, "SELECT value FROM missing_table"), + "SQLITE_ERROR", + "no such table: missing_table", + ); + expectSqlite3Error( + () => tart.sqlite3Exec(db, "INSERT INTO entries VALUES ('one')"), + "SQLITE_CONSTRAINT_UNIQUE", + "UNIQUE constraint failed: entries.value", + ); + stmt = tart.sqlite3Prepare(db, "SELECT value FROM entries"); const row = tart.sqlite3StmtGetFirst(stmt); if (row?.value !== "one") {