sqlite: check sqlite3_step() and sqlite3_reset() results - #63319
sqlite: check sqlite3_step() and sqlite3_reset() results#63319semimikoh wants to merge 2 commits into
Conversation
|
Review requested:
|
557bcde to
4740589
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #63319 +/- ##
==========================================
+ Coverage 90.29% 90.31% +0.01%
==========================================
Files 759 759
Lines 248295 248322 +27
Branches 46861 46876 +15
==========================================
+ Hits 224205 224268 +63
+ Misses 15517 15473 -44
- Partials 8573 8581 +8
🚀 New features to boost your workflow:
|
|
I tried to reproduce a situation where the current code wouldn't catch the error but I couldn't. Please, get such a case into a test so it's clear which situation we must cover. |
TrevorBurnham
left a comment
There was a problem hiding this comment.
Reviewed the reset-error handling. The direction looks right; sqlite3_reset() returning a deferred error from the prior sqlite3_step() is real and worth surfacing. Two things I'd want addressed:
-
In
StatementSyncIterator,RESET_OR_THROWexpands to areturn, so the new throwing paths skipiter->done_ = trueeven though the statement has already been reset. A caught error then leaves the iterator resumable, and it replays the result set from the top. Details inline. -
No tests.
get()/all()can now throw where they previously returned an already-built row/array, which is a user-visible change on a success path. Worth coverage pinning the new behavior, plus a note on whether it needs semver-major.
Things I checked that look correct: needs_reset = false is sequenced before sqlite3_reset(), so there's no double reset on the throwing path; no function can call THROW_ERR_SQLITE_ERROR twice, so the ShouldIgnoreSQLiteError() one-shot isn't consumed twice; void() threads through both macro layers; and every RESET_AND_CHECK caller keeps its OnScopeLeave safety net for the earlier failure paths.
| Isolate* isolate = env->isolate(); | ||
|
|
||
| sqlite3_reset(iter->stmt_->statement_); | ||
| RESET_OR_THROW( |
There was a problem hiding this comment.
CHECK_ERROR_OR_THROW does return (ret);, so when this reset reports a deferred error, iter->done_ = true on the next line is skipped — but sqlite3_reset() has already reset the statement, and reset_generation_ wasn't bumped (this is a raw reset, not ResetStatement()). So:
const it = stmt.iterate();
try { for (const row of it) break; } catch {} // it.return() throws
it.next(); // done_ === false, generation matches -> re-steps from row 1Setting done_ = true before the checked reset fixes it.
Separately: iterator.return() is called by the language during abrupt completion, including exception unwinding (for (const row of it) { throw err; }). Throwing here discards the user's pending exception, which is exactly what the PR description's "avoid replacing an already-pending exception" rule is meant to prevent — and this is the one place it isn't applied. Worth considering whether Return() should keep ignoring the reset result.
| CHECK_ERROR_OR_THROW( | ||
| env->isolate(), iter->stmt_->db_.get(), r, SQLITE_DONE, void()); | ||
| sqlite3_reset(iter->stmt_->statement_); | ||
| RESET_OR_THROW(env->isolate(), |
There was a problem hiding this comment.
Same shape: this early return skips the {done: true, value: null} result, and done_ is never set on this path (it's only written in the constructor and in Return()), so a caught error leaves the iterator resumable on an already-reset statement.
Setting iter->done_ = true here also fixes a pre-existing bug — after natural exhaustion the iterator already restarts today:
const it = stmt.iterate();
while (!it.next().done);
it.next(); // yields row 1 again| isolate, Null(isolate), keys.data(), row_values.data(), num_cols); | ||
| } | ||
|
|
||
| RESET_AND_CHECK(isolate, db, stmt, needs_reset, MaybeLocal<Value>()); |
There was a problem hiding this comment.
After a SQLITE_ROW step the VDBE is still in RUN_STATE, so this reset runs sqlite3VdbeHalt() and can genuinely fail — unlike the SQLITE_DONE path above, where the halt already happened during the step, making that check effectively a no-op. So get() can now throw after the row was built: e.g. INSERT ... RETURNING id with PRAGMA foreign_keys = ON and a deferred FK violation, where the implicit commit fails at reset. Same for all(), which discards a fully-built array.
That's arguably the more correct behavior, but it's a change on a path that currently succeeds, so it deserves a test and possibly a notable-change label.
| }); | ||
|
|
||
| int step_r = sqlite3_step(stmt); | ||
| if (step_r != SQLITE_DONE && step_r != SQLITE_ROW) { |
There was a problem hiding this comment.
Nit: accepting SQLITE_ROW here is right (run() on a RETURNING/SELECT statement should step once and discard) and matches the previous behavior of ignoring the step result entirely. A short comment would keep someone from "tightening" this to != SQLITE_DONE later.
4740589 to
4443a70
Compare
|
Rebased onto
|
Signed-off-by: semimikoh <ejffjeosms@gmail.com>
This comment was marked as outdated.
This comment was marked as outdated.
4443a70 to
ba4e195
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
ba4e195 to
ea9f2df
Compare
This comment was marked as outdated.
This comment was marked as outdated.
- 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 a pending exception propagating correctly when the loop body throws mid-iteration. Signed-off-by: semimikoh <ejffjeosms@gmail.com>
ea9f2df to
52592e6
Compare
|
@trivikr CI is green now (conflicts resolved, lint fixed). Ready for another look whenever you have time. |
Summary
Per the SQLite docs,
sqlite3_reset(S)may return a deferred error codefrom the prior
sqlite3_step(S)call. Several statement execution paths insrc/node_sqlite.ccdropped that return value, which could silently ignoreSQLite errors.
This also checks the previously ignored
sqlite3_step()result inStatementExecutionHelper::Run().Fixes: #63311
Approach
Successful execution paths now explicitly check
sqlite3_reset().Functions with early-return or V8-exception paths keep an
OnScopeLeavereset guard so prepared statements are left reusable. The guard intentionally
drops the reset result to avoid replacing an already-pending SQLite or V8
exception.
StatementSyncIterator::Next()andStatementSyncIterator::Return()use adirect checked reset because their control flow is linear.