tursodatabase/turso · error

cursor id {cursor_id} out of bounds

Error message

cursor id {cursor_id} out of bounds

What it means

ProgramState::get_cursor() panicked because cursor_id does not index into the per-execution cursors vector. The instruction stream referenced a cursor slot the state never allocated — a program/state sizing mismatch or bytecode using an id that was never opened. It is the runtime out-of-bounds form of a bad cursor reference.

Source

Thrown at core/vdbe/mod.rs:1471

            }
            crate::statement::StatementStatusCounter::Sort => self.metrics.sort_operations = 0,
            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() {

View on GitHub (pinned to 492c4a71cd)

Solutions

  1. Report with SQL — the engine must guarantee cursor slots cover every reference in the program
  2. Re-prepare statements after schema/DDL changes instead of reusing cached ones
  3. Simplify triggers/co-routines in the failing statement to change cursor layout
  4. Upgrade
Defensive patterns

Strategy: validation

Validate before calling

// debug audit: every cursor id referenced by an insn must be within the state's cursor array
fn audit_cursor_bounds(program: &Program, state: &ProgramState) -> Result<()> {
    let len = state.cursors.len();
    for id in program.referenced_cursor_ids() {
        if id >= len {
            crate::bail_parse_error!("cursor {id} out of bounds (state has {len} slots)");
        }
    }
    Ok(())
}

Prevention

When it happens

Trigger: Any opcode calling state.get_cursor(id) with id >= cursors.len() — after cursor renumbering, subprograms adding cursors after ProgramState creation, or a jump into code whose Open cursor emission was skipped.

Common situations: Trigger bodies and co-routines; cached prepared statements reused after schema changes; refactors of cursor allocation in vdbe/translate.

Related errors


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