tursodatabase/turso · error

VOpen on non-virtual table cursor

Error message

VOpen on non-virtual table cursor

What it means

Insn::VOpen executed on a cursor whose cursor_ref type is not CursorType::VirtualTable. VOpen instantiates a module cursor via virtual_table.open(connection), so any other cursor kind means the translator emitted a virtual-table opcode against a regular (or pseudo/sorter) cursor slot — a malformed program, panicked at the start of op_vopen.

Source

Thrown at core/vdbe/execute.rs:1476

        }
    }
    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 {
        panic!("VOpen on non-virtual table cursor");
    };
    let cursor = virtual_table.open(program.connection.clone())?;
    state
        .cursors
        .get_mut(*cursor_id)
        .unwrap_or_else(|| panic!("cursor id {} out of bounds", *cursor_id))
        .replace(Cursor::Virtual(cursor));
    state.pc += 1;
    Ok(InsnFunctionStepResult::Step)
}

pub fn op_vcreate(
    program: &Program,
    state: &mut ProgramState,
    insn: &Insn,
    _pager: &Arc<Pager>,
) -> InsnResult {
    load_insn!(

View on GitHub (pinned to 492c4a71cd)

Solutions

  1. Report with schema plus SQL — VOpen must only target cursors allocated as VirtualTable
  2. Check for name collisions between your virtual modules and real tables/views and rename to disambiguate
  3. Simplify the statement (remove triggers/RETURNING clauses) to bypass the shared-cursor path
  4. Upgrade
Defensive patterns

Strategy: type-guard

Validate before calling

// before stepping a program that opens vtab cursors, verify their kinds
for insn in &program.insns {
    if let Insn::VOpen { cursor_id } = insn {
        let Some((_, CursorType::VirtualTable(_))) = program.cursor_ref.get(*cursor_id) else {
            crate::bail_parse_error!("VOpen on non-virtual cursor {cursor_id}");
        };
    }
}

Type guard

fn is_virtual_cursor(program: &Program, id: CursorID) -> bool {
    matches!(
        program.cursor_ref.get(id).map(|(_, t)| t),
        Some(CursorType::VirtualTable(_))
    )
}

Prevention

When it happens

Trigger: Translating statements over virtual tables where the cursor was allocated as a B-tree cursor — for example a name resolving to both a real table and a vtab in different schema contexts, or vtab detection missed in a subplan (trigger, RETURNING, upsert) that shares cursor ids with the vtab path.

Common situations: Extensions registering vtabs whose names collide with user tables or views; schema changes between prepare and execute; complex DML over vtabs.

Related errors


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