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
- Report with SQL — the engine must guarantee cursor slots cover every reference in the program
- Re-prepare statements after schema/DDL changes instead of reusing cached ones
- Simplify triggers/co-routines in the failing statement to change cursor layout
- 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
- Re-prepare statements after DDL changes
- Assert cursor accounting (cursor_ref length vs state slots) in debug builds
- Catch panics per-statement at the embedding boundary
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
- cursor id {cursor_id} is None
- cursor id {} out of bounds
- Cursor not found: {cursor_id}
- OpenRead on pseudo cursor
- Rewind on non-btree/materialized-view cursor
AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20).
Data as JSON: /api/errors/1b44974cc3ad2a52.
Report an issue: GitHub.