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
- Report to Turso with the index definition and failing statement
- Run PRAGMA integrity_check; unparsable index cells are a corruption signal
- Rebuild the affected index (DROP INDEX + CREATE INDEX) if corruption of that index is confirmed
- 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
- REINDEX after any suspected corruption or abnormal shutdown before index-heavy writes
- Prefer narrow index keys; very wide keys exercise overflow chains during cursor save
- Run integrity checks on databases received from external sources
- Report cursor-save panics with the index DDL upstream
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
- invalid cell payload
- parent page should be on the stack
- there should be a pointer
- ancestor page should be on the stack
- post_balancing_seek_key should be Some
AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20).
Data as JSON: /api/errors/a91330506eee59f6.
Report an issue: GitHub.