tursodatabase/turso · error

Cursor is not an index: {cursor_id}

Error message

Cursor is not an index: {cursor_id}

What it means

resolve_index_for_cursor_id() found the cursor slot but its CursorType is not BTreeIndex, so there is no Index to return and it panics. The generated bytecode assumed an index cursor where a table (or pseudo/sorter/virtual) cursor was actually opened. It is a codegen inconsistency between the kind of cursor allocated and the instructions emitted against it.

Source

Thrown at core/vdbe/builder.rs:1889

    pub fn resolve_any_index_cursor_id_for_table_safe(
        &self,
        table_ref_id: TableInternalId,
    ) -> Option<CursorID> {
        self.cursor_ref.iter().position(|(k, _)| {
            k.as_ref()
                .is_some_and(|k| k.table_reference_id == table_ref_id && k.index.is_some())
        })
    }

    /// Resolve the [Index] that a given cursor is associated with.
    pub fn resolve_index_for_cursor_id(&self, cursor_id: CursorID) -> Arc<Index> {
        let cursor_ref = &self
            .cursor_ref
            .get(cursor_id)
            .unwrap_or_else(|| panic!("Cursor not found: {cursor_id}"))
            .1;
        let CursorType::BTreeIndex(index) = cursor_ref else {
            panic!("Cursor is not an index: {cursor_id}");
        };
        index.clone()
    }

    /// Get the [CursorType] of a given cursor.
    pub fn get_cursor_type(&self, cursor_id: CursorID) -> Option<&CursorType> {
        self.cursor_ref
            .get(cursor_id)
            .map(|(_, cursor_type)| cursor_type)
    }

    pub const fn set_collation(&mut self, c: Option<(CollationSeq, bool)>) {
        self.collation = c
    }

    pub const fn curr_collation_ctx(&self) -> Option<(CollationSeq, bool)> {
        self.collation
    }

View on GitHub (pinned to 492c4a71cd)

Solutions

  1. Report with the SQL and EXPLAIN output — the opcode/cursor-kind pair is inconsistent
  2. Compare bytecode against sqlite3 with scripts/diff.sh or EXPLAIN to see which cursor kind the reference engine uses
  3. Change the access path (drop/disable the index, or NOT INDEXED) so only table cursors are involved
  4. Upgrade

Example fix

// before: assumes the resolved cursor is always an index cursor
let idx = program_builder.resolve_index_for_cursor_id(cid);

// after: check the cursor kind and handle the table-cursor case
match program_builder.get_cursor_type(cid) {
    Some(CursorType::BTreeIndex(idx)) => idx.clone(),
    Some(CursorType::BTreeTable(_)) => { /* fall back to table-cursor path */ }
    other => crate::bail_parse_error!("cursor {cid} is not table or index ({other:?})"),
}
Defensive patterns

Strategy: type-guard

Validate before calling

if !matches!(builder.get_cursor_type(cid), Some(CursorType::BTreeIndex(_))) {
    crate::bail_parse_error!("cursor {cid} must be an index cursor for this instruction");
}

Type guard

fn is_index_cursor(b: &ProgramBuilder, id: CursorID) -> bool {
    matches!(b.get_cursor_type(id), Some(CursorType::BTreeIndex(_)))
}

Prevention

When it happens

Trigger: Emitting index-based opcodes whose cursor was opened as BTreeTable — commonly when one table reference gets both cursor kinds (the correlated-subquery limitation documented on resolve_any_index_cursor_id_for_table) and translation picks the wrong slot, or after a plan flips from index scan to full scan without updating the resolution.

Common situations: Mixed table+index cursor allocation for a single table reference; planner/optimizer changes flipping access paths; development on core/translate/ or the optimizer.

Related errors


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