fix RETURNING, Statement::reset() and Program::abort()#5478
fix RETURNING, Statement::reset() and Program::abort()#5478jussisaurio merged 11 commits intomainfrom
Conversation
|
Small regression in this PR: CREATE TABLE t1(a INTEGER PRIMARY KEY, b TEXT);
-- Without RETURNING: correct
INSERT INTO t1 VALUES(1, 'a');
SELECT changes(); -- Returns: 1 ✓
-- With RETURNING: doubled
INSERT INTO t1 VALUES(10, 'r') RETURNING *;
SELECT changes(); -- Returns: 2 (expected: 1)
INSERT INTO t1 VALUES(11, 's'), (12, 't') RETURNING *;
SELECT changes(); -- Returns: 4 (expected: 2)
UPDATE t1 SET b = 'y' WHERE a >= 10 RETURNING *;
SELECT changes(); -- Returns: 6 (expected: 3)
DELETE FROM t1 WHERE a >= 10 RETURNING *;
SELECT changes(); -- Returns: 6 (expected: 3) |
| break; | ||
| } | ||
| } | ||
| Err(e) => { |
There was a problem hiding this comment.
does this mean that commits can fail silently?
There was a problem hiding this comment.
Yeah - I made reset() fallible in 6847bf2 and added reset_best_effort() for Drop
| 41 Insert 0 24 25 0 intkey=r[25] data=r[24] | ||
| 42 Goto 0 43 0 0 | ||
| 43 Goto 0 44 0 0 | ||
| 44 Rewind 0 50 0 0 Rewind t2 |
There was a problem hiding this comment.
bug in the comment here, this is the ephemeral table's cursor, but the comment says t2
There was a problem hiding this comment.
fixed this and made ephemeral table namings consistent in EXPLAIN
…g commit w/ async io
6136e2d to
327dd5c
Compare
327dd5c to
5b9eb68
Compare
5b9eb68 to
f23a822
Compare
fixed this - ephemeral inserts for returning were not marked with the ephemeral flag. added test. |
Problem: Statement::reset_internal skipped abort/rollback cleanup while the thread was panicking. During unwind, Drop called reset_best_effort(), but reset_internal intentionally avoided cleanup paths, so connection transaction state could remain write-open after a panic. Why this is a problem: a panic in user code while a statement is running can leave transactional state and locks behind, blocking subsequent writes on the same connection and violating best-effort cleanup guarantees for Drop. Fix: execute reset cleanup paths regardless of std::thread::panicking(), and make reset_best_effort() panic-safe with catch_unwind so Drop never introduces a second panic. Errors are still logged, but cleanup is attempted during unwind.
Problem: reset/drop cleanup had two correctness holes. First, reset_internal aborted pending completion groups directly, which can panic (group callback invoked before all children finish). Second, abort(None, ...) could skip end_statement paths and leave subjournal ownership marked in-use, causing sticky Busy behavior for later statements. Why this is a problem: abandoning statements during IO or error handling can leave internal transactional resources in a bad state, trigger panics in cleanup, and block subsequent statements despite best-effort reset/drop semantics. Fix: drain pending statement IO via wait() during reset instead of force-aborting the group completion; always finalize subjournal ownership in Program::abort; clear uses_subjournal in ProgramState::reset as a defensive invariant; and add simulator regressions for cross-connection reset errors, panic-drop cleanup, and subjournal release on reset/drop.
92e7ac2 to
c0ac8bc
Compare
core/vdbe/explain.rs
Outdated
| let get_table_or_index_name = |cursor_id: usize| { | ||
| let mut ephemeral_cursors = HashSet::new(); | ||
| let mut changed = true; | ||
| while changed { |
There was a problem hiding this comment.
This can be simplified, if I understand correctly it's meant to prevent adding non-ephemeral cursors from OpenDup to ephemeral_cursors, but OpenDup can only ever be used with ephemeral tables, so line 38 can be done inconditionally, and the while loop can be removed.
| e, | ||
| "Error committing during statement reset", | ||
| ); | ||
| break; |
There was a problem hiding this comment.
nit: this could be simplified by breaking a Result<()> out of the loop (break e)
Merging this PR will improve performance by 12.68%
Performance Changes
Comparing Footnotes
|
Closes #4388
Closes #5490
Problem 1 - RETURNING implemented wrong
We were not executing RETURNING the same way as SQLite. Our way was:
SQlite does:
This is a problem because a naive implementation of our approach on
mainwould result in this:Where not all INSERTs happened, so the statement would roll back. Think from user POV: you just executed
INSERT INTO ... RETURNING ...and got a result row back, decided not to step through the rest and moved on. Now your tx is rolled back and the inserts didn't happen. Bad.Our current workaround on main
On main, if there is a DML operation in progress, on Statement drop, we do a best-effort synchronous completion of the entire statement so that the DML effects of the statement persist.
Haltand commits or rollbacks based on therc(result code) on the VM.Fix
has_returned_rowflag indicating that we've emitted a ResultRow to caller.reset_internalif there's a DML statement (change_cnt_on) and we've emitted a row to caller, this signifies that all the DML is done and we now invoke Halt synchronously to commit the changes. In all other cases we roll back.EXPLAINnaming for ephemeral tablesProblem 2 - Statement::reset() was infallible and not handling cleanup properly
Statement::reset()was infallible, although it can error in several points - errors were just logged. This can mask errors and also leave them unhandled, which can result in e.g. lock leaks.Fix
reset()fallible; callreset_best_effort()fromStatement::Drop- which does abort cleanup on panic unwind to ensure resources are released.Problem 3 - Program::abort() could leave subjournal in use on error
This a problem because it can cause persistent
Busyon future connections since they can't use the subjournal for statement subtransactions.Fix
Capture error in
Program::abort()and return it at the end, but release subjournal in all cases.Tests
Simulatortests usingMemorySimIOfor fault injection, to verify that abandoning statement after seeingStepResult::IOrolls back unless RETURNING scanback had already started.Simulatortests to verify thatreset()errors do not cause resource leaks (unfreed WAL locks / subjournal use) during panics or errors propagated fromreset()