diff --git a/src/workerd/util/sqlite-test.c++ b/src/workerd/util/sqlite-test.c++ index c3021b4fc21..b27094e2555 100644 --- a/src/workerd/util/sqlite-test.c++ +++ b/src/workerd/util/sqlite-test.c++ @@ -1021,6 +1021,44 @@ KJ_TEST("SQLite failed statement reset") { KJ_EXPECT(db.run("SELECT COUNT(*) FROM things").getInt(0) == 4); } +KJ_TEST("SQLite extended error codes in messages") { + // Verify that error messages include named extended error codes when they differ from the + // primary error code. + auto dir = kj::newInMemoryDirectory(kj::nullClock()); + SqliteDatabase::Vfs vfs(*dir); + SqliteDatabase db(vfs, kj::Path({"foo"}), kj::WriteMode::CREATE | kj::WriteMode::MODIFY); + + db.run(R"( + CREATE TABLE things ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL + ); + )"); + + db.run("INSERT INTO things VALUES (1, 'alice')"); + + // UNIQUE/PRIMARY KEY constraint: extended code should be SQLITE_CONSTRAINT_PRIMARYKEY. + KJ_EXPECT_THROW_MESSAGE( + "(extended: SQLITE_CONSTRAINT_PRIMARYKEY)", db.run("INSERT INTO things VALUES (1, 'bob')")); + + // NOT NULL constraint: extended code should be SQLITE_CONSTRAINT_NOTNULL. + KJ_EXPECT_THROW_MESSAGE( + "(extended: SQLITE_CONSTRAINT_NOTNULL)", db.run("INSERT INTO things VALUES (2, NULL)")); + + // Errors where extended == primary should NOT have a parenthesized suffix. + // SQLITE_ERROR for "no such table" has no extended variant. + try { + db.run("SELECT * FROM nonexistent"); + KJ_FAIL_ASSERT("expected exception"); + } catch (kj::Exception& e) { + auto desc = e.getDescription(); + + KJ_EXPECT(desc.contains("SQLITE_ERROR"), desc); + // The message should NOT have a parenthesized extended code like "(SQLITE_ERROR_...)". + KJ_EXPECT(!desc.contains("(SQLITE_ERROR_"), desc); + } +} + class MockRollbackCallback { public: kj::Function create() { diff --git a/src/workerd/util/sqlite.c++ b/src/workerd/util/sqlite.c++ index 673cb91b9bc..84693921134 100644 --- a/src/workerd/util/sqlite.c++ +++ b/src/workerd/util/sqlite.c++ @@ -83,6 +83,93 @@ kj::String namedErrorCode(int errorCode) { #undef LITERAL } +// Maps extended error codes to their symbolic names. +// See https://www.sqlite.org/rescode.html#extended_result_code_list. +kj::Maybe namedExtendedErrorCode(int extendedErrorCode) { +#define LITERAL(name) \ + case name: \ + return kj::str(#name); + switch (extendedErrorCode) { + LITERAL(SQLITE_ABORT_ROLLBACK) + LITERAL(SQLITE_AUTH_USER) + LITERAL(SQLITE_BUSY_RECOVERY) + LITERAL(SQLITE_BUSY_SNAPSHOT) + LITERAL(SQLITE_BUSY_TIMEOUT) + LITERAL(SQLITE_CANTOPEN_CONVPATH) + LITERAL(SQLITE_CANTOPEN_DIRTYWAL) + LITERAL(SQLITE_CANTOPEN_FULLPATH) + LITERAL(SQLITE_CANTOPEN_ISDIR) + LITERAL(SQLITE_CANTOPEN_NOTEMPDIR) + LITERAL(SQLITE_CANTOPEN_SYMLINK) + LITERAL(SQLITE_CONSTRAINT_CHECK) + LITERAL(SQLITE_CONSTRAINT_COMMITHOOK) + LITERAL(SQLITE_CONSTRAINT_DATATYPE) + LITERAL(SQLITE_CONSTRAINT_FOREIGNKEY) + LITERAL(SQLITE_CONSTRAINT_FUNCTION) + LITERAL(SQLITE_CONSTRAINT_NOTNULL) + LITERAL(SQLITE_CONSTRAINT_PINNED) + LITERAL(SQLITE_CONSTRAINT_PRIMARYKEY) + LITERAL(SQLITE_CONSTRAINT_ROWID) + LITERAL(SQLITE_CONSTRAINT_TRIGGER) + LITERAL(SQLITE_CONSTRAINT_UNIQUE) + LITERAL(SQLITE_CONSTRAINT_VTAB) + LITERAL(SQLITE_CORRUPT_INDEX) + LITERAL(SQLITE_CORRUPT_SEQUENCE) + LITERAL(SQLITE_CORRUPT_VTAB) + LITERAL(SQLITE_ERROR_MISSING_COLLSEQ) + LITERAL(SQLITE_ERROR_RETRY) + LITERAL(SQLITE_ERROR_SNAPSHOT) + LITERAL(SQLITE_IOERR_ACCESS) + LITERAL(SQLITE_IOERR_AUTH) + LITERAL(SQLITE_IOERR_BEGIN_ATOMIC) + LITERAL(SQLITE_IOERR_BLOCKED) + LITERAL(SQLITE_IOERR_CHECKRESERVEDLOCK) + LITERAL(SQLITE_IOERR_CLOSE) + LITERAL(SQLITE_IOERR_COMMIT_ATOMIC) + LITERAL(SQLITE_IOERR_CONVPATH) + LITERAL(SQLITE_IOERR_CORRUPTFS) + LITERAL(SQLITE_IOERR_DATA) + LITERAL(SQLITE_IOERR_DELETE) + LITERAL(SQLITE_IOERR_DELETE_NOENT) + LITERAL(SQLITE_IOERR_DIR_CLOSE) + LITERAL(SQLITE_IOERR_DIR_FSYNC) + LITERAL(SQLITE_IOERR_FSTAT) + LITERAL(SQLITE_IOERR_FSYNC) + LITERAL(SQLITE_IOERR_GETTEMPPATH) + LITERAL(SQLITE_IOERR_LOCK) + LITERAL(SQLITE_IOERR_MMAP) + LITERAL(SQLITE_IOERR_NOMEM) + LITERAL(SQLITE_IOERR_RDLOCK) + LITERAL(SQLITE_IOERR_READ) + LITERAL(SQLITE_IOERR_ROLLBACK_ATOMIC) + LITERAL(SQLITE_IOERR_SEEK) + LITERAL(SQLITE_IOERR_SHMLOCK) + LITERAL(SQLITE_IOERR_SHMMAP) + LITERAL(SQLITE_IOERR_SHMOPEN) + LITERAL(SQLITE_IOERR_SHMSIZE) + LITERAL(SQLITE_IOERR_SHORT_READ) + LITERAL(SQLITE_IOERR_TRUNCATE) + LITERAL(SQLITE_IOERR_UNLOCK) + LITERAL(SQLITE_IOERR_VNODE) + LITERAL(SQLITE_IOERR_WRITE) + LITERAL(SQLITE_LOCKED_SHAREDCACHE) + LITERAL(SQLITE_LOCKED_VTAB) + LITERAL(SQLITE_NOTICE_RECOVER_ROLLBACK) + LITERAL(SQLITE_NOTICE_RECOVER_WAL) + LITERAL(SQLITE_OK_LOAD_PERMANENTLY) + LITERAL(SQLITE_READONLY_CANTINIT) + LITERAL(SQLITE_READONLY_CANTLOCK) + LITERAL(SQLITE_READONLY_DBMOVED) + LITERAL(SQLITE_READONLY_DIRECTORY) + LITERAL(SQLITE_READONLY_RECOVERY) + LITERAL(SQLITE_READONLY_ROLLBACK) + LITERAL(SQLITE_WARNING_AUTOINDEX) + default: + return kj::none; + } +#undef LITERAL +} + constexpr size_t RA_MAX_METRICS_QUERY_SIZE = 1024; kj::String dbErrorMessage(int errorCode, sqlite3* db) { @@ -91,6 +178,12 @@ kj::String dbErrorMessage(int errorCode, sqlite3* db) { msg = kj::strTree(kj::mv(msg), " at offset ", offset); } msg = kj::strTree(kj::mv(msg), ": ", namedErrorCode(errorCode)); + int extendedCode = sqlite3_extended_errcode(db); + if (extendedCode != errorCode) { + KJ_IF_SOME(extendedName, namedExtendedErrorCode(extendedCode)) { + msg = kj::strTree(kj::mv(msg), " (extended: ", extendedName, ")"); + } + } return msg.flatten(); }