{"record":{"id":"01b08ee0ffcc4ec7","repo":"tursodatabase/turso","slug":"on-unexpected-cursor","errorCode":null,"errorMessage":"{} on unexpected cursor","messagePattern":"(.+?) on unexpected cursor","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/vdbe/mod.rs","lineNumber":1765,"sourceCode":"            }\n            _ => panic!(\"register holds unexpected value: {self:?}\"),\n        }\n    }\n}\n\n#[macro_export]\nmacro_rules! must_be_btree_cursor {\n    ($cursor_id:expr, $cursor_ref:expr, $state:expr, $insn_name:expr) => {{\n        let (_, cursor_type) = $cursor_ref.get($cursor_id).unwrap();\n        if matches!(\n            cursor_type,\n            CursorType::BTreeTable(_)\n                | CursorType::BTreeIndex(_)\n                | CursorType::MaterializedView(_, _)\n        ) {\n            $crate::get_cursor!($state, $cursor_id)\n        } else {\n            panic!(\"{} on unexpected cursor\", $insn_name)\n        }\n    }};\n}\n\n/// Macro is necessary to help the borrow checker see we are only accessing state.cursor field\n/// and nothing else\n#[macro_export]\nmacro_rules! get_cursor {\n    ($state:expr, $cursor_id:expr) => {\n        $state\n            .cursors\n            .get_mut($cursor_id)\n            .unwrap_or_else(|| panic!(\"cursor id {} out of bounds\", $cursor_id))\n            .as_mut()\n            .unwrap_or_else(|| panic!(\"cursor id {} is None\", $cursor_id))\n    };\n}\n","sourceCodeStart":1747,"sourceCodeEnd":1783,"githubUrl":"https://github.com/tursodatabase/turso/blob/492c4a71cd7c2649e7df83da1471b74f4b1c7aa9/core/vdbe/mod.rs#L1747-L1783","documentation":"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.","triggerScenarios":"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.","commonSituations":"DML inside triggers over vtabs; DELETE/UPDATE statements whose cursors interact with ORDER BY pipelines; upgrades changing cursor allocation.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// opcodes guarded by must_be_btree_cursor! must target btree-backed cursors\nfor insn in &program.insns {\n    if let Some((id, name)) = insn.requires_btree_cursor() {\n        if !matches!(\n            program.cursor_ref.get(id).map(|(_, t)| t),\n            Some(CursorType::BTreeTable(_)) | Some(CursorType::BTreeIndex(_))\n                | Some(CursorType::MaterializedView(..))\n        ) {\n            crate::bail_parse_error!(\"{name} requires a btree cursor\");\n        }\n    }\n}","typeGuard":"fn btree_backed(t: &CursorType) -> bool {\n    matches!(\n        t,\n        CursorType::BTreeTable(_) | CursorType::BTreeIndex(_) | CursorType::MaterializedView(..)\n    )\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["vdbe","executor","cursor-type","macro","panic","runtime"],"backgroundTag":"cursor-type-mismatch","analyzedSha":"492c4a71cd7c2649e7df83da1471b74f4b1c7aa9","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}