Skip to content

fix(sqlite): keep transaction depth in sync after SQLite rolls back on its own - #4438

Open
jayzhou2309 wants to merge 2 commits into
transact-rs:mainfrom
jayzhou2309:fix/sqlite-stale-depth-after-autorollback
Open

jayzhou2309 wants to merge 2 commits into
transact-rs:mainfrom
jayzhou2309:fix/sqlite-stale-depth-after-autorollback

Conversation

@jayzhou2309

Copy link
Copy Markdown

Does your PR solve an issue?

fixes #4434

Is this a breaking change?

No. A Transaction that SQLite already rolled back now drops (or calls rollback()) cleanly and leaves the connection at depth 0. Before, its ROLLBACK failed and left a stale depth behind. commit() on such a transaction still returns SQLite's cannot commit - no transaction is active error.

Why

On some errors, such as SQLITE_FULL, SQLite rolls back the whole transaction by itself (sqlite3_get_autocommit). When the Transaction is then dropped, the worker runs ROLLBACK, which fails because no transaction is active. Command::Rollback decremented transaction_depth only when the statement succeeded, so the connection kept depth 1. The next begin_with("BEGIN IMMEDIATE") was rejected with InvalidSavePointStatement before it reached SQLite, as the issue reports.

The worker now checks in_transaction() (the same autocommit check the custom-BEGIN path already uses) before it runs the rollback statement. With no active transaction, it skips the statement and still decrements the depth. For nested savepoints, each Transaction level decrements once as it drops, so the depth reaches 0 when the outermost one is gone.

Scope

  • sqlx-sqlite/src/connection/worker.rs: Command::Rollback skips the rollback statement when SQLite reports no active transaction, and decrements the depth either way.
  • tests/sqlite/sqlite.rs: new it_can_begin_after_sqlite_rolls_back_a_full_transaction, the issue's repro. It forces SQLITE_FULL with PRAGMA max_page_count = 2, drops the transaction, raises the limit, then runs and commits a BEGIN IMMEDIATE transaction.

Verification

  • The new test fails on main with attempted to call begin_with at non-zero transaction depth and passes with the fix. It lands in its own commit before the fix.
  • A throwaway test (not committed) checked two more cases on the fix. A savepoint inside the failed transaction recovers the same way. commit() after the auto-rollback still returns cannot commit - no transaction is active.
  • cargo test --no-default-features --features any,macros,migrate,sqlite,_unstable-all-types,runtime-tokio with DATABASE_URL=sqlite:tests/sqlite/sqlite.db: sqlite 45 passed, 1 ignored; sqlite-any 3; sqlite-error 6; sqlite-describe 31; sqlite-types 73. cargo test -p sqlx-sqlite passes.
  • cargo fmt --all -- --check and cargo clippy -D warnings on the pinned 1.94 toolchain: clean on sqlx-sqlite.
  • Not run: the async-global-executor and smol runtimes and sqlite-unbundled linking. The change doesn't depend on runtime or linking.

AI disclosure

This PR was written by an AI agent (Claude Code) on behalf of the account owner. The agent reproduced the bug with the new test, wrote the fix and the test, and ran the commands listed under Verification on macOS.

🤖 Written and posted by an AI agent (Claude Code) on behalf of @jayzhou2309.

jayzhou2309 and others added 2 commits October 1, 2026 23:25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n its own

On errors like SQLITE_FULL, SQLite rolls back the whole transaction by
itself. The ROLLBACK that SQLx sends when the `Transaction` is dropped then
fails, and the depth counter was only decremented on success. The connection
kept a stale depth, so a later `begin_with` failed with
`InvalidSavePointStatement`.

Skip the ROLLBACK statement when SQLite reports no active transaction and
still decrement the depth.

Fixes transact-rs#4434

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQLite: SQLITE_FULL leaves stale transaction depth and breaks subsequent begin_with calls

1 participant