{"record":{"id":"b140355fe9a29045","repo":"tursodatabase/turso","slug":"commit-must-end-the-transaction-sqlite","errorCode":null,"errorMessage":"COMMIT must end the transaction","messagePattern":"COMMIT must end the transaction","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"perf/fts/src/sqlite.rs","lineNumber":110,"sourceCode":"                });\n            }\n            let mut outcomes = Vec::new();\n            while let Some(outcome) = workers.join_next().await {\n                outcomes.push(outcome);\n            }\n            for outcome in outcomes {\n                let (conn, batch) = outcome?;\n                self.sessions.push(conn);\n                let batch = batch?;\n                result.queries += batch.queries;\n                result.rows += batch.rows;\n                result.id_sum += batch.id_sum;\n            }\n        }\n        if transactions {\n            for conn in &self.sessions {\n                conn.execute_batch(\"COMMIT\")?;\n                ensure!(conn.is_autocommit(), \"COMMIT must end the transaction\");\n            }\n            result.transactions = self.sessions.len();\n            result.max_active_transactions = self.sessions.len();\n        }\n        Ok(result)\n    }\n}\n\npub(super) fn query_batch(conn: &Connection, case: QueryCase, queries: usize) -> Result<RunResult> {\n    let mut result = RunResult::default();\n    for _ in 0..queries {\n        let mut statement = conn.prepare(sql(case))?;\n        let mut rows = statement.query([])?;\n        while let Some(row) = rows.next()? {\n            result.rows += 1;\n            result.id_sum += row.get::<_, i64>(0)?;\n        }\n        result.queries += 1;","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/tursodatabase/turso/blob/8d4a589f8d13ac184700d2a8f724f27e1995be3b/perf/fts/src/sqlite.rs#L92-L128","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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)."],"exampleFix":"// before\nconn.execute_batch(\"COMMIT\")?;\nensure!(conn.is_autocommit(), \"COMMIT must end the transaction\");\n// after\nif !conn.is_autocommit() {\n    conn.execute_batch(\"COMMIT\")?; // a nested/stale transaction was open; end it too\n}\nconn.execute_batch(\"COMMIT\")?;\nensure!(conn.is_autocommit(), \"COMMIT must end the transaction\");","handlingStrategy":"validation","validationCode":"// after the batch and before/after COMMIT\nif !conn.is_autocommit() {\n    conn.execute_batch(\"COMMIT\")?; // drain any nested transaction first\n}\nconn.execute_batch(\"COMMIT\")?;\nassert!(conn.is_autocommit(), \"COMMIT must end the transaction\");","typeGuard":"fn transaction_closed(conn: &rusqlite::Connection) -> bool {\n    conn.is_autocommit()\n}","tryCatchPattern":"match workload.run().await {\n    Ok(result) => result,\n    Err(e) if e.to_string().contains(\"COMMIT must end the transaction\") => {\n        // force-close any lingering transaction and restore autocommit\n        for conn in workload.sessions() {\n            while !conn.is_autocommit() {\n                conn.execute_batch(\"COMMIT\").or_else(|_| conn.execute_batch(\"ROLLBACK\"))?;\n            }\n        }\n        workload.run().await?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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."],"tags":["sqlite","transaction","commit","benchmark","invariant"],"backgroundTag":"internal-invariant-violation","analyzedSha":"8d4a589f8d13ac184700d2a8f724f27e1995be3b","analyzedAt":"2026-09-20T13:18:14.658Z","contentChangedAt":"2026-09-20T13:18:14.658Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}