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

  1. Balance every BEGIN with exactly one COMMIT and add a `conn.is_autocommit()` check before BEGIN so nesting cannot accumulate.
  2. On any error between BEGIN and COMMIT, issue ROLLBACK immediately in the error path so the session returns to autocommit.
  3. Check for implicit BEGINs from hooks or extensions registered on the connection and remove them from the benchmark path.
  4. 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

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


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)