tursodatabase/turso · critical
parent page should be on the stack
Error message
parent page should be on the stack
What it means
Raised in the DELETE state machine's InteriorNodeReplacement step (core/storage/btree.rs). When the deleted cell is a separator in an interior page, the cursor walks to the predecessor leaf (get_prev_record) and then restores the parent's cell index via stack.node_states.get_mut(btree_depth). The expect fires when the cursor stack has no entry at the interior page's recorded depth.
Source
Thrown at core/storage/btree.rs:7035
return_if_io!(self.get_prev_record());
let CursorState::Delete(DeleteState::InteriorNodeReplacement {
ref page,
btree_depth,
cell_idx,
original_child_pointer,
ref mut post_balancing_seek_key,
..
}) = self.state
else {
unreachable!("expected interior node replacement state");
};
// Ensure we keep the parent page at the same position as before the replacement.
self.stack
.node_states
.get_mut(btree_depth)
.expect("parent page should be on the stack")
.cell_idx = cell_idx as i32;
let (cell_payload, leaf_cell_idx) = {
let leaf_page = self.stack.top_ref();
let leaf_contents = leaf_page.get_contents();
turso_assert!(leaf_contents.is_leaf());
turso_assert_greater_than!(leaf_contents.cell_count(), 0);
let leaf_cell_idx = leaf_contents.cell_count() - 1;
let last_cell_on_child_page =
leaf_contents.cell_get(leaf_cell_idx, usable_space)?;
let mut cell_payload: crate::alloc::Vec<u8> = crate::alloc::vec![];
let child_pointer =
original_child_pointer.expect("there should be a pointer");
// Rewrite the old leaf cell as an interior cell depending on type.
match last_cell_on_child_page {
BTreeCell::TableLeafCell(leaf_cell) => {
// Table interior cells contain the left child pointer and the rowid as varint.
crate::with_btree_allocation_site!(View on GitHub (pinned to 492c4a71cd)
Solutions
- Report to Turso with a reproducing statement - a stack-depth invariant break is not caller-fixable
- Run PRAGMA integrity_check; corrupted interior pages can break depth bookkeeping
- Retry the delete in a new transaction with a freshly prepared statement
- As a workaround, delete the row by exact key from a new statement instead of reusing a cursor that has seeked/balanced heavily
Defensive patterns
Strategy: try-catch
Try / catch
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
stmt.execute(params![key])
}));
match result {
Ok(res) => res?,
Err(_) => { conn.close().ok(); /* reopen and retry once from disk state */ } Prevention
- Use freshly prepared statements for destructive operations instead of long-lived heavily-seeked cursors
- Run PRAGMA integrity_check periodically to catch tree damage before deletes trip invariants
- Exercise delete-heavy workloads (including smallest-key deletes) in staging before engine upgrades
- Report reproducible cases upstream - these panics mark engine bugs
When it happens
Trigger: DELETE of a row whose key is referenced by an interior (separator) cell, forcing replacement with the predecessor key. The descent to the leaf must keep the interior page pinned at btree_depth; the panic means the stack is shallower than that depth when the parent's cell_idx is restored.
Common situations: Deleting the smallest/oldest keys of a table or index (frequently separators), deep multi-level btrees, deletes interleaved with balancing that changed tree depth, engine regressions in cursor movement during delete.
Related errors
- ancestor page should be on the stack
- invalid cell payload
- there should be a pointer
- post_balancing_seek_key should be Some
- Cursor not found: {key:?}
AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20).
Data as JSON: /api/errors/a9e992f97ac52b4f.
Report an issue: GitHub.