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
- Report with the module name and the DML statement
- Split the statement so only one plain vtab write happens per statement (remove ON CONFLICT / RETURNING / trigger combinations)
- Stage rows through a temp table, then issue a simple single-target INSERT into the vtab
- 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
- Keep one plain vtab write per statement; avoid ON CONFLICT/RETURNING on modules until covered by tests
- Exercise vtab DML paths in CI for every module you ship
- Catch panics per-statement at the API boundary
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
- OpenRead on virtual table cursor, use Insn:VOpen instead
- VOpen 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/946e988af08134ea.
Report an issue: GitHub.