diff --git a/package.json b/package.json index 77872c4..7d0a77a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "prebuildify": "prebuildify --strip --napi", "test": "vitest --coverage --pool threads", "format": "run-p --print-label format:c format:js", - "format:c": "xcrun clang-format --style=chromium -Werror --verbose -i src/*.cc", + "format:c": "xcrun clang-format --style=chromium -Werror --verbose -i src/*.cc src/*.h", "format:js": "prettier --cache --write .", "lint": "run-p --print-label check:eslint check:format", "check:eslint": "eslint --cache .", diff --git a/src/addon.cc b/src/addon.cc index 1ade940..e008a5a 100644 --- a/src/addon.cc +++ b/src/addon.cc @@ -5,6 +5,7 @@ #include #include "addon.h" +#include "errors.h" #include "napi.h" #include "signal-tokenizer.h" @@ -246,14 +247,32 @@ Napi::Value Database::ThrowSqliteError(Napi::Env env, int error) { const char* msg = sqlite3_errmsg(handle_); int offset = sqlite3_error_offset(handle_); int extended = sqlite3_extended_errcode(handle_); + +#define EXTENDED_STR(NAME) \ + case NAME: \ + extended_name = #NAME; \ + break; + + const char* extended_name; + switch (extended) { + SQLITE_ERROR_ENUM(EXTENDED_STR) + default: + extended_name = "unknown"; + break; + } + +#undef EXTENDED_STR + + Napi::Error err; if (offset == -1) { - NAPI_THROW(FormatError(env, "sqlite error(%d): %s", extended, msg), - Napi::Value()); + err = FormatError(env, "sqlite error(%s): %s", extended_name, msg); } else { - NAPI_THROW(FormatError(env, "sqlite error(%d): %s, offset: %d", extended, - msg, offset), - Napi::Value()); + err = FormatError(env, "sqlite error(%s): %s, offset: %d", extended_name, + msg, offset); } + + err.Set("code", extended_name); + NAPI_THROW(err, Napi::Value()); } fts5_api* Database::GetFTS5API(Napi::Env env) { diff --git a/src/addon.h b/src/addon.h index 28aed2a..b854441 100644 --- a/src/addon.h +++ b/src/addon.h @@ -93,7 +93,7 @@ class Statement { if (std::isspace(ch) || ch == ';') { p = p.substr(1); - } else if (p.rfind("--", 0) == 0) { + } else if (p.rfind("--", 0) == 0) { // Line comment: "--" p = p.substr(2); diff --git a/src/errors.h b/src/errors.h new file mode 100644 index 0000000..c1ce843 --- /dev/null +++ b/src/errors.h @@ -0,0 +1,116 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +#ifndef SRC_ERRORS_H_ +#define SRC_ERRORS_H_ + +#define SQLITE_ERROR_ENUM(V) \ + V(SQLITE_ERROR) \ + V(SQLITE_INTERNAL) \ + V(SQLITE_PERM) \ + V(SQLITE_ABORT) \ + V(SQLITE_BUSY) \ + V(SQLITE_LOCKED) \ + V(SQLITE_NOMEM) \ + V(SQLITE_READONLY) \ + V(SQLITE_INTERRUPT) \ + V(SQLITE_IOERR) \ + V(SQLITE_CORRUPT) \ + V(SQLITE_NOTFOUND) \ + V(SQLITE_FULL) \ + V(SQLITE_CANTOPEN) \ + V(SQLITE_PROTOCOL) \ + V(SQLITE_EMPTY) \ + V(SQLITE_SCHEMA) \ + V(SQLITE_TOOBIG) \ + V(SQLITE_CONSTRAINT) \ + V(SQLITE_MISMATCH) \ + V(SQLITE_MISUSE) \ + V(SQLITE_NOLFS) \ + V(SQLITE_AUTH) \ + V(SQLITE_FORMAT) \ + V(SQLITE_RANGE) \ + V(SQLITE_NOTADB) \ + V(SQLITE_NOTICE) \ + V(SQLITE_WARNING) \ + V(SQLITE_ROW) \ + V(SQLITE_DONE) \ + V(SQLITE_ERROR_MISSING_COLLSEQ) \ + V(SQLITE_ERROR_RETRY) \ + V(SQLITE_ERROR_SNAPSHOT) \ + V(SQLITE_IOERR_READ) \ + V(SQLITE_IOERR_SHORT_READ) \ + V(SQLITE_IOERR_WRITE) \ + V(SQLITE_IOERR_FSYNC) \ + V(SQLITE_IOERR_DIR_FSYNC) \ + V(SQLITE_IOERR_TRUNCATE) \ + V(SQLITE_IOERR_FSTAT) \ + V(SQLITE_IOERR_UNLOCK) \ + V(SQLITE_IOERR_RDLOCK) \ + V(SQLITE_IOERR_DELETE) \ + V(SQLITE_IOERR_BLOCKED) \ + V(SQLITE_IOERR_NOMEM) \ + V(SQLITE_IOERR_ACCESS) \ + V(SQLITE_IOERR_CHECKRESERVEDLOCK) \ + V(SQLITE_IOERR_LOCK) \ + V(SQLITE_IOERR_CLOSE) \ + V(SQLITE_IOERR_DIR_CLOSE) \ + V(SQLITE_IOERR_SHMOPEN) \ + V(SQLITE_IOERR_SHMSIZE) \ + V(SQLITE_IOERR_SHMLOCK) \ + V(SQLITE_IOERR_SHMMAP) \ + V(SQLITE_IOERR_SEEK) \ + V(SQLITE_IOERR_DELETE_NOENT) \ + V(SQLITE_IOERR_MMAP) \ + V(SQLITE_IOERR_GETTEMPPATH) \ + V(SQLITE_IOERR_CONVPATH) \ + V(SQLITE_IOERR_VNODE) \ + V(SQLITE_IOERR_AUTH) \ + V(SQLITE_IOERR_BEGIN_ATOMIC) \ + V(SQLITE_IOERR_COMMIT_ATOMIC) \ + V(SQLITE_IOERR_ROLLBACK_ATOMIC) \ + V(SQLITE_IOERR_DATA) \ + V(SQLITE_IOERR_CORRUPTFS) \ + V(SQLITE_IOERR_IN_PAGE) \ + V(SQLITE_LOCKED_SHAREDCACHE) \ + V(SQLITE_LOCKED_VTAB) \ + V(SQLITE_BUSY_RECOVERY) \ + V(SQLITE_BUSY_SNAPSHOT) \ + V(SQLITE_BUSY_TIMEOUT) \ + V(SQLITE_CANTOPEN_NOTEMPDIR) \ + V(SQLITE_CANTOPEN_ISDIR) \ + V(SQLITE_CANTOPEN_FULLPATH) \ + V(SQLITE_CANTOPEN_CONVPATH) \ + V(SQLITE_CANTOPEN_DIRTYWAL) \ + V(SQLITE_CANTOPEN_SYMLINK) \ + V(SQLITE_CORRUPT_VTAB) \ + V(SQLITE_CORRUPT_SEQUENCE) \ + V(SQLITE_CORRUPT_INDEX) \ + V(SQLITE_READONLY_RECOVERY) \ + V(SQLITE_READONLY_CANTLOCK) \ + V(SQLITE_READONLY_ROLLBACK) \ + V(SQLITE_READONLY_DBMOVED) \ + V(SQLITE_READONLY_CANTINIT) \ + V(SQLITE_READONLY_DIRECTORY) \ + V(SQLITE_ABORT_ROLLBACK) \ + V(SQLITE_CONSTRAINT_CHECK) \ + V(SQLITE_CONSTRAINT_COMMITHOOK) \ + V(SQLITE_CONSTRAINT_FOREIGNKEY) \ + V(SQLITE_CONSTRAINT_FUNCTION) \ + V(SQLITE_CONSTRAINT_NOTNULL) \ + V(SQLITE_CONSTRAINT_PRIMARYKEY) \ + V(SQLITE_CONSTRAINT_TRIGGER) \ + V(SQLITE_CONSTRAINT_UNIQUE) \ + V(SQLITE_CONSTRAINT_VTAB) \ + V(SQLITE_CONSTRAINT_ROWID) \ + V(SQLITE_CONSTRAINT_PINNED) \ + V(SQLITE_CONSTRAINT_DATATYPE) \ + V(SQLITE_NOTICE_RECOVER_WAL) \ + V(SQLITE_NOTICE_RECOVER_ROLLBACK) \ + V(SQLITE_NOTICE_RBU) \ + V(SQLITE_WARNING_AUTOINDEX) \ + V(SQLITE_AUTH_USER) \ + V(SQLITE_OK_LOAD_PERMANENTLY) \ + V(SQLITE_OK_SYMLINK) + +#endif // SRC_ERRORS_H_ diff --git a/test/memory.test.ts b/test/memory.test.ts index a70424d..340ee93 100644 --- a/test/memory.test.ts +++ b/test/memory.test.ts @@ -390,6 +390,25 @@ test('bigint mode', () => { ).toEqual(n); }); +test('extended error codes', () => { + db.exec(` + CREATE TABLE parent (id INTEGER PRIMARY KEY); + + CREATE TABLE refs (id INTEGER, FOREIGN KEY (id) REFERENCES parent(id)); + `); + + const stmt = db.prepare('INSERT INTO refs (id) VALUES (?)'); + + expect(() => stmt.run([4])).toThrowError( + expect.objectContaining({ + message: + 'sqlite error(SQLITE_CONSTRAINT_FOREIGNKEY): ' + + 'FOREIGN KEY constraint failed', + code: 'SQLITE_CONSTRAINT_FOREIGNKEY', + }), + ); +}); + test('tokenizer setup', () => { db.initTokenizer(); });