{"record":{"id":"1921ff920630b482","repo":"tursodatabase/turso","slug":"begin-must-start-a-transaction","errorCode":null,"errorMessage":"BEGIN must start a transaction","messagePattern":"BEGIN must start a transaction","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"perf/fts/src/sqlite.rs","lineNumber":79,"sourceCode":"        };\n        let mut result = RunResult::default();\n        for _ in 0..self.config.execution.batches() {\n            let batch = self.batch(queries).await?;\n            result.queries += batch.queries;\n            result.transactions += batch.transactions;\n            result.rows += batch.rows;\n            result.id_sum += batch.id_sum;\n            result.max_active_transactions = batch.max_active_transactions;\n        }\n        Ok(result)\n    }\n\n    async fn batch(&mut self, queries: usize) -> Result<RunResult> {\n        let transactions = matches!(self.config.execution, Execution::Transactions { .. });\n        if transactions {\n            for conn in &self.sessions {\n                conn.execute_batch(\"BEGIN\")?;\n                ensure!(!conn.is_autocommit(), \"BEGIN must start a transaction\");\n            }\n        }\n        let mut result = RunResult::default();\n        if self.sessions.len() == 1 {\n            result = query_batch(&self.sessions[0], self.config.query, queries)?;\n        } else {\n            let mut workers = tokio::task::JoinSet::new();\n            for conn in self.sessions.drain(..) {\n                let case = self.config.query;\n                workers.spawn_blocking(move || {\n                    let result = query_batch(&conn, case, queries);\n                    (conn, result)\n                });\n            }\n            let mut outcomes = Vec::new();\n            while let Some(outcome) = workers.join_next().await {\n                outcomes.push(outcome);\n            }","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/tursodatabase/turso/blob/8d4a589f8d13ac184700d2a8f724f27e1995be3b/perf/fts/src/sqlite.rs#L61-L97","documentation":"After the benchmark issues `BEGIN` on a session, it asserts that the connection actually left autocommit mode (`!conn.is_autocommit()`). This check fails when SQLite did not enter an explicit transaction despite the BEGIN statement executing without an error — meaning the transaction control invariant of the workload harness was violated. The library throws it to fail fast rather than silently benchmarking in autocommit mode, which would produce misleading throughput numbers.","triggerScenarios":"Calling `SqliteWorkload::batch` with `config.execution == Execution::Transactions { .. }` when a session's `BEGIN` via `conn.execute_batch(\"BEGIN\")` does not flip the connection out of autocommit — e.g. the connection is already inside an explicit transaction from a prior leaked/uncommitted batch, or a driver/trace hook (such as a statement-level auto-BEGIN or arusqlite busy/interrupt path) consumed or aborted the BEGIN.","commonSituations":"Reusing connections across batches where an earlier COMMIT failed or was never reached because `query_batch` returned an error mid-transaction (leaving the session with an open transaction); configuring multi-session runs where one worker panics inside `spawn_blocking` and the sessions are re-used; wrapping the whole benchmark in outer transaction-control code that already issued BEGIN.","solutions":["Check `conn.is_autocommit()` before issuing BEGIN for each session and roll back (`conn.execute_batch(\"ROLLBACK\")` or `conn.execute_batch(\"COMMIT\")`) any stale open transaction first.","Ensure every error path between BEGIN and COMMIT issues a ROLLBACK so sessions return to autocommit before the next batch.","Verify no outer code (wrapper, benchmark harness, tracing hook) has already started a transaction on these connections.","If a panic occurred in a spawn_blocking worker, recreate the affected connections instead of reusing them."],"exampleFix":"// before\nconn.execute_batch(\"BEGIN\")?;\nensure!(!conn.is_autocommit(), \"BEGIN must start a transaction\");\n// after\nif !conn.is_autocommit() {\n    conn.execute_batch(\"ROLLBACK\")?; // clear stale transaction from a failed prior batch\n}\nconn.execute_batch(\"BEGIN\")?;\nensure!(!conn.is_autocommit(), \"BEGIN must start a transaction\");","handlingStrategy":"validation","validationCode":"// before starting a transactional batch on each connection\nif !conn.is_autocommit() {\n    // stale transaction from a previous failed batch — clear it\n    conn.execute_batch(\"ROLLBACK\")?;\n}\nassert!(conn.is_autocommit(), \"connection not ready for BEGIN\");","typeGuard":"fn ready_for_begin(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(\"BEGIN must start a transaction\") => {\n        // roll back stale transactions on all sessions, then retry once\n        for conn in workload.sessions() {\n            let _ = conn.execute_batch(\"ROLLBACK\");\n        }\n        workload.run().await?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Always pair BEGIN/COMMIT in a scope guard so ROLLBACK runs on every error path.","Check is_autocommit() before each BEGIN in long-running loops that reuse connections.","Never reuse a connection after a panic in a worker thread without resetting its transaction state.","Avoid registering hooks or extensions that issue implicit BEGIN during reads."],"tags":["sqlite","transaction","autocommit","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"}