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
- Capture the full panic message — the {insn_name} prefix identifies the mismatched opcode, include it in the bug report along with the SQL
- Isolate the DML from ORDER BY / DISTINCT / vtab references to find which cursor is mis-targeted
- Rewrite the DML without the combining construct (e.g. separate trigger bodies, plain DELETE)
- 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
- Capture the insn name from the panic message — it pinpoints the mismatched opcode for the bug report
- Cover DML-inside-triggers and vtab mixes in regression tests
- Debug-build audits for btree-only opcodes vs cursor kinds
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
- OpenRead on pseudo cursor
- Rewind on non-btree/materialized-view cursor
- unexpected cursor type
- Next on non-btree/materialized-view cursor
- OpenRead on sorter cursor
AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20).
Data as JSON: /api/errors/01b08ee0ffcc4ec7.
Report an issue: GitHub.