tursodatabase/turso · critical

has_record=true but record() returned None

Error message

has_record=true but record() returned None

What it means

BTreeCursor::save_position() for index btrees (core/storage/btree.rs): after has_record() returned true, the code clones the current key with record() and expects Some. record() returning None violates the cursor contract that a valid cursor on an index leaf yields a record - e.g. the current cell cannot be parsed as an index payload.

Source

Thrown at core/storage/btree.rs:7629

            let page = self.stack.top_ref();
            let contents = page.get_contents();
            debug_assert!(
                matches!(contents.page_type(), Ok(PageType::TableLeaf)),
                "save_position: table cursor with has_record=true must be on a leaf"
            );
            let rowid = contents.cell_table_leaf_read_rowid(cell_idx as usize)?;
            self.save_context(CursorContext {
                key: CursorContextKey::TableRowId(rowid),
                seek_op: SeekOp::GE { eq_only: true },
            });
            return Ok(IOResult::Done(SavePositionResult::Saved));
        }
        // Index btree: yield IO for overflow chains. Allocate to the actual
        // payload size so wide-key indexes don't keep a page-sized buffer
        // per saved cursor.
        let cloned = {
            let record = return_if_io!(self.record());
            let record = record.expect("has_record=true but record() returned None");
            let payload = record.get_payload();
            let mut owned = crate::with_btree_allocation_site!(
                SavedCursorRecord,
                ImmutableRecord::new(payload.len())
            )?;
            crate::with_btree_allocation_site!(
                SavedCursorRecord,
                owned.start_serialization(payload)
            )?;
            owned
        };
        self.save_context(CursorContext {
            key: CursorContextKey::IndexKeyRowId(ImmutableRecordRef::from_owned_record(cloned)),
            seek_op: SeekOp::GE { eq_only: true },
        });
        Ok(IOResult::Done(SavePositionResult::Saved))
    }

View on GitHub (pinned to 492c4a71cd)

Solutions

  1. Report to Turso with the index definition and failing statement
  2. Run PRAGMA integrity_check; unparsable index cells are a corruption signal
  3. Rebuild the affected index (DROP INDEX + CREATE INDEX) if corruption of that index is confirmed
  4. Retry on a fresh connection and updated engine build
Defensive patterns

Strategy: try-catch

Validate before calling

// Before heavy UPDATE/DELETE on an indexed table, verify the index is well-formed:
conn.execute("PRAGMA integrity_check", ())?; // reports index cell corruption
// Rebuild suspect indexes before running the workload:
conn.execute("REINDEX", ())?;

Try / catch

let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
    conn.execute("UPDATE t SET indexed_col = ? WHERE ...", [v])
}));
if result.is_err() { conn.close().ok(); /* reopen, REINDEX, retry */ }

Prevention

When it happens

Trigger: Saving an index cursor position (UPDATE of an indexed column, DELETE using index lookups, statement yield mid-scan) when the cursor's current cell fails to produce a record - cursor effectively parked on a non-record position (interior page or unparsable cell) while valid_state/has_record still claim a live row.

Common situations: Wide index keys with overflow chains, engine regressions in record() parsing for index cells, databases where corruption makes a cell unreadable.

Related errors


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