tursodatabase/turso · error

Cursor not found: {key:?}

Error message

Cursor not found: {key:?}

What it means

During bytecode building, resolve_cursor_id maps a CursorKey (table/index reference) to the CursorID of an earlier OpenRead/OpenIndex. If the key was never opened, it panics - the program references a cursor that translation never allocated. A sibling helper, resolve_cursor_id_safe, returns Option for cases where absence is legal.

Source

Thrown at core/vdbe/builder.rs:1857

    // translate [CursorKey] to cursor id
    pub fn resolve_cursor_id_safe(&self, key: &CursorKey) -> Option<CursorID> {
        // Check cursor overrides first, only apply override for table cursors.
        // Index cursor lookups are not overridden because when a cursor override is active,
        // the calling code (translate_expr) should skip index logic entirely.
        if key.index.is_none() && !key.is_build {
            let table_id: usize = key.table_reference_id.into();
            if let Some(&cursor_id) = self.cursor_overrides.get(&table_id) {
                return Some(cursor_id);
            }
        }
        self.cursor_ref
            .iter()
            .position(|(k, _)| k.as_ref().is_some_and(|k| k.equals(key)))
    }

    pub fn resolve_cursor_id(&self, key: &CursorKey) -> CursorID {
        self.resolve_cursor_id_safe(key)
            .unwrap_or_else(|| panic!("Cursor not found: {key:?}"))
    }

    /// Resolve the first allocated index cursor for a given table reference.
    /// This method exists due to a limitation of our translation system where
    /// a subquery that references an outer query table cannot know whether a
    /// table cursor, index cursor, or both were opened for that table reference.
    /// Hence: currently we first try to resolve a table cursor, and if that fails,
    /// we resolve an index cursor via this method.
    pub fn resolve_any_index_cursor_id_for_table(&self, table_ref_id: TableInternalId) -> CursorID {
        self.resolve_any_index_cursor_id_for_table_safe(table_ref_id)
            .unwrap_or_else(|| panic!("No index cursor found for table {table_ref_id}"))
    }

    pub fn resolve_any_index_cursor_id_for_table_safe(
        &self,
        table_ref_id: TableInternalId,
    ) -> Option<CursorID> {
        self.cursor_ref.iter().position(|(k, _)| {

View on GitHub (pinned to 492c4a71cd)

Solutions

  1. Emit/register the cursor (OpenRead/OpenIndex or cursor_overrides) before resolving its ID
  2. Use resolve_cursor_id_safe where absence is expected and handle the None branch
  3. Add an EXPLAIN-based regression test for the plan shape

Example fix

// before
let id = builder.resolve_cursor_id(&key); // panics: cursor never opened

// after
builder.open_table_cursor(key.clone());   // emit OpenRead first
let id = builder.resolve_cursor_id(&key);
Defensive patterns

Strategy: validation

Validate before calling

// prefer the safe variant when the cursor may legitimately be absent
match builder.resolve_cursor_id_safe(&key) {
    Some(id) => id,
    None => {
        // emit OpenRead/OpenIndex for `key` first, then resolve again
        builder.open_table_cursor(key.clone());
        builder.resolve_cursor_id(&key)
    }
}

Prevention

When it happens

Trigger: Internal: new planner or translation shapes that use a table/index cursor before (or without) emitting the corresponding open instruction; subqueries referencing outer-query cursors resolved against the wrong program; cursor_overrides wiring regressions.

Common situations: Contributor PRs to the optimizer/translator; correlated-subquery changes; refactors of cursor registration order.

Related errors


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