From 880d4262653471d0df1097f6dbeff5950db8fa86 Mon Sep 17 00:00:00 2001 From: islandryu Date: Sun, 12 Oct 2025 17:19:29 +0900 Subject: [PATCH 1/3] sqlite: improve error reporting for prepared statements in SQLTagStore Fixes: https://github.com/nodejs/node/issues/60198 Signed-off-by: islandryu --- src/env_properties.h | 1 + src/node_sqlite.cc | 47 ++++++++++++++++++++++- test/parallel/test-sqlite-template-tag.js | 13 +++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/env_properties.h b/src/env_properties.h index eb26d3b6cf05..c04656b7284a 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -178,6 +178,7 @@ V(errno_string, "errno") \ V(error_string, "error") \ V(errstr_string, "errstr") \ + V(errmsg_string, "errmsg") \ V(events_waiting, "eventsWaiting") \ V(events, "events") \ V(exclusive_string, "exclusive") \ diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index c6b8a8d0a611..1eca3fd96322 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -290,6 +290,35 @@ inline MaybeLocal CreateSQLiteError(Isolate* isolate, sqlite3* db) { isolate, sqlite3_errmsg(db), sqlite3_errstr(errcode), errcode); } +inline MaybeLocal CreateSQLiteError(Isolate* isolate, + sqlite3* db, + const char* message) { + int errcode = sqlite3_extended_errcode(db); + const char* errstr = sqlite3_errstr(errcode); + const char* errmsg = sqlite3_errmsg(db); + Local js_errstr; + Local js_errmsg; + Local e; + if (!String::NewFromUtf8(isolate, errstr).ToLocal(&js_errmsg) || + !String::NewFromUtf8(isolate, errmsg).ToLocal(&js_errstr) || + !CreateSQLiteError(isolate, message).ToLocal(&e) || + e->Set(isolate->GetCurrentContext(), + Environment::GetCurrent(isolate)->errcode_string(), + Integer::New(isolate, errcode)) + .IsNothing() || + e->Set(isolate->GetCurrentContext(), + Environment::GetCurrent(isolate)->errstr_string(), + js_errstr) + .IsNothing() || + e->Set(isolate->GetCurrentContext(), + Environment::GetCurrent(isolate)->errmsg_string(), + js_errmsg) + .IsNothing()) { + return MaybeLocal(); + } + return e; +} + void JSValueToSQLiteResult(Isolate* isolate, sqlite3_context* ctx, Local value) { @@ -344,6 +373,20 @@ inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, const char* message) { } } +inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, + DatabaseSync* db, + const char* message) { + if (db->ShouldIgnoreSQLiteError()) { + db->SetIgnoreNextSQLiteError(false); + return; + } + + Local e; + if (CreateSQLiteError(isolate, db->Connection(), message).ToLocal(&e)) { + isolate->ThrowException(e); + } +} + inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, int errcode) { const char* errstr = sqlite3_errstr(errcode); @@ -3972,7 +4015,9 @@ BaseObjectPtr SQLTagStore::PrepareStatement( StatementPtr stmt_ptr(s); if (r != SQLITE_OK) { - THROW_ERR_SQLITE_ERROR(isolate, session->database_.get()); + THROW_ERR_SQLITE_ERROR( + isolate, session->database_.get(), "Failed to prepare statement"); + sqlite3_finalize(s); return BaseObjectPtr(); } diff --git a/test/parallel/test-sqlite-template-tag.js b/test/parallel/test-sqlite-template-tag.js index eaa6d19fc7cd..9e3f31f7983b 100644 --- a/test/parallel/test-sqlite-template-tag.js +++ b/test/parallel/test-sqlite-template-tag.js @@ -388,3 +388,16 @@ test('cached statements are finalized when the database is closed', () => { message: /no such table/i, }); }); + +test('failed prepares throw', () => { + assert.throws(() => { + sql.all`SELECT * FROM does_not_exist`; // eslint-disable-line no-unused-expressions + }, { + name: 'Error', + message: 'Failed to prepare statement', + code: 'ERR_SQLITE_ERROR', + errcode: 1, + errstr: 'no such table: does_not_exist', + errmsg: 'SQL logic error' + }); +}); From 4f2fe698a4bd016d60ad569b606479620839de22 Mon Sep 17 00:00:00 2001 From: islandryu Date: Sun, 12 Oct 2025 20:25:52 +0900 Subject: [PATCH 2/3] swap errstr and errmsg Signed-off-by: islandryu --- src/node_sqlite.cc | 4 ++-- test/parallel/test-sqlite-template-tag.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 1eca3fd96322..9698ad741da8 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -299,8 +299,8 @@ inline MaybeLocal CreateSQLiteError(Isolate* isolate, Local js_errstr; Local js_errmsg; Local e; - if (!String::NewFromUtf8(isolate, errstr).ToLocal(&js_errmsg) || - !String::NewFromUtf8(isolate, errmsg).ToLocal(&js_errstr) || + if (!String::NewFromUtf8(isolate, errstr).ToLocal(&js_errstr) || + !String::NewFromUtf8(isolate, errmsg).ToLocal(&js_errmsg) || !CreateSQLiteError(isolate, message).ToLocal(&e) || e->Set(isolate->GetCurrentContext(), Environment::GetCurrent(isolate)->errcode_string(), diff --git a/test/parallel/test-sqlite-template-tag.js b/test/parallel/test-sqlite-template-tag.js index 9e3f31f7983b..f17701b409aa 100644 --- a/test/parallel/test-sqlite-template-tag.js +++ b/test/parallel/test-sqlite-template-tag.js @@ -391,13 +391,13 @@ test('cached statements are finalized when the database is closed', () => { test('failed prepares throw', () => { assert.throws(() => { - sql.all`SELECT * FROM does_not_exist`; // eslint-disable-line no-unused-expressions + sql.all`SELECT * FROM does_not_exist`; }, { name: 'Error', message: 'Failed to prepare statement', code: 'ERR_SQLITE_ERROR', errcode: 1, - errstr: 'no such table: does_not_exist', - errmsg: 'SQL logic error' + errstr: 'SQL logic error', + errmsg: 'no such table: does_not_exist' }); }); From 954b99625e83b9a6ffe85fafaba0ab110b57ab6a Mon Sep 17 00:00:00 2001 From: islandryu Date: Sun, 23 Aug 2026 10:27:51 +0900 Subject: [PATCH 3/3] test: align sqlite template tag error assertions --- test/parallel/test-sqlite-template-tag.js | 31 +++++++++++++---------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/test/parallel/test-sqlite-template-tag.js b/test/parallel/test-sqlite-template-tag.js index f17701b409aa..42242bf0d110 100644 --- a/test/parallel/test-sqlite-template-tag.js +++ b/test/parallel/test-sqlite-template-tag.js @@ -298,22 +298,22 @@ test('sql.db returns the associated DatabaseSync instance', () => { test('sql error messages are descriptive', () => { assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'test'})`.changes, 1); - // Test with non-existent column - assert.throws(() => { - const result = sql.get`SELECT nonexistent_column FROM foo`; - assert.fail(`Expected error, got: ${JSON.stringify(result)}`); - }, { + assert.throws(() => sql.get`SELECT nonexistent_column FROM foo`, { + name: 'Error', code: 'ERR_SQLITE_ERROR', - message: /no such column/i, + message: 'Failed to prepare statement', + errcode: 1, + errstr: 'SQL logic error', + errmsg: /no such column/i, }); - // Test with non-existent table - assert.throws(() => { - const result = sql.get`SELECT * FROM nonexistent_table`; - assert.fail(`Expected error, got: ${JSON.stringify(result)}`); - }, { + assert.throws(() => sql.get`SELECT * FROM nonexistent_table`, { + name: 'Error', code: 'ERR_SQLITE_ERROR', - message: /no such table/i, + message: 'Failed to prepare statement', + errcode: 1, + errstr: 'SQL logic error', + errmsg: /no such table/i, }); }); @@ -384,13 +384,18 @@ test('cached statements are finalized when the database is closed', () => { db.open(); assert.throws(() => sql.all`SELECT id FROM foo`, { + name: 'Error', code: 'ERR_SQLITE_ERROR', - message: /no such table/i, + message: 'Failed to prepare statement', + errcode: 1, + errstr: 'SQL logic error', + errmsg: 'no such table: foo' }); }); test('failed prepares throw', () => { assert.throws(() => { + // eslint-disable-next-line no-unused-expressions sql.all`SELECT * FROM does_not_exist`; }, { name: 'Error',