{"record":{"id":"a1ad9c324fcbec08","repo":"tursodatabase/turso","slug":"ancestor-page-should-be-on-the-stack","errorCode":null,"errorMessage":"ancestor page should be on the stack","messagePattern":"ancestor page should be on the stack","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"core/storage/btree.rs","lineNumber":7141,"sourceCode":"                DeleteState::CheckNeedsBalancing { btree_depth, .. } => {\n                    let page = self.stack.top_ref();\n                    // 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.\n                    // 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.\n\n                    let leaf_underflows = {\n                        let leaf_contents = page.get_contents();\n                        let free_space = compute_free_space(leaf_contents, usable_space)?;\n                        free_space * 3 > usable_space * 2\n                    };\n\n                    let interior_overflows_or_underflows = {\n                        // Invariant: ancestor pages on the stack are pinned to the page cache,\n                        // so we don't need return_if_locked_maybe_load! any ancestor,\n                        // and we already loaded the current page above.\n                        let interior_page = self\n                            .stack\n                            .get_page_at_level(*btree_depth)\n                            .expect(\"ancestor page should be on the stack\");\n                        let interior_contents = interior_page.get_contents();\n                        let overflows = !interior_contents.overflow_cells.is_empty();\n                        if overflows {\n                            true\n                        } else {\n                            let free_space = compute_free_space(interior_contents, usable_space)?;\n                            free_space * 3 > usable_space * 2\n                        }\n                    };\n\n                    let needs_balancing = leaf_underflows || interior_overflows_or_underflows;\n\n                    let CursorState::Delete(DeleteState::CheckNeedsBalancing {\n                        btree_depth,\n                        ref mut post_balancing_seek_key,\n                        interior_node_was_replaced,\n                        ..\n                    }) = self.state","sourceCodeStart":7123,"sourceCodeEnd":7159,"githubUrl":"https://github.com/tursodatabase/turso/blob/492c4a71cd7c2649e7df83da1471b74f4b1c7aa9/core/storage/btree.rs#L7123-L7159","documentation":"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.","triggerScenarios":"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.","commonSituations":"Long-running random deletes leaving sparse pages that underflow, deep trees, engine regressions in the delete/balance state machine interleaving.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"let result = std::panic::catch_unwind(AssertUnwindSafe(|| {\n    conn.execute(\"DELETE FROM sparse_table WHERE k < ?\", [threshold])\n}));\nif result.is_err() { conn.close().ok(); /* reopen; consider VACUUM before retry */ }","preventionTips":["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"],"tags":["btree","balancing","delete","stack-invariant","panic"],"backgroundTag":"invalid-cursor-state","analyzedSha":"492c4a71cd7c2649e7df83da1471b74f4b1c7aa9","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}