tursodatabase/turso · error

VUpdate on non-virtual table cursor

Error message

VUpdate on non-virtual table cursor

What it means

Insn::VUpdate executed on a cursor that is not CursorType::VirtualTable. VUpdate routes INSERT/UPDATE/DELETE rows into the virtual table module's xUpdate callback; emitting it against a B-tree (or sorter/pseudo) cursor means the translator marked the write target as virtual while the cursor was allocated as something else — a malformed program, panicked inside op_vupdate.

Source

Thrown at core/vdbe/execute.rs:1618

) -> InsnResult {
    #[cfg(not(feature = "cli_only"))]
    let _ = pager;
    load_insn!(
        VUpdate {
            cursor_id,
            arg_count,
            start_reg,
            conflict_action,
            ..
        },
        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!("VUpdate on non-virtual table cursor");
    };
    let allow_dbpage_write = {
        #[cfg(feature = "cli_only")]
        {
            virtual_table.name == crate::dbpage::DBPAGE_TABLE_NAME
                && program.connection.db.opts.unsafe_testing
        }
        #[cfg(not(feature = "cli_only"))]
        {
            false
        }
    };
    if virtual_table.readonly() && !allow_dbpage_write {
        return Err(LimboError::ReadOnly.into());
    }

    if unlikely(*arg_count < 2) {
        return Err(LimboError::InternalError(

View on GitHub (pinned to 492c4a71cd)

Solutions

  1. Report with the module name and the DML statement
  2. Split the statement so only one plain vtab write happens per statement (remove ON CONFLICT / RETURNING / trigger combinations)
  3. Stage rows through a temp table, then issue a simple single-target INSERT into the vtab
  4. Upgrade

Example fix

-- before: upsert semantics on a virtual table
INSERT INTO vtab(k, v) VALUES (1, 'a') ON CONFLICT(k) DO UPDATE SET v = 'b';

-- after: plain insert for modules without upsert support
INSERT INTO vtab(k, v) VALUES (1, 'a');
Defensive patterns

Strategy: validation

Validate before calling

// VUpdate must only target virtual table cursors
for insn in &program.insns {
    if let Insn::VUpdate { cursor_id, .. } = insn {
        if !matches!(program.cursor_ref.get(*cursor_id).map(|(_, t)| t), Some(CursorType::VirtualTable(_))) {
            crate::bail_parse_error!("VUpdate on non-virtual cursor {cursor_id}");
        }
    }
}

Prevention

When it happens

Trigger: INSERT/UPDATE/DELETE on a virtual table through code paths that combine vtab writes with ordinary table machinery — upsert clauses, ON CONFLICT, REPLACE conflict resolution, triggers, or INSERT ... SELECT where the source is another vtab.

Common situations: Writing to csv-style, pg_catalog-style, or third-party modules; engine upgrades changing virtual-table write translation.

Related errors


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