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.state

View on GitHub (pinned to 492c4a71cd)

Solutions

  1. Report to Turso with a reproduction - ancestor pinning broke, which is an engine bug
  2. Run VACUUM to rebuild and rebalance the btree, then retry the workload
  3. Retry the delete in a fresh transaction
  4. 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

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


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