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

  1. Report with the virtual table module and full SQL — translation must emit VOpen/VColumn/VUpdate for vtab cursors
  2. 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
  3. Stage access through a plain SELECT from the vtab into a temp table before doing complex operations
  4. 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

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


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