tursodatabase/turso · error

cursor id {cursor_id} is None

Error message

cursor id {cursor_id} is None

What it means

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.

Source

Thrown at core/vdbe/mod.rs:1473

            crate::statement::StatementStatusCounter::VmStep => self.metrics.insn_executed = 0,
            crate::statement::StatementStatusCounter::Reprepare => self.metrics.reprepares = 0,
            crate::statement::StatementStatusCounter::RowsRead => self.metrics.rows_read = 0,
            crate::statement::StatementStatusCounter::RowsWritten => self.metrics.rows_written = 0,
        }
        if let Some(OpProgramState::Step { statement, .. }) = self.active_op_state.program_mut() {
            statement.reset_stmt_status(counter);
        }
        for statement in self.subprogram_stmt_cache.values_mut() {
            statement.reset_stmt_status(counter);
        }
    }

    pub fn get_cursor(&mut self, cursor_id: CursorID) -> &mut Cursor {
        self.cursors
            .get_mut(cursor_id)
            .unwrap_or_else(|| panic!("cursor id {cursor_id} out of bounds"))
            .as_mut()
            .unwrap_or_else(|| panic!("cursor id {cursor_id} is None"))
    }

    /// Close all virtual table cursors owned by this program.
    ///
    /// A virtual table cursor can own a nested helper statement on the same
    /// connection (e.g. `PragmaVirtualTableCursor` runs `PRAGMA ...` via
    /// `Connection::prepare_internal`), and that helper holds the
    /// connection's nested-statement guard until it is dropped. Both
    /// `commit_txn` and `abort` consult `Connection::is_nested_stmt()` to
    /// decide whether the current statement owns top-level transaction
    /// finalization, so the helpers must be dropped first — otherwise a root
    /// statement that scanned a pragma virtual table misclassifies itself as
    /// nested, skips ending its implicit read transaction, and subsequent
    /// writes on the connection never auto-commit (issue #7466).
    pub(crate) fn close_virtual_table_cursors(&mut self) {
        for slot in self.cursors.iter_mut() {
            if matches!(slot, Some(Cursor::Virtual(_))) {
                *slot = None;

View on GitHub (pinned to 492c4a71cd)

Solutions

  1. Report with SQL — every cursor use must be dominated by its open in the bytecode control-flow graph
  2. Re-prepare after schema changes; do not reuse stale statements across DDL
  3. Reduce jump-heavy constructs (short-circuit AND/OR chains, CASE) to isolate the bad jump
  4. Upgrade
Defensive patterns

Strategy: validation

Validate before calling

// straight-line dominance check: a cursor id is only usable after its open insn
fn opened_before(program: &Program, pc: usize, id: CursorID) -> bool {
    program.insns[..pc].iter().any(|i| {
        matches!(
            i,
            Insn::OpenRead { cursor_id, .. } | Insn::VOpen { cursor_id }
                if *cursor_id == id
        )
    })
}

Prevention

When it happens

Trigger: 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/.

Common situations: Complex WHERE short-circuits, CASE expressions, and co-routine jumps; refactors of jump patching; upgrades.

Related errors


AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20). Data as JSON: /api/errors/96bc2615b44eb0c0. Report an issue: GitHub.