commit e714497143b635564ea1df807ee52bc6bc0d0643
parent b9db78e6eb55a36f348348e9932f2a82c4283732
Author: Florian Dold <dold@taler.net>
Date: Sun, 9 Aug 2026 22:10:19 +0200
sqlite: expose structured errors
Diffstat:
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") {