Skip to content

Commit ba4e195

Browse files
committed
sqlite: address review feedback on reset error handling
- StatementSyncIterator::Return() no longer throws on a deferred reset error, matching the OnScopeLeave guards used elsewhere: it is invoked during abrupt iterator completion (e.g. a throw inside a for...of body), and throwing there would discard the caller's already-pending exception. - Add a short comment on the accepted SQLITE_ROW result in Run(). - Add tests covering get()/all() surfacing a deferred SQLite error from reset() after already building a row/array, the iterator not replaying results after natural exhaustion, and Return() not discarding a pending exception on a deferred reset error. Signed-off-by: semimikoh <ejffjeosms@gmail.com>
1 parent 2fae197 commit ba4e195

2 files changed

Lines changed: 92 additions & 2 deletions

File tree

‎src/node_sqlite.cc‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3006,6 +3006,9 @@ MaybeLocal<Object> StatementExecutionHelper::Run(Environment* env,
30063006
});
30073007

30083008
int step_r = sqlite3_step(stmt);
3009+
// SQLITE_ROW is accepted here (and discarded) so that run() can still be
3010+
// used on RETURNING/SELECT statements, matching prior behavior of
3011+
// ignoring the step result entirely.
30093012
if (step_r != SQLITE_DONE && step_r != SQLITE_ROW) {
30103013
THROW_ERR_SQLITE_ERROR(isolate, db);
30113014
return MaybeLocal<Object>();
@@ -3894,8 +3897,11 @@ void StatementSyncIterator::Return(const FunctionCallbackInfo<Value>& args) {
38943897
env, iter->stmt_->IsFinalized(), "statement has been finalized");
38953898
Isolate* isolate = env->isolate();
38963899

3897-
RESET_OR_THROW(
3898-
isolate, iter->stmt_->db_.get(), iter->stmt_->statement_, void());
3900+
// Unlike Next(), the reset result is intentionally ignored here: Return()
3901+
// is invoked by the language during abrupt completion (e.g. a `throw`
3902+
// inside a `for...of` body), and throwing on a deferred SQLite error
3903+
// would discard the caller's already-pending exception.
3904+
sqlite3_reset(iter->stmt_->statement_);
38993905
iter->done_ = true;
39003906

39013907
auto iter_template = getLazyIterTemplate(env);

‎test/parallel/test-sqlite-statement-sync.js‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,28 @@ suite('StatementSync.prototype.get()', () => {
7979
message: /statement has been finalized/,
8080
});
8181
});
82+
83+
test('surfaces a deferred SQLite error from reset() even though a row was already built', (t) => {
84+
using db = new DatabaseSync(':memory:');
85+
db.exec(`
86+
PRAGMA foreign_keys = ON;
87+
PRAGMA defer_foreign_keys = ON;
88+
CREATE TABLE parent(id INTEGER PRIMARY KEY);
89+
CREATE TABLE child(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id));
90+
`);
91+
// The FK check is deferred until the implicit transaction commits, which
92+
// happens inside reset() here because RETURNING leaves the statement's
93+
// VDBE running after the row is produced.
94+
const stmt = db.prepare(
95+
'INSERT INTO child (parent_id) VALUES (999) RETURNING id'
96+
);
97+
t.assert.throws(() => {
98+
stmt.get();
99+
}, {
100+
code: 'ERR_SQLITE_ERROR',
101+
message: /FOREIGN KEY constraint failed/,
102+
});
103+
});
82104
});
83105

84106
suite('StatementSync.prototype.all()', () => {
@@ -144,6 +166,25 @@ suite('StatementSync.prototype.all()', () => {
144166
message: /statement has been finalized/,
145167
});
146168
});
169+
170+
test('surfaces a deferred SQLite error from reset() even though the array was already built', (t) => {
171+
using db = new DatabaseSync(':memory:');
172+
db.exec(`
173+
PRAGMA foreign_keys = ON;
174+
PRAGMA defer_foreign_keys = ON;
175+
CREATE TABLE parent(id INTEGER PRIMARY KEY);
176+
CREATE TABLE child(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id));
177+
`);
178+
const stmt = db.prepare(
179+
'INSERT INTO child (parent_id) VALUES (999) RETURNING id'
180+
);
181+
t.assert.throws(() => {
182+
stmt.all();
183+
}, {
184+
code: 'ERR_SQLITE_ERROR',
185+
message: /FOREIGN KEY constraint failed/,
186+
});
187+
});
147188
});
148189

149190
suite('StatementSync.prototype.iterate()', () => {
@@ -322,6 +363,49 @@ suite('StatementSync.prototype.iterate()', () => {
322363
message: /statement has been finalized/,
323364
});
324365
});
366+
367+
test('does not replay results after the iterator is naturally exhausted', (t) => {
368+
using db = new DatabaseSync(':memory:');
369+
db.exec(`
370+
CREATE TABLE test(key TEXT);
371+
INSERT INTO test (key) VALUES ('key1');
372+
`);
373+
const it = db.prepare('SELECT * FROM test').iterate();
374+
t.assert.deepStrictEqual(it.next(), {
375+
__proto__: null, done: false, value: { __proto__: null, key: 'key1' },
376+
});
377+
t.assert.deepStrictEqual(
378+
it.next(), { __proto__: null, done: true, value: null });
379+
// Calling next() again on an exhausted iterator must keep reporting
380+
// done, not silently reset the statement and replay from row 1.
381+
t.assert.deepStrictEqual(
382+
it.next(), { __proto__: null, done: true, value: null });
383+
});
384+
385+
test('does not discard a pending exception with a deferred SQLite error on abrupt exit', (t) => {
386+
using db = new DatabaseSync(':memory:');
387+
db.exec(`
388+
PRAGMA foreign_keys = ON;
389+
PRAGMA defer_foreign_keys = ON;
390+
CREATE TABLE parent(id INTEGER PRIMARY KEY);
391+
CREATE TABLE child(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id));
392+
`);
393+
// Two RETURNING rows so the statement is still mid-execution (VDBE
394+
// running) when the loop body throws after the first row, forcing
395+
// iterator.return() to reset a statement with a deferred FK violation
396+
// still pending.
397+
const stmt = db.prepare(`
398+
INSERT INTO child (parent_id)
399+
SELECT column1 FROM (VALUES (999), (998))
400+
RETURNING id
401+
`);
402+
const userError = new Error('boom');
403+
t.assert.throws(() => {
404+
for (const _row of stmt.iterate()) {
405+
throw userError;
406+
}
407+
}, (err) => err === userError);
408+
});
325409
});
326410

327411
suite('StatementSync.prototype.run()', () => {

0 commit comments

Comments
 (0)