tursodatabase/turso · critical
ancestor page should be on the stack
Error message
ancestor page should be on the stack
What it means
Post-delete balancing check (core/storage/btree.rs): after cell removal the code reads the ancestor interior page from the cursor stack via stack.get_page_at_level(btree_depth) to test overflow/underflow (free_space * 3 > usable_space * 2). The expect fires when no ancestor page sits at that stack level, violating the documented invariant that ancestors are pinned on the stack.
Source
Thrown at core/storage/btree.rs:7141
DeleteState::CheckNeedsBalancing { btree_depth, .. } => {
let page = self.stack.top_ref();
// Check if either the leaf page we took the replacement cell from underflows, or if the interior page we inserted it into overflows OR underflows.
// If the latter is true, we must always balance that level regardless of whether the leaf page (or any ancestor pages in between) need balancing.
let leaf_underflows = {
let leaf_contents = page.get_contents();
let free_space = compute_free_space(leaf_contents, usable_space)?;
free_space * 3 > usable_space * 2
};
let interior_overflows_or_underflows = {
// Invariant: ancestor pages on the stack are pinned to the page cache,
// so we don't need return_if_locked_maybe_load! any ancestor,
// and we already loaded the current page above.
let interior_page = self
.stack
.get_page_at_level(*btree_depth)
.expect("ancestor page should be on the stack");
let interior_contents = interior_page.get_contents();
let overflows = !interior_contents.overflow_cells.is_empty();
if overflows {
true
} else {
let free_space = compute_free_space(interior_contents, usable_space)?;
free_space * 3 > usable_space * 2
}
};
let needs_balancing = leaf_underflows || interior_overflows_or_underflows;
let CursorState::Delete(DeleteState::CheckNeedsBalancing {
btree_depth,
ref mut post_balancing_seek_key,
interior_node_was_replaced,
..
}) = self.stateView on GitHub (pinned to 492c4a71cd)
Solutions
- Report to Turso with a reproduction - ancestor pinning broke, which is an engine bug
- Run VACUUM to rebuild and rebalance the btree, then retry the workload
- Retry the delete in a fresh transaction
- Run PRAGMA integrity_check to verify tree structure afterwards
Defensive patterns
Strategy: try-catch
Try / catch
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
conn.execute("DELETE FROM sparse_table WHERE k < ?", [threshold])
}));
if result.is_err() { conn.close().ok(); /* reopen; consider VACUUM before retry */ } Prevention
- VACUUM heavily-deleted databases to rebalance btrees before more deletes
- Batch large deletes so pages underflow progressively rather than all at once
- Run integrity checks when delete patterns change (e.g. purging oldest keys)
- Report reproducible stack-invariant panics upstream
When it happens
Trigger: Any DELETE after which the leaf or interior page under/overflows; the stack depth changed between recording btree_depth and the check - e.g. the balance_only_ancestor loop popped the stack below btree_depth, or balancing moved pages between the steps.
Common situations: Long-running random deletes leaving sparse pages that underflow, deep trees, engine regressions in the delete/balance state machine interleaving.
Related errors
- parent page should be on the stack
- post_balancing_seek_key should be Some
- invalid cell payload
- there should be a pointer
- has_record=true but record() returned None
AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20).
Data as JSON: /api/errors/a1ad9c324fcbec08.
Report an issue: GitHub.