tursodatabase/turso · error
Next on non-btree/materialized-view cursor
Error message
Next on non-btree/materialized-view cursor
What it means
Insn::Next (advance a scan cursor to the following row) executed on a cursor outside the handled set — the match covers BTree, MaterializedView, and IndexMethod; sorter, pseudo, and virtual table cursors are not (sorters advance via SorterNext, vtabs via VNext). The program advanced a cursor with the wrong opcode — a codegen inconsistency caught in op_next.
Source
Thrown at core/vdbe/execute.rs:3382
let is_null_row = btree_cursor.get_null_flag();
btree_cursor.set_null_flag(false);
if is_null_row {
true // is_empty = true
} else {
return_if_io!(btree_cursor.next());
btree_cursor.is_empty()
}
}
Cursor::MaterializedView(mv_cursor) => {
let has_more = return_if_io!(mv_cursor.next());
!has_more
}
Cursor::IndexMethod(_) => {
let cursor = cursor.as_index_method_mut();
let has_more = return_if_io!(cursor.query_next());
!has_more
}
_ => panic!("Next on non-btree/materialized-view cursor"),
}
};
if !is_empty {
// Increment metrics for row read
state.record_rows_read(1);
state.metrics.btree_next = state.metrics.btree_next.saturating_add(1);
state.metrics.search_count = state.metrics.search_count.saturating_add(1);
// Only steps codegen marked as part of a full table scan count as
// fullscan steps, matching SQLITE_STMTSTATUS_FULLSCAN_STEP.
if *fullscan {
state.metrics.fullscan_steps = state.metrics.fullscan_steps.saturating_add(1);
}
if let Some((_, cursor_type)) = program.cursor_ref.get(*cursor_id) {
if cursor_type.is_index() {
state.metrics.index_steps = state.metrics.index_steps.saturating_add(1);
}
}
state.pc = pc_if_next.as_offset_int();View on GitHub (pinned to 6c72522679)
Solutions
- Report the SQL — the loop's advance opcode must match the cursor kind
- Test removing ORDER BY / GROUP BY / DISTINCT and vtab references to isolate which cursor is mis-targeted
- Restructure so sorting happens in an outer query level
- Upgrade
Defensive patterns
Strategy: validation
Validate before calling
// Next must only advance cursors that support it
for insn in &program.insns {
if let Insn::Next { cursor_id, .. } = insn {
match program.cursor_ref.get(*cursor_id).map(|(_, t)| t) {
Some(CursorType::BTreeTable(_)) | Some(CursorType::BTreeIndex(_))
| Some(CursorType::MaterializedView(..)) | Some(CursorType::IndexMethod(..)) => {}
other => crate::bail_parse_error!("Next on unsupported cursor ({other:?})"),
}
}
} Prevention
- Cover ORDER BY/GROUP BY/DISTINCT with joins and subqueries in CI
- Debug-build audits matching loop opcodes to cursor kinds
- Boundary catch_unwind for engine panics
When it happens
Trigger: Loop emission picking the generic Next for a cursor allocated as a sorter (ORDER BY/GROUP BY pipeline), pseudo, or virtual table — e.g. after flattening rules changed or DISTINCT handling was reworked, so the loop opcode no longer matches the cursor kind.
Common situations: ORDER BY / GROUP BY / DISTINCT queries mixed with joins and subqueries; engine upgrades altering loop emission.
Related errors
- OpenRead on pseudo cursor
- Rewind on non-btree/materialized-view cursor
- unexpected cursor type
- {} on unexpected cursor
- OpenRead on sorter cursor
AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20).
Data as JSON: /api/errors/92c904bc92e55d41.
Report an issue: GitHub.