tursodatabase/turso · error
OpenRead on pseudo cursor
Error message
OpenRead on pseudo cursor
What it means
At execution time Insn::OpenRead tried to open a cursor slot whose cursor_ref type is CursorType::Pseudo. Pseudo cursors hold one materialized in-memory row (used for VALUES rows and co-routine output) and have no B-tree to open, so OpenRead cannot apply. The bytecode reused a pseudo cursor id as if it were a B-tree cursor — a translation bug, hit inside op_open_read while stepping the program.
Source
Thrown at core/vdbe/execute.rs:1379
pager,
maybe_transform_root_page_to_positive(mv_store.as_ref(), *root_page),
index.as_ref(),
num_columns,
)?);
let index_info = Arc::new(if let Some(mv_store) = mv_store.as_ref() {
IndexInfo::new_from_index_in(index, mv_store.allocator())?
} else {
IndexInfo::new_from_index(index)?
});
let cursor =
maybe_promote_to_mvcc_cursor(btree_cursor, MvccCursorType::Index(index_info))?;
cursors
.get_mut(*cursor_id)
.expect("cursor_id should be valid")
.replace(Cursor::new_btree(cursor));
}
CursorType::Pseudo(_) => {
panic!("OpenRead on pseudo cursor");
}
CursorType::Sorter => {
panic!("OpenRead on sorter cursor");
}
CursorType::IndexMethod(..) => {
unreachable!("IndexMethod handled above")
}
CursorType::VirtualTable(_) => {
panic!("OpenRead on virtual table cursor, use Insn:VOpen instead");
}
}
state.pc += 1;
Ok(InsnFunctionStepResult::Step)
}
pub fn op_vopen(
program: &Program,
state: &mut ProgramState,View on GitHub (pinned to 6c72522679)
Solutions
- Report the SQL — OpenRead may only target BTreeTable/BTreeIndex/MaterializedView cursors; the translator allocated the wrong kind
- Rewrite the VALUES/co-routine construct into a real temp table or plain SELECT to avoid pseudo cursors
- Upgrade to a version with the cursor-allocation fix
Defensive patterns
Strategy: validation
Validate before calling
// audit a compiled program before first execution: OpenRead must target btree-backed cursors
fn audit_open_reads(program: &Program) -> Result<()> {
for insn in &program.insns {
if let Insn::OpenRead { cursor_id, .. } = insn {
match program.cursor_ref.get(*cursor_id).map(|(_, t)| t) {
Some(CursorType::BTreeTable(_)) | Some(CursorType::BTreeIndex(_))
| Some(CursorType::MaterializedView(..)) => {}
other => crate::bail_parse_error!("OpenRead targets non-btree cursor ({other:?})"),
}
}
}
Ok(())
} Prevention
- Run a bytecode audit pass in debug builds that type-checks every cursor-consuming insn against cursor_ref
- Wrap statement execution in catch_unwind at your API boundary so engine panics become per-query errors, not process crashes
- Keep regression tests mixing VALUES/CTE materialization with table scans
When it happens
Trigger: A plan that materializes rows into a pseudo cursor and later emits OpenRead for the same cursor id — typically after cursor renumbering/reallocation changes, or when a co-routine or VALUES-clause pseudo cursor id leaks into an outer open-table sequence.
Common situations: Queries combining VALUES clauses, co-routine subqueries, or CTE materialization with ordinary table scans; engine versions where cursor allocation was refactored.
Related errors
- Rewind on non-btree/materialized-view cursor
- unexpected cursor type
- Next on non-btree/materialized-view cursor
- {} on unexpected cursor
- OpenRead on sorter cursor
AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20).
Data as JSON: /api/errors/74795af3d86c562d.
Report an issue: GitHub.