From 70c57f7137da85cec81f2d3ac4abb70377625be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 30 Jun 2026 14:44:20 -0300 Subject: [PATCH 1/2] sqlite: read column count after step in StatementSync.all() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Guilherme Araújo --- src/node_sqlite.cc | 6 ++++- test/parallel/test-sqlite-statement-sync.js | 27 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 049fd786126fce..c3b7d279f4dca0 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -2865,12 +2865,16 @@ MaybeLocal StatementExecutionHelper::All(Environment* env, Isolate* isolate = env->isolate(); EscapableHandleScope scope(isolate); int r; - int num_cols = sqlite3_column_count(stmt); + int num_cols = 0; LocalVector rows(isolate); LocalVector row_values(isolate); LocalVector row_keys(isolate); while ((r = sqlite3_step(stmt)) == SQLITE_ROW) { + if (num_cols == 0) { + num_cols = sqlite3_column_count(stmt); + } + if (ExtractRowValues(env, stmt, num_cols, use_big_ints, &row_values) .IsNothing()) { return MaybeLocal(); diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index aa7a3a73ae6649..e3370513d98e10 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -83,6 +83,33 @@ suite('StatementSync.prototype.all()', () => { { __proto__: null, key: 'key2', val: 'val2' }, ]); }); + + test('reflects an added column on first use after the schema changes', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + db.exec('CREATE TABLE storage(key TEXT, val TEXT)'); + db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1'); + + const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'"); + t.assert.deepStrictEqual(stmt.all(), [ + { __proto__: null, key: 'key1', val: 'val1', extra: 'def' }, + ]); + }); + + test('reflects a dropped column on first use after the schema changes', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)'); + db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)') + .run('key1', 'val1', 'x'); + + const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + db.exec('ALTER TABLE storage DROP COLUMN extra'); + t.assert.deepStrictEqual(stmt.all(), [ + { __proto__: null, key: 'key1', val: 'val1' }, + ]); + }); }); suite('StatementSync.prototype.iterate()', () => { From 4a55b8e583e1ebee521a87ecf9f59e70a8ec10b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Tue, 30 Jun 2026 15:36:06 -0300 Subject: [PATCH 2/2] sqlite: add schema-change regression tests --- test/parallel/test-sqlite-statement-sync.js | 58 ++++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index e3370513d98e10..b3a1dc434537c3 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -53,6 +53,29 @@ suite('StatementSync.prototype.get()', () => { const stmt = db.prepare('SELECT 1 as __proto__, 2 as constructor, 3 as toString'); t.assert.deepStrictEqual(stmt.get(), { __proto__: null, ['__proto__']: 1, constructor: 2, toString: 3 }); }); + + test('reflects an added column after the schema changes', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT)'); + db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1'); + const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'"); + t.assert.deepStrictEqual(stmt.get(), { + __proto__: null, key: 'key1', val: 'val1', extra: 'def', + }); + }); + + test('reflects a dropped column after the schema changes', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)'); + db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)') + .run('key1', 'val1', 'x'); + const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + db.exec('ALTER TABLE storage DROP COLUMN extra'); + t.assert.deepStrictEqual(stmt.get(), { + __proto__: null, key: 'key1', val: 'val1', + }); + }); }); suite('StatementSync.prototype.all()', () => { @@ -84,12 +107,10 @@ suite('StatementSync.prototype.all()', () => { ]); }); - test('reflects an added column on first use after the schema changes', (t) => { - const db = new DatabaseSync(nextDb()); - t.after(() => { db.close(); }); + test('reflects an added column after the schema changes', (t) => { + using db = new DatabaseSync(':memory:'); db.exec('CREATE TABLE storage(key TEXT, val TEXT)'); db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1'); - const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'"); t.assert.deepStrictEqual(stmt.all(), [ @@ -97,13 +118,11 @@ suite('StatementSync.prototype.all()', () => { ]); }); - test('reflects a dropped column on first use after the schema changes', (t) => { - const db = new DatabaseSync(nextDb()); - t.after(() => { db.close(); }); + test('reflects a dropped column after the schema changes', (t) => { + using db = new DatabaseSync(':memory:'); db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)'); db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)') .run('key1', 'val1', 'x'); - const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); db.exec('ALTER TABLE storage DROP COLUMN extra'); t.assert.deepStrictEqual(stmt.all(), [ @@ -152,6 +171,29 @@ suite('StatementSync.prototype.iterate()', () => { } }); + test('reflects an added column after the schema changes', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT)'); + db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1'); + const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'"); + t.assert.deepStrictEqual(stmt.iterate().toArray(), [ + { __proto__: null, key: 'key1', val: 'val1', extra: 'def' }, + ]); + }); + + test('reflects a dropped column after the schema changes', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)'); + db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)') + .run('key1', 'val1', 'x'); + const stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + db.exec('ALTER TABLE storage DROP COLUMN extra'); + t.assert.deepStrictEqual(stmt.iterate().toArray(), [ + { __proto__: null, key: 'key1', val: 'val1' }, + ]); + }); + test('iterator keeps the prepared statement from being collected', (t) => { const db = new DatabaseSync(':memory:'); db.exec(`