Skip to content

fix(sqlite): reset transaction depth after SQLite rolls back on its own - #4515

Open
yxshee wants to merge 1 commit into
transact-rs:mainfrom
yxshee:fix/sqlite-stale-depth
Open

yxshee wants to merge 1 commit into
transact-rs:mainfrom
yxshee:fix/sqlite-stale-depth

Conversation

@yxshee

@yxshee yxshee commented Nov 9, 2026

Copy link
Copy Markdown

When a statement fails with SQLITE_FULL (and a few other errors), SQLite rolls the whole transaction back by itself. The worker still thinks it is inside the transaction, so when the Transaction is dropped it sends ROLLBACK (or ROLLBACK TO for a savepoint), that fails with "no transaction is active" / "no such savepoint", and transaction_depth is never decremented. After that the connection is stuck: begin_with(...) always returns InvalidSavePointStatement and begin() issues a SAVEPOINT instead of BEGIN.

Reproduction from the issue, in short:

sqlx::query("PRAGMA max_page_count = 2").execute(&mut conn).await?;
let mut tx = conn.begin().await?;
sqlx::query("INSERT INTO t VALUES (zeroblob(65536))").execute(&mut *tx).await.unwrap_err(); // code 13
drop(tx);
conn.begin_with("BEGIN IMMEDIATE").await?; // Err(InvalidSavePointStatement), expected Ok

The rollback handler now checks sqlite3_get_autocommit (in_transaction(), already used for custom BEGIN) first. If depth is non-zero but SQLite is no longer in a transaction, it resets the depth to 0 and reports success instead of running a rollback that can only fail. A failed commit() leaves the Transaction open, so its drop goes through the same path.

Added it_can_recover_from_automatic_rollback in tests/sqlite/sqlite.rs, which hits SQLITE_FULL inside a nested transaction and then calls begin_with("BEGIN IMMEDIATE"). On main it fails with attempted to call begin_with at non-zero transaction depth. With the change, the SQLite test suite (--features any,macros,migrate,sqlite,_unstable-all-types,runtime-tokio,sqlite-preupdate-hook), cargo test -p sqlx-sqlite --all-features, clippy with -D warnings and cargo fmt --check all pass locally.

Does your PR solve an issue?

fixes #4434

Is this a breaking change?

No. The only behavior change is that rolling back a transaction SQLite has already rolled back returns Ok(()) and leaves the connection usable, where before it returned an error and left the connection unable to start a new transaction.

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