tursodatabase/turso · error
COMMIT must end the transaction
Error message
COMMIT must end the transaction
What it means
After issuing `COMMIT` on each session, the harness asserts the connection returned to autocommit mode (`conn.is_autocommit()`). If it did not, the COMMIT did not actually end the open transaction — the workload's transactional invariant was broken. The library throws this so the benchmark never reports a completed transaction that SQLite is still holding open, which would corrupt timings and leave the WAL in an unexpected state.
Solutions
- Balance every BEGIN with exactly one COMMIT and add a `conn.is_autocommit()` check before BEGIN so nesting cannot accumulate.
- On any error between BEGIN and COMMIT, issue ROLLBACK immediately in the error path so the session returns to autocommit.
- Check for implicit BEGINs from hooks or extensions registered on the connection and remove them from the benchmark path.
- Verify the WAL busy behavior is not deferring the commit: ensure `conn.execute_batch("COMMIT")` result is checked and, on busy, retry only the COMMIT (not re-BEGIN).
Example fix
// before
conn.execute_batch("COMMIT")?;
ensure!(conn.is_autocommit(), "COMMIT must end the transaction");
// after
if !conn.is_autocommit() {
conn.execute_batch("COMMIT")?; // a nested/stale transaction was open; end it too
}
conn.execute_batch("COMMIT")?;
ensure!(conn.is_autocommit(), "COMMIT must end the transaction"); Defensive patterns
Strategy: validation
Validate before calling
// after the batch and before/after COMMIT
if !conn.is_autocommit() {
conn.execute_batch("COMMIT")?; // drain any nested transaction first
}
conn.execute_batch("COMMIT")?;
assert!(conn.is_autocommit(), "COMMIT must end the transaction"); Type guard
fn transaction_closed(conn: &rusqlite::Connection) -> bool {
conn.is_autocommit()
} Try / catch
match workload.run().await {
Ok(result) => result,
Err(e) if e.to_string().contains("COMMIT must end the transaction") => {
// force-close any lingering transaction and restore autocommit
for conn in workload.sessions() {
while !conn.is_autocommit() {
conn.execute_batch("COMMIT").or_else(|_| conn.execute_batch("ROLLBACK"))?;
}
}
workload.run().await?
}
Err(e) => return Err(e),
} Prevention
- Track transaction nesting explicitly if any code path can open a second transaction on the same connection.
- Issue ROLLBACK in every error branch between BEGIN and COMMIT.
- After each transactional batch, assert is_autocommit() so imbalance is caught at the first affected batch.
- With WAL and concurrent writers, be prepared for COMMIT to need a retry on busy rather than assuming it always finalizes.
When it happens
Trigger: Calling `SqliteWorkload::batch` with `Execution::Transactions { .. }` when `conn.execute_batch("COMMIT")` leaves `is_autocommit() == false` — typically because a nested transaction was opened (e.g. SAVEPOINT began inside the batch because a transaction was already active), a query inside the batch re-entered transaction mode, or the COMMIT statement failed to finalize (e.g. deferred statement still active or commit was retried/deferred by a busy handler).
Common situations: Sessions left with extra nesting after a prior batch failed between an outer BEGIN and this COMMIT; running with `journal_mode=wal` plus concurrent writers so COMMIT is retried and the harness observes a mid-commit state; a driver hook (tracing/extension callback) issuing implicit BEGIN during the batch's SELECT statements.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- BEGIN must start a transaction
- COMMIT must end the transaction
- SqliteCommand.ToSqliteException(ex)
- transactions must overlap before queries start
- 1
AI-assisted analysis of tursodatabase/turso@8d4a589f8d (2026-09-20).
Data as JSON: /api/errors/b140355fe9a29045.
Report an issue: GitHub.
Appendix: source
Thrown at perf/fts/src/sqlite.rs:110
});
}
let mut outcomes = Vec::new();
while let Some(outcome) = workers.join_next().await {
outcomes.push(outcome);
}
for outcome in outcomes {
let (conn, batch) = outcome?;
self.sessions.push(conn);
let batch = batch?;
result.queries += batch.queries;
result.rows += batch.rows;
result.id_sum += batch.id_sum;
}
}
if transactions {
for conn in &self.sessions {
conn.execute_batch("COMMIT")?;
ensure!(conn.is_autocommit(), "COMMIT must end the transaction");
}
result.transactions = self.sessions.len();
result.max_active_transactions = self.sessions.len();
}
Ok(result)
}
}
pub(super) fn query_batch(conn: &Connection, case: QueryCase, queries: usize) -> Result<RunResult> {
let mut result = RunResult::default();
for _ in 0..queries {
let mut statement = conn.prepare(sql(case))?;
let mut rows = statement.query([])?;
while let Some(row) = rows.next()? {
result.rows += 1;
result.id_sum += row.get::<_, i64>(0)?;
}
result.queries += 1;View on GitHub (pinned to 8d4a589f8d)