tursodatabase/turso · error

{} on unexpected cursor

Error message

{} on unexpected cursor

What it means

The must_be_btree_cursor! macro guards opcodes that require a B-tree-backed cursor (BTreeTable, BTreeIndex, or MaterializedView). The panic fires when such an opcode — its name is embedded in the message — runs against a sorter, pseudo, or virtual table cursor. The translator emitted a B-tree-only operation for a cursor of another kind; the insn name in the panic tells you exactly which opcode mismatched.

Source

Thrown at core/vdbe/mod.rs:1765

            }
            _ => panic!("register holds unexpected value: {self:?}"),
        }
    }
}

#[macro_export]
macro_rules! must_be_btree_cursor {
    ($cursor_id:expr, $cursor_ref:expr, $state:expr, $insn_name:expr) => {{
        let (_, cursor_type) = $cursor_ref.get($cursor_id).unwrap();
        if matches!(
            cursor_type,
            CursorType::BTreeTable(_)
                | CursorType::BTreeIndex(_)
                | CursorType::MaterializedView(_, _)
        ) {
            $crate::get_cursor!($state, $cursor_id)
        } else {
            panic!("{} on unexpected cursor", $insn_name)
        }
    }};
}

/// Macro is necessary to help the borrow checker see we are only accessing state.cursor field
/// and nothing else
#[macro_export]
macro_rules! get_cursor {
    ($state:expr, $cursor_id:expr) => {
        $state
            .cursors
            .get_mut($cursor_id)
            .unwrap_or_else(|| panic!("cursor id {} out of bounds", $cursor_id))
            .as_mut()
            .unwrap_or_else(|| panic!("cursor id {} is None", $cursor_id))
    };
}

View on GitHub (pinned to 492c4a71cd)

Solutions

  1. Capture the full panic message — the {insn_name} prefix identifies the mismatched opcode, include it in the bug report along with the SQL
  2. Isolate the DML from ORDER BY / DISTINCT / vtab references to find which cursor is mis-targeted
  3. Rewrite the DML without the combining construct (e.g. separate trigger bodies, plain DELETE)
  4. Upgrade
Defensive patterns

Strategy: validation

Validate before calling

// opcodes guarded by must_be_btree_cursor! must target btree-backed cursors
for insn in &program.insns {
    if let Some((id, name)) = insn.requires_btree_cursor() {
        if !matches!(
            program.cursor_ref.get(id).map(|(_, t)| t),
            Some(CursorType::BTreeTable(_)) | Some(CursorType::BTreeIndex(_))
                | Some(CursorType::MaterializedView(..))
        ) {
            crate::bail_parse_error!("{name} requires a btree cursor");
        }
    }
}

Type guard

fn btree_backed(t: &CursorType) -> bool {
    matches!(
        t,
        CursorType::BTreeTable(_) | CursorType::BTreeIndex(_) | CursorType::MaterializedView(..)
    )
}

Prevention

When it happens

Trigger: Row-mutating or seeking opcodes (insert/delete/seek families) resolved to cursor ids allocated as sorter, pseudo, or vtab cursors — typically through cursor reuse across subquery flattening, trigger bodies, or upsert paths.

Common situations: DML inside triggers over vtabs; DELETE/UPDATE statements whose cursors interact with ORDER BY pipelines; upgrades changing cursor allocation.

Related errors


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