{"record":{"id":"96bc2615b44eb0c0","repo":"tursodatabase/turso","slug":"cursor-id-cursor-id-is-none","errorCode":null,"errorMessage":"cursor id {cursor_id} is None","messagePattern":"cursor id (.+?) is None","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/vdbe/mod.rs","lineNumber":1473,"sourceCode":"            crate::statement::StatementStatusCounter::VmStep => self.metrics.insn_executed = 0,\n            crate::statement::StatementStatusCounter::Reprepare => self.metrics.reprepares = 0,\n            crate::statement::StatementStatusCounter::RowsRead => self.metrics.rows_read = 0,\n            crate::statement::StatementStatusCounter::RowsWritten => self.metrics.rows_written = 0,\n        }\n        if let Some(OpProgramState::Step { statement, .. }) = self.active_op_state.program_mut() {\n            statement.reset_stmt_status(counter);\n        }\n        for statement in self.subprogram_stmt_cache.values_mut() {\n            statement.reset_stmt_status(counter);\n        }\n    }\n\n    pub fn get_cursor(&mut self, cursor_id: CursorID) -> &mut Cursor {\n        self.cursors\n            .get_mut(cursor_id)\n            .unwrap_or_else(|| panic!(\"cursor id {cursor_id} out of bounds\"))\n            .as_mut()\n            .unwrap_or_else(|| panic!(\"cursor id {cursor_id} is None\"))\n    }\n\n    /// Close all virtual table cursors owned by this program.\n    ///\n    /// A virtual table cursor can own a nested helper statement on the same\n    /// connection (e.g. `PragmaVirtualTableCursor` runs `PRAGMA ...` via\n    /// `Connection::prepare_internal`), and that helper holds the\n    /// connection's nested-statement guard until it is dropped. Both\n    /// `commit_txn` and `abort` consult `Connection::is_nested_stmt()` to\n    /// decide whether the current statement owns top-level transaction\n    /// finalization, so the helpers must be dropped first — otherwise a root\n    /// statement that scanned a pragma virtual table misclassifies itself as\n    /// nested, skips ending its implicit read transaction, and subsequent\n    /// writes on the connection never auto-commit (issue #7466).\n    pub(crate) fn close_virtual_table_cursors(&mut self) {\n        for slot in self.cursors.iter_mut() {\n            if matches!(slot, Some(Cursor::Virtual(_))) {\n                *slot = None;","sourceCodeStart":1455,"sourceCodeEnd":1491,"githubUrl":"https://github.com/tursodatabase/turso/blob/492c4a71cd7c2649e7df83da1471b74f4b1c7aa9/core/vdbe/mod.rs#L1455-L1491","documentation":"ProgramState::get_cursor() found the slot but it holds None: the cursor was never opened (its OpenRead/VOpen/OpenPseudo has not executed) or was already closed by Insn::Close. Bytecode that consumes a cursor before opening it — often a jump that skips the open — trips this panic mid-execution.","triggerScenarios":"Control flow entering a loop body before the cursor open executed (bad jump target), an open emitted inside a conditional branch whose use site lies outside it, or Close followed by further use — usually after changes to jump/pc patching in translate/.","commonSituations":"Complex WHERE short-circuits, CASE expressions, and co-routine jumps; refactors of jump patching; upgrades.","solutions":["Report with SQL — every cursor use must be dominated by its open in the bytecode control-flow graph","Re-prepare after schema changes; do not reuse stale statements across DDL","Reduce jump-heavy constructs (short-circuit AND/OR chains, CASE) to isolate the bad jump","Upgrade"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// straight-line dominance check: a cursor id is only usable after its open insn\nfn opened_before(program: &Program, pc: usize, id: CursorID) -> bool {\n    program.insns[..pc].iter().any(|i| {\n        matches!(\n            i,\n            Insn::OpenRead { cursor_id, .. } | Insn::VOpen { cursor_id }\n                if *cursor_id == id\n        )\n    })\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Verify in debug builds that each cursor-consuming insn is dominated by its open","Test jump-heavy queries (CASE, short-circuit AND/OR) in CI","Boundary catch_unwind so a bad jump becomes a query error"],"tags":["vdbe","state","cursor","lifetime","panic","runtime"],"backgroundTag":"cursor-not-open","analyzedSha":"492c4a71cd7c2649e7df83da1471b74f4b1c7aa9","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}