tursodatabase/turso · error
OpenRead on virtual table cursor, use Insn:VOpen instead
Error message
OpenRead on virtual table cursor, use Insn:VOpen instead
What it means
Insn::OpenRead executed on a CursorType::VirtualTable cursor. Virtual tables are opened with Insn::VOpen, which calls the module's open callback to create a module cursor; they have no root page for a B-tree OpenRead. If translation emits OpenRead for a vtab cursor, the virtual table was not detected in that code path and the program is malformed.
Source
Thrown at core/vdbe/execute.rs:1457
});
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.into_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,
insn: &Insn,
_pager: &Arc<Pager>,
) -> InsnResult {
load_insn!(VOpen { cursor_id }, insn);
let (_, cursor_type) = program
.cursor_ref
.get(*cursor_id)
.expect("cursor_id should exist in cursor_ref");
let CursorType::VirtualTable(virtual_table) = cursor_type else {View on GitHub (pinned to 492c4a71cd)
Solutions
- Report with the virtual table module and full SQL — translation must emit VOpen/VColumn/VUpdate for vtab cursors
- Validate the statement against sqlite3 to confirm it is legal, then reduce it (drop RETURNING / ON CONFLICT / trigger parts) to isolate the path missing vtab handling
- Stage access through a plain SELECT from the vtab into a temp table before doing complex operations
- Upgrade
Example fix
// contributor fix in translate/: open vtab cursors with VOpen, not OpenRead
if table.is_virtual() {
program.emit_insn(Insn::VOpen { cursor_id });
} else {
program.emit_insn(Insn::OpenRead { cursor_id, root_page, .. });
} Defensive patterns
Strategy: validation
Validate before calling
// OpenRead must never target a virtual table cursor
for insn in &program.insns {
if let Insn::OpenRead { cursor_id, .. } = insn {
if matches!(program.cursor_ref.get(*cursor_id).map(|(_, t)| t), Some(CursorType::VirtualTable(_))) {
crate::bail_parse_error!("OpenRead on virtual table cursor; VOpen required");
}
}
} Prevention
- Maintain conformance coverage for every vtab extension under SELECT/INSERT/UPDATE/DELETE plus RETURNING
- Audit emitted programs for OpenRead targeting VirtualTable cursors in debug builds
- Validate new statements against sqlite3 before enabling them in production paths
When it happens
Trigger: Statements over virtual tables (csv, regexp, fts, pragma-backed vtabs like pragma_table_info, dbstat/dbpage) where a code path — upsert, RETURNING, trigger body, UPDATE ... FROM — fell back to generic table-cursor emission and missed the is_virtual check.
Common situations: Using bundled extensions and catalog-style vtabs in complex DML; engine versions where virtual-table detection in translate/ changed; shadow tables accessed directly.
Related errors
- VOpen on non-virtual table cursor
- VUpdate on non-virtual table cursor
- OpenRead on pseudo cursor
- Rewind on non-btree/materialized-view cursor
- unexpected cursor type
AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20).
Data as JSON: /api/errors/bad167c052756a9e.
Report an issue: GitHub.