{"record":{"id":"946e988af08134ea","repo":"tursodatabase/turso","slug":"vupdate-on-non-virtual-table-cursor","errorCode":null,"errorMessage":"VUpdate on non-virtual table cursor","messagePattern":"VUpdate on non-virtual table cursor","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/vdbe/execute.rs","lineNumber":1618,"sourceCode":") -> InsnResult {\n    #[cfg(not(feature = \"cli_only\"))]\n    let _ = pager;\n    load_insn!(\n        VUpdate {\n            cursor_id,\n            arg_count,\n            start_reg,\n            conflict_action,\n            ..\n        },\n        insn\n    );\n    let (_, cursor_type) = program\n        .cursor_ref\n        .get(*cursor_id)\n        .expect(\"cursor_id should exist in cursor_ref\");\n    let CursorType::VirtualTable(virtual_table) = cursor_type else {\n        panic!(\"VUpdate on non-virtual table cursor\");\n    };\n    let allow_dbpage_write = {\n        #[cfg(feature = \"cli_only\")]\n        {\n            virtual_table.name == crate::dbpage::DBPAGE_TABLE_NAME\n                && program.connection.db.opts.unsafe_testing\n        }\n        #[cfg(not(feature = \"cli_only\"))]\n        {\n            false\n        }\n    };\n    if virtual_table.readonly() && !allow_dbpage_write {\n        return Err(LimboError::ReadOnly.into());\n    }\n\n    if unlikely(*arg_count < 2) {\n        return Err(LimboError::InternalError(","sourceCodeStart":1600,"sourceCodeEnd":1636,"githubUrl":"https://github.com/tursodatabase/turso/blob/492c4a71cd7c2649e7df83da1471b74f4b1c7aa9/core/vdbe/execute.rs#L1600-L1636","documentation":"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.","triggerScenarios":"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.","commonSituations":"Writing to csv-style, pg_catalog-style, or third-party modules; engine upgrades changing virtual-table write translation.","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"],"exampleFix":"-- before: upsert semantics on a virtual table\nINSERT INTO vtab(k, v) VALUES (1, 'a') ON CONFLICT(k) DO UPDATE SET v = 'b';\n\n-- after: plain insert for modules without upsert support\nINSERT INTO vtab(k, v) VALUES (1, 'a');","handlingStrategy":"validation","validationCode":"// VUpdate must only target virtual table cursors\nfor insn in &program.insns {\n    if let Insn::VUpdate { cursor_id, .. } = insn {\n        if !matches!(program.cursor_ref.get(*cursor_id).map(|(_, t)| t), Some(CursorType::VirtualTable(_))) {\n            crate::bail_parse_error!(\"VUpdate on non-virtual cursor {cursor_id}\");\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["vdbe","executor","virtual-table","dml","cursor-type","panic","runtime"],"backgroundTag":"virtual-table-instruction-mismatch","analyzedSha":"492c4a71cd7c2649e7df83da1471b74f4b1c7aa9","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}