From 9c6fb011fb29f9c9fbf317ae2dd3094526a425d0 Mon Sep 17 00:00:00 2001 From: zerone0x Date: Fri, 6 Mar 2026 08:10:28 +0100 Subject: [PATCH] sqlite: report zero changes for read-only statements Read-only statements (e.g. SELECT) should always report 0 for both changes and lastInsertRowid. Previously, running a SELECT after an INSERT would leak the change count from the INSERT because sqlite3_changes64() returns the count from the most recent write statement on the connection. Use sqlite3_stmt_readonly() to detect read-only statements and return 0 instead of querying the connection-level counters. Fixes: https://github.com/nodejs/node/issues/59764 Co-Authored-By: Claude --- src/node_sqlite.cc | 9 ++++-- test/parallel/test-sqlite-statement-sync.js | 33 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 98612f39695897..7fe616f454f753 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -2698,8 +2698,13 @@ MaybeLocal StatementExecutionHelper::Run(Environment* env, int r = sqlite3_reset(stmt); CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, MaybeLocal()); - sqlite3_int64 last_insert_rowid = sqlite3_last_insert_rowid(db->Connection()); - sqlite3_int64 changes = sqlite3_changes64(db->Connection()); + // Read-only statements (e.g. SELECT) never modify rows, so report 0 + // instead of leaking the count from the most recent write statement. + bool is_readonly = sqlite3_stmt_readonly(stmt); + sqlite3_int64 last_insert_rowid = + is_readonly ? 0 : sqlite3_last_insert_rowid(db->Connection()); + sqlite3_int64 changes = + is_readonly ? 0 : sqlite3_changes64(db->Connection()); Local last_insert_rowid_val; Local changes_val; diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index aa7a3a73ae6649..281f6366082b5c 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -909,3 +909,36 @@ suite('options.allowBareNamedParameters', () => { ); }); }); + +suite('read-only statements report zero changes', () => { + test('SELECT after INSERT reports zero changes', (t) => { + const db = new DatabaseSync(':memory:'); + t.after(() => { db.close(); }); + db.exec('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)'); + const insert = db.prepare('INSERT INTO test (name) VALUES (?)'); + t.assert.deepStrictEqual( + insert.run('foo'), + { changes: 1, lastInsertRowid: 1 }, + ); + const select = db.prepare('SELECT * FROM test'); + t.assert.deepStrictEqual( + select.run(), + { changes: 0, lastInsertRowid: 0 }, + ); + }); + + test('SELECT after multiple INSERTs reports zero changes', (t) => { + const db = new DatabaseSync(':memory:'); + t.after(() => { db.close(); }); + db.exec('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)'); + const insert = db.prepare('INSERT INTO test (name) VALUES (?)'); + insert.run('a'); + insert.run('b'); + insert.run('c'); + const select = db.prepare('SELECT * FROM test'); + t.assert.deepStrictEqual( + select.run(), + { changes: 0, lastInsertRowid: 0 }, + ); + }); +});