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

  1. Report the SQL — the loop's advance opcode must match the cursor kind
  2. Test removing ORDER BY / GROUP BY / DISTINCT and vtab references to isolate which cursor is mis-targeted
  3. Restructure so sorting happens in an outer query level
  4. 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

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


AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20). Data as JSON: /api/errors/92c904bc92e55d41. Report an issue: GitHub.