Skip to main content

reddb_server/storage/engine/
btree.rs

1//! B+ Tree Implementation for Page-Based Storage
2//!
3//! A B+ tree optimized for disk I/O with the following properties:
4//! - All values stored in leaf nodes
5//! - Interior nodes only contain keys and child pointers
6//! - Leaf nodes form a doubly-linked list for range scans
7//! - Each node fits in a single 16 KiB page
8//!
9//! # Page Layout
10//!
11//! Interior Node:
12//! ```text
13//! ┌────────────────────────────────────────────────────────────┐
14//! │ PageHeader (32 bytes)                                      │
15//! ├────────────────────────────────────────────────────────────┤
16//! │ right_child: u32 - Rightmost child pointer                 │
17//! │ cells: [key_len: u16, key: [u8], child: u32]...           │
18//! └────────────────────────────────────────────────────────────┘
19//! ```
20//!
21//! Leaf Node:
22//! ```text
23//! ┌────────────────────────────────────────────────────────────┐
24//! │ PageHeader (32 bytes)                                      │
25//! ├────────────────────────────────────────────────────────────┤
26//! │ prev_leaf: u32 - Previous leaf (0 = none)                  │
27//! │ next_leaf: u32 - Next leaf (0 = none)                      │
28//! │ cells: [key_len: u16, val_len: u16, key: [u8], val: [u8]] │
29//! └────────────────────────────────────────────────────────────┘
30//! ```
31
32use std::cmp::Ordering;
33use std::sync::{Arc, RwLock};
34
35use super::page::{Page, PageType, HEADER_SIZE, PAGE_SIZE};
36use super::pager::{Pager, PagerError};
37
38/// Maximum key size (to ensure at least 2 keys per node)
39pub const MAX_KEY_SIZE: usize = 1024;
40
41/// Hard upper bound on logical value size.
42///
43/// Per ADR 0023 (large-value overflow), values up to 256 MiB are stored
44/// transparently — inline below [`value_layout::OVERFLOW_THRESHOLD`],
45/// inline+compressed when LZ4 shrinks them under the same threshold, or
46/// spilled into an [`overflow::OverflowChain`] otherwise. Values above
47/// this ceiling are rejected with [`BTreeError::ValueTooLarge`] before
48/// any compression or page allocation runs.
49///
50/// Re-exported from [`value_layout::MAX_VALUE_SIZE`] so callers that
51/// historically read this constant (`unified::store::impl_pages`,
52/// `query::planner::stats_catalog`) continue to compile.
53pub const MAX_VALUE_SIZE: usize = value_layout::MAX_VALUE_SIZE;
54
55/// Minimum fill factor before merge (as percentage)
56pub const MIN_FILL_FACTOR: usize = 25;
57
58/// Offset of prev_leaf in leaf page
59const LEAF_PREV_OFFSET: usize = HEADER_SIZE;
60
61/// Offset of next_leaf in leaf page
62const LEAF_NEXT_OFFSET: usize = HEADER_SIZE + 4;
63
64/// Start of the slot array in leaf pages.
65///
66/// Slotted-page layout:
67/// ```text
68/// [PageHeader 32B][prev u32 | next u32 | 8B]
69/// [slot 0: u16][slot 1: u16]...[slot N-1: u16]  ← grows forward
70/// ... free space ...
71/// [cell N-1][cell N-2]...[cell 0]               ← grows backward from free_end
72/// ```
73/// Each cell is laid out as `[key_len:u16][val_len:u16][key][val]`.
74/// `page.cell_count()` is N; `page.free_end()` is the offset of the
75/// lowest (most recently written) cell. The slot array lives right
76/// after the leaf-chain links and each u16 slot is the absolute page
77/// offset of its cell.
78const LEAF_SLOT_ARRAY_OFFSET: usize = HEADER_SIZE + 8;
79
80/// Kept for source-compat with older code paths (e.g. interior-node
81/// helpers); equivalent to `LEAF_SLOT_ARRAY_OFFSET` for leaves.
82const LEAF_DATA_OFFSET: usize = LEAF_SLOT_ARRAY_OFFSET;
83
84/// Start of cell data in interior pages (right_child is in header)
85const INTERIOR_DATA_OFFSET: usize = HEADER_SIZE;
86
87/// B+ Tree error types
88#[derive(Debug, Clone)]
89pub enum BTreeError {
90    /// Key not found
91    NotFound,
92    /// Key already exists
93    DuplicateKey,
94    /// Key too large
95    KeyTooLarge(usize),
96    /// Value too large
97    ValueTooLarge(usize),
98    /// Tree is corrupted
99    Corrupted(String),
100    /// Pager error
101    Pager(String),
102    /// Internal lock was poisoned by a panicked thread
103    LockPoisoned(String),
104    /// Large-value layout encode/decode failed (overflow chain or LZ4).
105    ValueLayout(String),
106}
107
108impl std::fmt::Display for BTreeError {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        match self {
111            Self::NotFound => write!(f, "Key not found"),
112            Self::DuplicateKey => write!(f, "Key already exists"),
113            Self::KeyTooLarge(size) => {
114                write!(f, "Key too large: {} bytes (max {})", size, MAX_KEY_SIZE)
115            }
116            Self::ValueTooLarge(size) => write!(
117                f,
118                "Value too large: {} bytes (max {})",
119                size, MAX_VALUE_SIZE
120            ),
121            Self::Corrupted(msg) => write!(f, "B-tree corrupted: {}", msg),
122            Self::Pager(msg) => write!(f, "Pager error: {}", msg),
123            Self::LockPoisoned(msg) => write!(f, "Lock poisoned: {}", msg),
124            Self::ValueLayout(msg) => write!(f, "Value layout: {}", msg),
125        }
126    }
127}
128
129impl From<value_layout::ValueLayoutError> for BTreeError {
130    fn from(e: value_layout::ValueLayoutError) -> Self {
131        match e {
132            value_layout::ValueLayoutError::ValueTooLarge(n) => Self::ValueTooLarge(n),
133            other => Self::ValueLayout(other.to_string()),
134        }
135    }
136}
137
138impl std::error::Error for BTreeError {}
139
140impl From<PagerError> for BTreeError {
141    fn from(e: PagerError) -> Self {
142        Self::Pager(e.to_string())
143    }
144}
145
146impl From<super::page::PageError> for BTreeError {
147    fn from(e: super::page::PageError) -> Self {
148        Self::Corrupted(e.to_string())
149    }
150}
151
152/// Result type for B+ tree operations
153pub type BTreeResult<T> = Result<T, BTreeError>;
154
155/// B+ Tree cursor for iteration
156pub struct BTreeCursor {
157    /// A cursor can outlive the tree handle that created it.
158    _retirement_pin: Arc<()>,
159    /// Current leaf page ID
160    leaf_page_id: u32,
161    /// Current position within leaf
162    position: usize,
163    /// Pager reference
164    pager: Arc<Pager>,
165    /// A6 — true once we've issued a prefetch hint for the next leaf on
166    /// this leaf. Reset to false every time we move to a new leaf so the
167    /// hint fires once per leaf, at the halfway point.
168    prefetched_next: bool,
169}
170
171impl BTreeCursor {
172    /// Move to next entry
173    pub fn next(&mut self) -> BTreeResult<Option<(Vec<u8>, Vec<u8>)>> {
174        if self.leaf_page_id == 0 {
175            return Ok(None);
176        }
177
178        let page = self.pager.read_page(self.leaf_page_id)?;
179        if page.page_type()? != PageType::BTreeLeaf {
180            return Err(BTreeError::Corrupted(
181                "cursor reached a non-leaf page".into(),
182            ));
183        }
184        let cell_count = page.cell_count() as usize;
185
186        // A6 — prefetch the next leaf when we cross the halfway point of
187        // the current one. The hint fires at most once per leaf (guarded
188        // by `prefetched_next`) so the syscall overhead is amortized over
189        // ~half a leaf's worth of entries. The kernel uses the hint to
190        // start DMA-ing the next page while we finish the current one.
191        if !self.prefetched_next && self.position >= cell_count / 2 && cell_count > 0 {
192            let next_leaf = read_next_leaf(&page);
193            if next_leaf != 0 {
194                self.pager.prefetch_hint(next_leaf);
195            }
196            self.prefetched_next = true;
197        }
198
199        // Check if we have more cells in current page
200        if self.position < cell_count {
201            let (key, stored) = read_leaf_cell(&page, self.position)?;
202            let value = value_layout::decode(&self.pager, &stored)?;
203            self.position += 1;
204            return Ok(Some((key, value)));
205        }
206
207        // Move to next leaf
208        let next_leaf = read_next_leaf(&page);
209        if next_leaf == 0 {
210            self.leaf_page_id = 0;
211            return Ok(None);
212        }
213        let previous_high_key = leaf_high_key(&page)?;
214
215        self.leaf_page_id = next_leaf;
216        self.position = 0;
217        self.prefetched_next = false; // reset for the new leaf
218
219        // Read from new leaf
220        let page = self.pager.read_page(self.leaf_page_id)?;
221        if page.page_type()? != PageType::BTreeLeaf {
222            return Err(BTreeError::Corrupted(
223                "leaf chain reached a non-leaf page".into(),
224            ));
225        }
226        if page.cell_count() == 0 {
227            return Ok(None);
228        }
229
230        let (key, stored) = read_leaf_cell(&page, 0)?;
231        // Every leaf transition must advance the key range. This bounds a
232        // corrupt cyclic chain without growing a visited-page set or an
233        // unbounded checkpoint buffer (#2376).
234        if previous_high_key
235            .as_ref()
236            .is_some_and(|previous| key <= *previous)
237        {
238            self.leaf_page_id = 0;
239            return Err(BTreeError::Corrupted(
240                "leaf chain does not advance its key range".into(),
241            ));
242        }
243        let value = value_layout::decode(&self.pager, &stored)?;
244        self.position = 1;
245        Ok(Some((key, value)))
246    }
247
248    /// Peek at current entry without advancing
249    pub fn peek(&self) -> BTreeResult<Option<(Vec<u8>, Vec<u8>)>> {
250        if self.leaf_page_id == 0 {
251            return Ok(None);
252        }
253
254        let page = self.pager.read_page(self.leaf_page_id)?;
255        let cell_count = page.cell_count() as usize;
256
257        if self.position >= cell_count {
258            return Ok(None);
259        }
260
261        let (key, stored) = read_leaf_cell(&page, self.position)?;
262        let value = value_layout::decode(&self.pager, &stored)?;
263        Ok(Some((key, value)))
264    }
265}
266
267/// B+ Tree implementation
268pub struct BTree {
269    retirement_pin: Arc<()>,
270    /// Pager for page I/O
271    pager: Arc<Pager>,
272    /// Root page ID (0 = empty tree)
273    root_page_id: RwLock<u32>,
274    /// D2 fastpath: cached rightmost leaf (page_id, high_key).
275    ///
276    /// When the next key to insert is strictly greater than `high_key`,
277    /// skip `find_leaf` (tree descent from root) and go directly to the
278    /// cached page. Invalidated on splits and deletes.
279    ///
280    /// Mirrors `BTPageCacheIsValid` / `BTREE_FASTPATH_MIN_LEVEL` in
281    /// `nbtinsert.c:31`.
282    rightmost_leaf: RwLock<Option<(u32, Vec<u8>)>>,
283    /// Serialises structural writers (insert / upsert / delete / bulk).
284    ///
285    /// Writers copy a page, modify the copy and write it back, and there
286    /// are no page latches, so two writers on one tree would lose each
287    /// other's leaf updates or tear a split (#2376). Reentrant because the
288    /// public mutators call each other (`upsert` -> `insert`, the bulk
289    /// paths -> `insert_encoded`). Readers do not take it.
290    writer: parking_lot::ReentrantMutex<()>,
291}
292
293#[path = "btree/impl.rs"]
294mod btree_impl;
295
296#[path = "btree/reclaim.rs"]
297mod reclaim;
298
299#[path = "btree/lehman_yao.rs"]
300pub mod lehman_yao;
301
302#[path = "btree/value_layout.rs"]
303pub mod value_layout;
304// ==================== Search Helpers ====================
305
306enum SearchResult {
307    Found(usize),
308    NotFound(usize),
309}
310
311fn search_leaf(page: &Page, key: &[u8]) -> BTreeResult<SearchResult> {
312    let cell_count = page.cell_count() as usize;
313
314    // Binary search
315    let mut low = 0;
316    let mut high = cell_count;
317
318    while low < high {
319        let mid = (low + high) / 2;
320        let (cell_key, _) = read_leaf_cell(page, mid)?;
321
322        match cell_key.as_slice().cmp(key) {
323            Ordering::Less => low = mid + 1,
324            Ordering::Greater => high = mid,
325            Ordering::Equal => return Ok(SearchResult::Found(mid)),
326        }
327    }
328
329    Ok(SearchResult::NotFound(low))
330}
331
332fn find_child(page: &Page, key: &[u8]) -> BTreeResult<u32> {
333    let cell_count = page.cell_count() as usize;
334
335    // Binary search for the correct child
336    for i in 0..cell_count {
337        let (cell_key, child) = read_interior_cell(page, i)?;
338        if key < cell_key.as_slice() {
339            return Ok(child);
340        }
341    }
342
343    // Key is >= all keys, use right child
344    Ok(page.right_child())
345}
346
347fn find_first_child(page: &Page) -> BTreeResult<u32> {
348    if page.cell_count() == 0 {
349        return Ok(page.right_child());
350    }
351    let (_, child) = read_interior_cell(page, 0)?;
352    Ok(child)
353}
354
355fn leaf_min_bytes() -> usize {
356    (PAGE_SIZE - LEAF_DATA_OFFSET) * MIN_FILL_FACTOR / 100
357}
358
359fn interior_min_bytes() -> usize {
360    (PAGE_SIZE - INTERIOR_DATA_OFFSET) * MIN_FILL_FACTOR / 100
361}
362
363/// Bytes consumed by one leaf entry in the slotted layout: one u16
364/// slot in the pointer array plus the cell itself (`[key_len:u16]
365/// [val_len:u16][key][val]`).
366fn leaf_entry_size(entry: &(Vec<u8>, Vec<u8>)) -> usize {
367    2 + 4 + entry.0.len() + entry.1.len()
368}
369
370fn leaf_entries_size(entries: &[(Vec<u8>, Vec<u8>)]) -> usize {
371    entries.iter().map(leaf_entry_size).sum()
372}
373
374#[inline]
375fn leaf_slot_offset_for(index: usize) -> usize {
376    LEAF_SLOT_ARRAY_OFFSET + index * 2
377}
378
379#[inline]
380fn leaf_read_slot(page: &Page, index: usize) -> BTreeResult<usize> {
381    let data = page.as_bytes();
382    let slot_pos = leaf_slot_offset_for(index);
383    if slot_pos + 2 > PAGE_SIZE {
384        return Err(BTreeError::Corrupted("slot array overflows page".into()));
385    }
386    Ok(u16::from_le_bytes([data[slot_pos], data[slot_pos + 1]]) as usize)
387}
388
389#[inline]
390fn leaf_write_slot(page: &mut Page, index: usize, cell_offset: u16) -> BTreeResult<()> {
391    let data = page.as_bytes_mut();
392    let slot_pos = leaf_slot_offset_for(index);
393    if slot_pos + 2 > PAGE_SIZE {
394        return Err(BTreeError::Corrupted("slot array overflows page".into()));
395    }
396    data[slot_pos..slot_pos + 2].copy_from_slice(&cell_offset.to_le_bytes());
397    Ok(())
398}
399
400#[inline]
401fn leaf_slots_end(page: &Page) -> usize {
402    LEAF_SLOT_ARRAY_OFFSET + (page.cell_count() as usize) * 2
403}
404
405#[inline]
406fn leaf_cells_start(page: &Page) -> usize {
407    let end = page.free_end() as usize;
408    if end == 0 {
409        PAGE_SIZE
410    } else {
411        end
412    }
413}
414
415#[inline]
416fn leaf_free_bytes(page: &Page) -> usize {
417    let slot_end = leaf_slots_end(page);
418    let cells = leaf_cells_start(page);
419    cells.saturating_sub(slot_end)
420}
421
422fn interior_key_size(key: &[u8]) -> usize {
423    2 + key.len() + 4
424}
425
426fn interior_entries_size(keys: &[Vec<u8>]) -> usize {
427    keys.iter().map(|k| interior_key_size(k)).sum()
428}
429
430// ==================== Leaf Page Helpers ====================
431
432/// Read the cell at slot `index` in O(1). Follows the u16 slot pointer
433/// into the cell data area; the cell header is `[key_len:u16][val_len:u16]`
434/// followed by the raw key and value bytes.
435fn read_leaf_cell(page: &Page, index: usize) -> BTreeResult<(Vec<u8>, Vec<u8>)> {
436    let cell_count = page.cell_count() as usize;
437    if index >= cell_count {
438        return Err(BTreeError::Corrupted("Cell index out of range".into()));
439    }
440    let offset = leaf_read_slot(page, index)?;
441    let data = page.as_bytes();
442    if offset + 4 > PAGE_SIZE {
443        return Err(BTreeError::Corrupted("cell header out of range".into()));
444    }
445    let key_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
446    let val_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
447    let end = offset + 4 + key_len + val_len;
448    if end > PAGE_SIZE {
449        return Err(BTreeError::Corrupted("cell body out of range".into()));
450    }
451    let key = data[offset + 4..offset + 4 + key_len].to_vec();
452    let value = data[offset + 4 + key_len..end].to_vec();
453    Ok((key, value))
454}
455
456fn read_leaf_entries(page: &Page) -> BTreeResult<Vec<(Vec<u8>, Vec<u8>)>> {
457    let cell_count = page.cell_count() as usize;
458    let mut entries = Vec::with_capacity(cell_count);
459    for i in 0..cell_count {
460        entries.push(read_leaf_cell(page, i)?);
461    }
462    Ok(entries)
463}
464
465/// Wipe and re-lay an entire leaf in slotted form. Used by the split
466/// path (which hands us a pre-sorted `entries` vector) and by
467/// `clear_leaf_cells` under the hood. Entries are appended at the end
468/// of the free area so the highest-index slot ends up at the lowest
469/// offset — same shape the single-key insert path produces.
470fn write_leaf_entries(page: &mut Page, entries: &[(Vec<u8>, Vec<u8>)]) -> BTreeResult<()> {
471    clear_leaf_cells(page);
472    for (i, (k, v)) in entries.iter().enumerate() {
473        let cell_size = 4 + k.len() + v.len();
474        let cells_start = leaf_cells_start(page);
475        let slot_end = LEAF_SLOT_ARRAY_OFFSET + (i + 1) * 2;
476        if slot_end + cell_size > cells_start {
477            return Err(BTreeError::Corrupted("leaf rebuild overflowed page".into()));
478        }
479        let cell_offset = cells_start - cell_size;
480        {
481            let data = page.as_bytes_mut();
482            data[cell_offset..cell_offset + 2].copy_from_slice(&(k.len() as u16).to_le_bytes());
483            data[cell_offset + 2..cell_offset + 4].copy_from_slice(&(v.len() as u16).to_le_bytes());
484            data[cell_offset + 4..cell_offset + 4 + k.len()].copy_from_slice(k);
485            data[cell_offset + 4 + k.len()..cell_offset + 4 + k.len() + v.len()].copy_from_slice(v);
486        }
487        page.set_free_end(cell_offset as u16);
488        leaf_write_slot(page, i, cell_offset as u16)?;
489    }
490    page.set_cell_count(entries.len() as u16);
491    page.set_free_start((LEAF_SLOT_ARRAY_OFFSET + entries.len() * 2) as u16);
492    Ok(())
493}
494
495fn can_insert_leaf(page: &Page, key: &[u8], value: &[u8]) -> bool {
496    let needed = 2 + 4 + key.len() + value.len();
497    leaf_free_bytes(page) >= needed
498}
499
500/// Insert `(key, value)` into the slotted leaf in O(log M) search +
501/// O(M) slot-array memmove. Cell data is appended at the tail of the
502/// free area (backward from `free_end`); the slot pointer is inserted
503/// Overwrite the value portion of cell at `index` in-place. Valid when
504/// `new_value.len() <= existing_value.len()` — the caller
505/// (typically `BTree::upsert`) checks this before calling.
506///
507/// Same-length writes are trivial (byte copy). Shrinks update the cell's
508/// `v_len` header and leave the trailing bytes as dead space until the
509/// page is naturally rewritten; the slotted layout keeps cells
510/// addressable by their own offset+len so the gap is harmless.
511///
512/// Fixed-size column updates (SET score = X on a typed schema) hit this
513/// path, avoiding the delete + re-insert + rebalance cycle that
514/// dominates bulk_update.
515pub(crate) fn overwrite_leaf_value(
516    page: &mut Page,
517    index: usize,
518    new_value: &[u8],
519) -> BTreeResult<()> {
520    let cell_offset = leaf_read_slot(page, index)?;
521    let data = page.as_bytes();
522    if cell_offset + 4 > data.len() {
523        return Err(BTreeError::Corrupted(
524            "leaf cell header out of bounds".into(),
525        ));
526    }
527    let k_len = u16::from_le_bytes([data[cell_offset], data[cell_offset + 1]]) as usize;
528    let old_v_len = u16::from_le_bytes([data[cell_offset + 2], data[cell_offset + 3]]) as usize;
529    if new_value.len() > old_v_len {
530        return Err(BTreeError::Corrupted(
531            "overwrite_leaf_value: new value larger than slot".into(),
532        ));
533    }
534    let value_start = cell_offset + 4 + k_len;
535    if value_start + new_value.len() > data.len() {
536        return Err(BTreeError::Corrupted(
537            "leaf cell value out of bounds".into(),
538        ));
539    }
540    let data = page.as_bytes_mut();
541    // Update the v_len header if the value shrank. read_leaf_cell reads
542    // v_len from this header so the shortened value deserializes correctly.
543    if new_value.len() != old_v_len {
544        let new_len = new_value.len() as u16;
545        data[cell_offset + 2..cell_offset + 4].copy_from_slice(&new_len.to_le_bytes());
546    }
547    data[value_start..value_start + new_value.len()].copy_from_slice(new_value);
548    Ok(())
549}
550
551/// at the sorted position.
552fn insert_into_leaf(page: &mut Page, key: &[u8], value: &[u8]) -> BTreeResult<()> {
553    // 1. Binary search the slot array to find the insertion index.
554    let cell_count = page.cell_count() as usize;
555    let mut low = 0;
556    let mut high = cell_count;
557    while low < high {
558        let mid = (low + high) / 2;
559        let (cell_key, _) = read_leaf_cell(page, mid)?;
560        match cell_key.as_slice().cmp(key) {
561            Ordering::Less => low = mid + 1,
562            Ordering::Greater => high = mid,
563            Ordering::Equal => {
564                // Duplicate keys are tolerated by the B-tree (caller
565                // decides semantics); append after the existing run.
566                low = mid + 1;
567                break;
568            }
569        }
570    }
571    let insert_pos = low;
572
573    // 2. Reserve the cell at the tail of the free area.
574    let cell_size = 4 + key.len() + value.len();
575    let slot_end_after = LEAF_SLOT_ARRAY_OFFSET + (cell_count + 1) * 2;
576    let cells_start = leaf_cells_start(page);
577    if slot_end_after + cell_size > cells_start {
578        return Err(BTreeError::Corrupted("leaf page full".into()));
579    }
580    let cell_offset = cells_start - cell_size;
581    {
582        let data = page.as_bytes_mut();
583        data[cell_offset..cell_offset + 2].copy_from_slice(&(key.len() as u16).to_le_bytes());
584        data[cell_offset + 2..cell_offset + 4].copy_from_slice(&(value.len() as u16).to_le_bytes());
585        data[cell_offset + 4..cell_offset + 4 + key.len()].copy_from_slice(key);
586        data[cell_offset + 4 + key.len()..cell_offset + 4 + key.len() + value.len()]
587            .copy_from_slice(value);
588    }
589    page.set_free_end(cell_offset as u16);
590
591    // 3. Shift the slot-array tail right by one slot, then write the
592    //    new pointer into the freed slot. This is a single memmove on
593    //    a couple dozen u16s — far cheaper than the O(M²) rebuild.
594    {
595        let shift_from = leaf_slot_offset_for(insert_pos);
596        let shift_to = shift_from + 2;
597        let shift_len = (cell_count - insert_pos) * 2;
598        if shift_len > 0 {
599            let data = page.as_bytes_mut();
600            data.copy_within(shift_from..shift_from + shift_len, shift_to);
601        }
602    }
603    leaf_write_slot(page, insert_pos, cell_offset as u16)?;
604
605    // 4. Bump counters.
606    page.set_cell_count((cell_count + 1) as u16);
607    page.set_free_start((LEAF_SLOT_ARRAY_OFFSET + (cell_count + 1) * 2) as u16);
608    Ok(())
609}
610
611/// Remove the slot at `index`. Cell bytes are left in place (lazy
612/// compaction); the slot-array tail is shifted left to close the gap.
613/// The caller is expected to call `clear_leaf_cells` + rebuild if the
614/// page wants its free space reclaimed.
615fn delete_from_leaf(page: &mut Page, index: usize) -> BTreeResult<()> {
616    let cell_count = page.cell_count() as usize;
617    if index >= cell_count {
618        return Err(BTreeError::Corrupted("delete index out of range".into()));
619    }
620    if index + 1 < cell_count {
621        let shift_from = leaf_slot_offset_for(index + 1);
622        let shift_to = leaf_slot_offset_for(index);
623        let shift_len = (cell_count - index - 1) * 2;
624        let data = page.as_bytes_mut();
625        data.copy_within(shift_from..shift_from + shift_len, shift_to);
626    }
627    page.set_cell_count((cell_count - 1) as u16);
628    page.set_free_start((LEAF_SLOT_ARRAY_OFFSET + (cell_count - 1) * 2) as u16);
629    Ok(())
630}
631
632/// Reset the leaf to an empty slotted state — cell count 0, free
633/// cursors at the extremes. Zeroes the slot array + cell data area so
634/// stale bytes never leak through a corrupted read.
635fn clear_leaf_cells(page: &mut Page) {
636    {
637        let data = page.as_bytes_mut();
638        for byte in &mut data[LEAF_SLOT_ARRAY_OFFSET..] {
639            *byte = 0;
640        }
641    }
642    page.set_cell_count(0);
643    page.set_free_start(LEAF_SLOT_ARRAY_OFFSET as u16);
644    page.set_free_end(PAGE_SIZE as u16);
645}
646
647fn init_leaf_links(page: &mut Page, prev: u32, next: u32) {
648    let data = page.as_bytes_mut();
649    data[LEAF_PREV_OFFSET..LEAF_PREV_OFFSET + 4].copy_from_slice(&prev.to_le_bytes());
650    data[LEAF_NEXT_OFFSET..LEAF_NEXT_OFFSET + 4].copy_from_slice(&next.to_le_bytes());
651}
652
653fn read_next_leaf(page: &Page) -> u32 {
654    let data = page.as_bytes();
655    u32::from_le_bytes([
656        data[LEAF_NEXT_OFFSET],
657        data[LEAF_NEXT_OFFSET + 1],
658        data[LEAF_NEXT_OFFSET + 2],
659        data[LEAF_NEXT_OFFSET + 3],
660    ])
661}
662
663/// Returns the right-sibling page ID for a leaf page.
664/// 0 means this is the rightmost leaf — used by the D2 fastpath cache.
665#[inline]
666fn leaf_right_sibling(page: &Page) -> u32 {
667    read_next_leaf(page)
668}
669
670/// Returns the maximum (last) key stored in a leaf page, or `None` if empty.
671/// This is the Lehman-Yao "high_key" — any key > this value belongs on a
672/// right-sibling page. Reads only the last slot so it is O(1).
673fn leaf_high_key(page: &Page) -> BTreeResult<Option<Vec<u8>>> {
674    let cell_count = page.cell_count() as usize;
675    if cell_count == 0 {
676        return Ok(None);
677    }
678    let (key, _) = read_leaf_cell(page, cell_count - 1)?;
679    Ok(Some(key))
680}
681
682fn set_prev_leaf(page: &mut Page, prev: u32) {
683    let data = page.as_bytes_mut();
684    data[LEAF_PREV_OFFSET..LEAF_PREV_OFFSET + 4].copy_from_slice(&prev.to_le_bytes());
685}
686
687fn set_next_leaf(page: &mut Page, next: u32) {
688    let data = page.as_bytes_mut();
689    data[LEAF_NEXT_OFFSET..LEAF_NEXT_OFFSET + 4].copy_from_slice(&next.to_le_bytes());
690}
691
692// ==================== Interior Page Helpers ====================
693//
694// Slotted-page layout mirroring the leaf format:
695//
696// ```text
697// [PageHeader 32B]
698// [slot 0: u16][slot 1: u16]...[slot N-1: u16]  ← grows forward
699// ... free space ...
700// [cell N-1][cell N-2]...[cell 0]               ← grows backward from free_end
701// ```
702//
703// Each cell is `[key_len:u16][key][child:u32]`. The right-most child
704// pointer lives in `PageHeader.right_child` exactly as before.
705
706#[inline]
707fn interior_slot_offset_for(index: usize) -> usize {
708    INTERIOR_DATA_OFFSET + index * 2
709}
710
711#[inline]
712fn interior_read_slot(page: &Page, index: usize) -> BTreeResult<usize> {
713    let data = page.as_bytes();
714    let slot_pos = interior_slot_offset_for(index);
715    if slot_pos + 2 > PAGE_SIZE {
716        return Err(BTreeError::Corrupted(
717            "interior slot array overflows page".into(),
718        ));
719    }
720    Ok(u16::from_le_bytes([data[slot_pos], data[slot_pos + 1]]) as usize)
721}
722
723#[inline]
724fn interior_write_slot(page: &mut Page, index: usize, cell_offset: u16) -> BTreeResult<()> {
725    let data = page.as_bytes_mut();
726    let slot_pos = interior_slot_offset_for(index);
727    if slot_pos + 2 > PAGE_SIZE {
728        return Err(BTreeError::Corrupted(
729            "interior slot array overflows page".into(),
730        ));
731    }
732    data[slot_pos..slot_pos + 2].copy_from_slice(&cell_offset.to_le_bytes());
733    Ok(())
734}
735
736#[inline]
737fn interior_cells_start(page: &Page) -> usize {
738    let end = page.free_end() as usize;
739    if end == 0 {
740        PAGE_SIZE
741    } else {
742        end
743    }
744}
745
746#[inline]
747fn interior_free_bytes(page: &Page) -> usize {
748    let slot_end = INTERIOR_DATA_OFFSET + (page.cell_count() as usize) * 2;
749    interior_cells_start(page).saturating_sub(slot_end)
750}
751
752fn read_interior_cell(page: &Page, index: usize) -> BTreeResult<(Vec<u8>, u32)> {
753    let cell_count = page.cell_count() as usize;
754    if index >= cell_count {
755        return Err(BTreeError::Corrupted("Cell index out of range".into()));
756    }
757    let offset = interior_read_slot(page, index)?;
758    let data = page.as_bytes();
759    if offset + 2 > PAGE_SIZE {
760        return Err(BTreeError::Corrupted(
761            "interior cell header out of range".into(),
762        ));
763    }
764    let key_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
765    let end = offset + 2 + key_len + 4;
766    if end > PAGE_SIZE {
767        return Err(BTreeError::Corrupted(
768            "interior cell body out of range".into(),
769        ));
770    }
771    let key = data[offset + 2..offset + 2 + key_len].to_vec();
772    let child = u32::from_le_bytes([
773        data[offset + 2 + key_len],
774        data[offset + 2 + key_len + 1],
775        data[offset + 2 + key_len + 2],
776        data[offset + 2 + key_len + 3],
777    ]);
778    Ok((key, child))
779}
780
781fn read_interior_keys_children(page: &Page) -> BTreeResult<(Vec<Vec<u8>>, Vec<u32>)> {
782    let cell_count = page.cell_count() as usize;
783    let mut keys = Vec::with_capacity(cell_count);
784    let mut children = Vec::with_capacity(cell_count + 1);
785
786    for i in 0..cell_count {
787        let (key, child) = read_interior_cell(page, i)?;
788        keys.push(key);
789        children.push(child);
790    }
791
792    if cell_count == 0 {
793        let right_child = page.right_child();
794        if right_child != 0 {
795            children.push(right_child);
796        }
797    } else {
798        children.push(page.right_child());
799    }
800
801    Ok((keys, children))
802}
803
804/// Bulk-write an interior page from scratch in slotted form. Used by
805/// the split path. `keys` and `children` follow the B+ tree contract:
806/// `children.len() == keys.len() + 1`, with the last child landing in
807/// `PageHeader.right_child` (as before).
808fn write_interior_entries(page: &mut Page, keys: &[Vec<u8>], children: &[u32]) -> BTreeResult<()> {
809    if !keys.is_empty() && children.len() != keys.len() + 1 {
810        return Err(BTreeError::Corrupted(
811            "Interior keys/children length mismatch".into(),
812        ));
813    }
814
815    clear_interior_cells(page);
816    if keys.is_empty() {
817        let right_child = children.first().copied().unwrap_or(0);
818        page.set_right_child(right_child);
819        return Ok(());
820    }
821
822    for (i, key) in keys.iter().enumerate() {
823        let cell_size = 2 + key.len() + 4;
824        let cells_start = interior_cells_start(page);
825        let slot_end = INTERIOR_DATA_OFFSET + (i + 1) * 2;
826        if slot_end + cell_size > cells_start {
827            return Err(BTreeError::Corrupted(
828                "interior rebuild overflowed page".into(),
829            ));
830        }
831        let cell_offset = cells_start - cell_size;
832        {
833            let data = page.as_bytes_mut();
834            data[cell_offset..cell_offset + 2].copy_from_slice(&(key.len() as u16).to_le_bytes());
835            data[cell_offset + 2..cell_offset + 2 + key.len()].copy_from_slice(key);
836            data[cell_offset + 2 + key.len()..cell_offset + 2 + key.len() + 4]
837                .copy_from_slice(&children[i].to_le_bytes());
838        }
839        page.set_free_end(cell_offset as u16);
840        interior_write_slot(page, i, cell_offset as u16)?;
841    }
842    page.set_cell_count(keys.len() as u16);
843    page.set_free_start((INTERIOR_DATA_OFFSET + keys.len() * 2) as u16);
844    page.set_right_child(*children.last().ok_or_else(|| {
845        BTreeError::Corrupted("write_interior_entries: children empty with non-empty keys".into())
846    })?);
847    Ok(())
848}
849
850fn can_insert_interior(page: &Page, key: &[u8]) -> bool {
851    let needed = 2 + 2 + key.len() + 4;
852    interior_free_bytes(page) >= needed
853}
854
855/// Insert a `(key, left_child)` separator into the interior page.
856/// `right_child` replaces whatever child used to sit to the right of
857/// the inserted key (either a middle child's pointer or the page's
858/// `right_child` when the key is the new maximum).
859fn insert_into_interior(
860    page: &mut Page,
861    key: &[u8],
862    left_child: u32,
863    right_child: u32,
864) -> BTreeResult<()> {
865    // 1. Binary search the slot array for the insert position.
866    let cell_count = page.cell_count() as usize;
867    let mut low = 0;
868    let mut high = cell_count;
869    while low < high {
870        let mid = (low + high) / 2;
871        let (cell_key, _) = read_interior_cell(page, mid)?;
872        match cell_key.as_slice().cmp(key) {
873            Ordering::Less => low = mid + 1,
874            Ordering::Greater | Ordering::Equal => high = mid,
875        }
876    }
877    let insert_pos = low;
878
879    // 2. Redirect the previous owner of the split to `right_child`.
880    //    If the insertion is at the tail, the previous owner was the
881    //    page's right_child pointer; otherwise it was the child slot
882    //    of the cell currently at `insert_pos`.
883    if insert_pos < cell_count {
884        let old_offset = interior_read_slot(page, insert_pos)?;
885        let data = page.as_bytes();
886        let key_len = u16::from_le_bytes([data[old_offset], data[old_offset + 1]]) as usize;
887        let child_pos = old_offset + 2 + key_len;
888        let data = page.as_bytes_mut();
889        data[child_pos..child_pos + 4].copy_from_slice(&right_child.to_le_bytes());
890    } else {
891        page.set_right_child(right_child);
892    }
893
894    // 3. Reserve the new cell at the tail of the free area.
895    let cell_size = 2 + key.len() + 4;
896    let slot_end_after = INTERIOR_DATA_OFFSET + (cell_count + 1) * 2;
897    let cells_start = interior_cells_start(page);
898    if slot_end_after + cell_size > cells_start {
899        return Err(BTreeError::Corrupted("interior page full".into()));
900    }
901    let cell_offset = cells_start - cell_size;
902    {
903        let data = page.as_bytes_mut();
904        data[cell_offset..cell_offset + 2].copy_from_slice(&(key.len() as u16).to_le_bytes());
905        data[cell_offset + 2..cell_offset + 2 + key.len()].copy_from_slice(key);
906        data[cell_offset + 2 + key.len()..cell_offset + 2 + key.len() + 4]
907            .copy_from_slice(&left_child.to_le_bytes());
908    }
909    page.set_free_end(cell_offset as u16);
910
911    // 4. Shift the slot-array tail right by one slot, write the new
912    //    pointer into the freed slot, bump counters.
913    {
914        let shift_from = interior_slot_offset_for(insert_pos);
915        let shift_to = shift_from + 2;
916        let shift_len = (cell_count - insert_pos) * 2;
917        if shift_len > 0 {
918            let data = page.as_bytes_mut();
919            data.copy_within(shift_from..shift_from + shift_len, shift_to);
920        }
921    }
922    interior_write_slot(page, insert_pos, cell_offset as u16)?;
923    page.set_cell_count((cell_count + 1) as u16);
924    page.set_free_start((INTERIOR_DATA_OFFSET + (cell_count + 1) * 2) as u16);
925    Ok(())
926}
927
928fn clear_interior_cells(page: &mut Page) {
929    {
930        let data = page.as_bytes_mut();
931        for byte in &mut data[INTERIOR_DATA_OFFSET..] {
932            *byte = 0;
933        }
934    }
935    page.set_cell_count(0);
936    page.set_free_start(INTERIOR_DATA_OFFSET as u16);
937    page.set_free_end(PAGE_SIZE as u16);
938}
939
940#[cfg(test)]
941mod tests {
942    use super::*;
943    use std::path::PathBuf;
944
945    /// RAII temp DB path: removes the `.db` and every pager sidecar
946    /// (`.db-dwb`, `.db-hdr`, `.db-wal`, …) on drop so nothing is left in
947    /// TMPDIR for the leak guard (scripts/check-temp-residue.sh). The existing
948    /// tests only call `cleanup` at the START (a no-op for unique paths), so
949    /// without this the sidecars created during the test leaked. Derefs to
950    /// `PathBuf` so every `&path` / `cleanup(&path)` call site keeps working.
951    struct TempDbPath(PathBuf);
952    impl std::ops::Deref for TempDbPath {
953        type Target = PathBuf;
954        fn deref(&self) -> &PathBuf {
955            &self.0
956        }
957    }
958    impl AsRef<std::path::Path> for TempDbPath {
959        fn as_ref(&self) -> &std::path::Path {
960            self.0.as_ref()
961        }
962    }
963    impl Drop for TempDbPath {
964        fn drop(&mut self) {
965            cleanup(&self.0);
966        }
967    }
968
969    fn temp_db_path() -> TempDbPath {
970        use std::sync::atomic::{AtomicU64, Ordering};
971        static COUNTER: AtomicU64 = AtomicU64::new(0);
972        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
973        let mut path = std::env::temp_dir();
974        path.push(format!("reddb_btree_test_{}_{}.db", std::process::id(), id));
975        TempDbPath(path)
976    }
977
978    fn cleanup(path: &PathBuf) {
979        // Remove the `.db` AND every sidecar the pager writes next to it
980        // (`.db-dwb`, `.db-hdr`, `.db-wal`, …) so nothing is left in TMPDIR for
981        // the leak guard (scripts/check-temp-residue.sh) to flag.
982        if let (Some(dir), Some(name)) = (
983            path.parent(),
984            path.file_name().and_then(|name| name.to_str()),
985        ) {
986            if let Ok(entries) = std::fs::read_dir(dir) {
987                for entry in entries.flatten() {
988                    if entry
989                        .file_name()
990                        .to_str()
991                        .is_some_and(|entry_name| entry_name.starts_with(name))
992                    {
993                        let _ = std::fs::remove_file(entry.path());
994                    }
995                }
996            }
997        }
998    }
999
1000    #[test]
1001    fn test_btree_empty() {
1002        let path = temp_db_path();
1003        cleanup(&path);
1004
1005        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1006        let tree = BTree::new(pager);
1007
1008        assert!(tree.is_empty());
1009        assert_eq!(tree.root_page_id(), 0);
1010        assert_eq!(tree.get(b"key").expect("get() should succeed"), None);
1011        assert_eq!(tree.count().expect("count() should succeed"), 0);
1012
1013        cleanup(&path);
1014    }
1015
1016    #[test]
1017    fn test_btree_single_insert() {
1018        let path = temp_db_path();
1019        cleanup(&path);
1020
1021        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1022        let tree = BTree::new(pager);
1023
1024        tree.insert(b"hello", b"world")
1025            .expect("insert() should succeed");
1026
1027        assert!(!tree.is_empty());
1028        assert_eq!(
1029            tree.get(b"hello").expect("get() should succeed"),
1030            Some(b"world".to_vec())
1031        );
1032        assert_eq!(tree.get(b"other").expect("get() should succeed"), None);
1033        assert_eq!(tree.count().expect("count() should succeed"), 1);
1034
1035        cleanup(&path);
1036    }
1037
1038    #[test]
1039    fn test_btree_multiple_inserts() {
1040        let path = temp_db_path();
1041        cleanup(&path);
1042
1043        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1044        let tree = BTree::new(pager);
1045
1046        tree.insert(b"c", b"3").expect("insert() should succeed");
1047        tree.insert(b"a", b"1").expect("insert() should succeed");
1048        tree.insert(b"b", b"2").expect("insert() should succeed");
1049
1050        assert_eq!(
1051            tree.get(b"a").expect("get() should succeed"),
1052            Some(b"1".to_vec())
1053        );
1054        assert_eq!(
1055            tree.get(b"b").expect("get() should succeed"),
1056            Some(b"2".to_vec())
1057        );
1058        assert_eq!(
1059            tree.get(b"c").expect("get() should succeed"),
1060            Some(b"3".to_vec())
1061        );
1062        assert_eq!(tree.count().expect("count() should succeed"), 3);
1063
1064        cleanup(&path);
1065    }
1066
1067    #[test]
1068    fn test_btree_duplicate_key() {
1069        let path = temp_db_path();
1070        cleanup(&path);
1071
1072        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1073        let tree = BTree::new(pager);
1074
1075        tree.insert(b"key", b"value1")
1076            .expect("insert() should succeed");
1077        let result = tree.insert(b"key", b"value2");
1078
1079        assert!(matches!(result, Err(BTreeError::DuplicateKey)));
1080        assert_eq!(
1081            tree.get(b"key").expect("get() should succeed"),
1082            Some(b"value1".to_vec())
1083        );
1084
1085        cleanup(&path);
1086    }
1087
1088    #[test]
1089    fn test_btree_delete() {
1090        let path = temp_db_path();
1091        cleanup(&path);
1092
1093        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1094        let tree = BTree::new(pager);
1095
1096        tree.insert(b"a", b"1").expect("insert() should succeed");
1097        tree.insert(b"b", b"2").expect("insert() should succeed");
1098        tree.insert(b"c", b"3").expect("insert() should succeed");
1099
1100        assert!(tree.delete(b"b").expect("delete() should succeed"));
1101        assert!(!tree.delete(b"d").expect("delete() should succeed"));
1102
1103        assert_eq!(
1104            tree.get(b"a").expect("get() should succeed"),
1105            Some(b"1".to_vec())
1106        );
1107        assert_eq!(tree.get(b"b").expect("get() should succeed"), None);
1108        assert_eq!(
1109            tree.get(b"c").expect("get() should succeed"),
1110            Some(b"3".to_vec())
1111        );
1112        assert_eq!(tree.count().expect("count() should succeed"), 2);
1113
1114        cleanup(&path);
1115    }
1116
1117    #[test]
1118    fn test_btree_delete_rebalance_removes_empty_leaf() {
1119        let path = temp_db_path();
1120        cleanup(&path);
1121
1122        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1123        let tree = BTree::new(pager.clone());
1124
1125        let value = vec![b'v'; 200];
1126        for i in 0..300u32 {
1127            let key = format!("key{:03}", i);
1128            tree.insert(key.as_bytes(), &value)
1129                .expect("insert() should succeed");
1130        }
1131
1132        let root_id = tree.root_page_id();
1133        let first_leaf = tree
1134            .find_first_leaf(root_id)
1135            .expect("find_first_leaf() should succeed");
1136        let mut leaf_ids = Vec::new();
1137        let mut current = first_leaf;
1138        loop {
1139            leaf_ids.push(current);
1140            let page = pager
1141                .read_page(current)
1142                .expect("read_page() should succeed");
1143            let next = read_next_leaf(&page);
1144            if next == 0 {
1145                break;
1146            }
1147            current = next;
1148        }
1149
1150        assert!(leaf_ids.len() >= 3);
1151
1152        let target_leaf = leaf_ids[1];
1153        let page = pager
1154            .read_page(target_leaf)
1155            .expect("read_page() should succeed");
1156        let cell_count = page.cell_count() as usize;
1157        let mut keys = Vec::with_capacity(cell_count);
1158        for i in 0..cell_count {
1159            let (key, _) = read_leaf_cell(&page, i).expect("read_leaf_cell() should succeed");
1160            keys.push(key);
1161        }
1162
1163        for key in &keys {
1164            tree.delete(key).expect("delete() should succeed");
1165        }
1166
1167        let expected = 300 - keys.len();
1168        assert_eq!(tree.count().expect("count() should succeed"), expected);
1169
1170        let mut cursor = tree.cursor_first().expect("cursor_first() should succeed");
1171        let mut results = Vec::new();
1172        while let Some((key, _)) = cursor.next().expect("next() should succeed") {
1173            results.push(key);
1174        }
1175
1176        assert_eq!(results.len(), expected);
1177        let last_key = format!("key{:03}", 299).into_bytes();
1178        assert_eq!(results.last(), Some(&last_key));
1179
1180        cleanup(&path);
1181    }
1182
1183    #[test]
1184    fn test_btree_cursor() {
1185        let path = temp_db_path();
1186        cleanup(&path);
1187
1188        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1189        let tree = BTree::new(pager);
1190
1191        tree.insert(b"c", b"3").expect("insert() should succeed");
1192        tree.insert(b"a", b"1").expect("insert() should succeed");
1193        tree.insert(b"b", b"2").expect("insert() should succeed");
1194
1195        let mut cursor = tree.cursor_first().expect("cursor_first() should succeed");
1196        let mut results: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
1197
1198        while let Some(entry) = cursor.next().expect("next() should succeed") {
1199            results.push(entry);
1200        }
1201
1202        assert_eq!(results.len(), 3);
1203        assert_eq!(results[0], (b"a".to_vec(), b"1".to_vec()));
1204        assert_eq!(results[1], (b"b".to_vec(), b"2".to_vec()));
1205        assert_eq!(results[2], (b"c".to_vec(), b"3".to_vec()));
1206
1207        cleanup(&path);
1208    }
1209
1210    #[test]
1211    fn test_btree_range() {
1212        let path = temp_db_path();
1213        cleanup(&path);
1214
1215        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1216        let tree = BTree::new(pager);
1217
1218        for i in 0..10u8 {
1219            let key = format!("key{:02}", i);
1220            let value = format!("val{:02}", i);
1221            tree.insert(key.as_bytes(), value.as_bytes())
1222                .expect("insert() should succeed");
1223        }
1224
1225        let results = tree
1226            .range(b"key03", b"key06")
1227            .expect("range() should succeed");
1228
1229        assert_eq!(results.len(), 4);
1230        assert_eq!(results[0].0, b"key03".to_vec());
1231        assert_eq!(results[3].0, b"key06".to_vec());
1232
1233        cleanup(&path);
1234    }
1235
1236    #[test]
1237    fn test_btree_large_keys() {
1238        let path = temp_db_path();
1239        cleanup(&path);
1240
1241        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1242        let tree = BTree::new(pager);
1243
1244        let key = vec![b'x'; 500];
1245        let value = vec![b'y'; 500];
1246
1247        tree.insert(&key, &value).expect("insert() should succeed");
1248        assert_eq!(tree.get(&key).expect("get() should succeed"), Some(value));
1249
1250        cleanup(&path);
1251    }
1252
1253    #[test]
1254    fn test_btree_key_too_large() {
1255        let path = temp_db_path();
1256        cleanup(&path);
1257
1258        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1259        let tree = BTree::new(pager);
1260
1261        let key = vec![b'x'; MAX_KEY_SIZE + 1];
1262        let result = tree.insert(&key, b"value");
1263
1264        assert!(matches!(result, Err(BTreeError::KeyTooLarge(_))));
1265
1266        cleanup(&path);
1267    }
1268
1269    #[test]
1270    fn test_btree_many_inserts() {
1271        let path = temp_db_path();
1272        cleanup(&path);
1273
1274        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1275        let tree = BTree::new(pager);
1276
1277        // Insert enough entries to force splits
1278        for i in 0..100u32 {
1279            let key = format!("key{:08}", i);
1280            let value = format!("value{:08}", i);
1281            tree.insert(key.as_bytes(), value.as_bytes())
1282                .expect("insert() should succeed");
1283        }
1284
1285        // Verify all entries
1286        for i in 0..100u32 {
1287            let key = format!("key{:08}", i);
1288            let expected = format!("value{:08}", i);
1289            assert_eq!(
1290                tree.get(key.as_bytes()).expect("get() should succeed"),
1291                Some(expected.into_bytes())
1292            );
1293        }
1294
1295        assert_eq!(tree.count().expect("count() should succeed"), 100);
1296
1297        cleanup(&path);
1298    }
1299
1300    #[test]
1301    fn test_btree_bulk_insert_sorted_preserves_leaf_layout() {
1302        let path = temp_db_path();
1303        cleanup(&path);
1304
1305        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1306        let tree = BTree::new(pager);
1307
1308        let items: Vec<(Vec<u8>, Vec<u8>)> = (0..240u32)
1309            .map(|i| {
1310                (
1311                    format!("key{:08}", i).into_bytes(),
1312                    vec![b'v'; 32 + (i as usize % 7)],
1313                )
1314            })
1315            .collect();
1316
1317        tree.bulk_insert_sorted(&items)
1318            .expect("bulk_insert_sorted() should succeed");
1319
1320        assert_eq!(tree.count().expect("count() should succeed"), items.len());
1321
1322        for (key, value) in &items {
1323            assert_eq!(
1324                tree.get(key).expect("get() should succeed"),
1325                Some(value.clone())
1326            );
1327        }
1328
1329        cleanup(&path);
1330    }
1331
1332    /// Property test for `BTree::upsert_batch_sorted` (#159).
1333    ///
1334    /// Generates random batches of (key, value) updates and applies them to
1335    /// two independently-built trees: the baseline uses the existing
1336    /// loop-of-`upsert` path, and the candidate uses `upsert_batch_sorted`
1337    /// after the caller-side sort.  Both trees must end up with identical
1338    /// key→value contents (verified via per-key `get` and a full cursor
1339    /// scan), proving the new path is observationally equivalent for
1340    /// arbitrary batches up to 200 entries.
1341    mod prop_upsert_batch_sorted {
1342        use super::*;
1343        use proptest::prelude::*;
1344        use std::collections::BTreeMap;
1345
1346        /// Build a tree pre-populated with `seed` and return the path so
1347        /// the caller can clean it up.
1348        fn populate_tree(seed: &[(Vec<u8>, Vec<u8>)]) -> (BTree, TempDbPath, Arc<Pager>) {
1349            let path = temp_db_path();
1350            cleanup(&path);
1351            let pager =
1352                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1353            let tree = BTree::new(Arc::clone(&pager));
1354            for (k, v) in seed {
1355                tree.upsert(k, v).expect("upsert() should succeed");
1356            }
1357            (tree, path, pager)
1358        }
1359
1360        fn key_strategy() -> impl Strategy<Value = Vec<u8>> {
1361            // Short keys keep many entries per leaf so a single batch
1362            // hits multiple keys in the same leaf — the path the new
1363            // method optimises.  1..=4 bytes from a small alphabet.
1364            prop::collection::vec(0u8..16u8, 1..=4)
1365        }
1366
1367        fn value_strategy() -> impl Strategy<Value = Vec<u8>> {
1368            // Tiny values — same-or-shrink fits the in-place overwrite
1369            // fast path; growing values fall back to the generic
1370            // single-key `upsert`, exercising both branches.
1371            prop::collection::vec(0u8..255u8, 0..=8)
1372        }
1373
1374        fn pair_strategy() -> impl Strategy<Value = (Vec<u8>, Vec<u8>)> {
1375            (key_strategy(), value_strategy())
1376        }
1377
1378        proptest! {
1379            #![proptest_config(ProptestConfig {
1380                cases: 64,
1381                .. ProptestConfig::default()
1382            })]
1383
1384            #[test]
1385            fn batch_upsert_matches_loop_upsert(
1386                seed in prop::collection::vec(pair_strategy(), 0..50),
1387                batch in prop::collection::vec(pair_strategy(), 1..200),
1388            ) {
1389                // Dedup the seed by key (last write wins) so both trees
1390                // start from the same well-defined state.
1391                let mut seed_map: BTreeMap<Vec<u8>, Vec<u8>> = BTreeMap::new();
1392                for (k, v) in seed {
1393                    seed_map.insert(k, v);
1394                }
1395                let seed: Vec<(Vec<u8>, Vec<u8>)> = seed_map.into_iter().collect();
1396
1397                // Baseline: loop-of-upsert.
1398                let (baseline, baseline_path, _baseline_pager) = populate_tree(&seed);
1399                for (k, v) in &batch {
1400                    baseline.upsert(k, v).expect("upsert() should succeed");
1401                }
1402
1403                // Candidate: upsert_batch_sorted after caller-side sort
1404                // (matches the contract documented on the method).
1405                let (candidate, candidate_path, _candidate_pager) = populate_tree(&seed);
1406                let mut sorted = batch.clone();
1407                sorted.sort_by(|a, b| a.0.cmp(&b.0));
1408                candidate.upsert_batch_sorted(&sorted).expect("upsert_batch_sorted() should succeed");
1409
1410                // Compute the expected end state from BTreeMap (last
1411                // write wins), independent of insertion order.
1412                let mut expected: BTreeMap<Vec<u8>, Vec<u8>> = BTreeMap::new();
1413                for (k, v) in &seed {
1414                    expected.insert(k.clone(), v.clone());
1415                }
1416                for (k, v) in &batch {
1417                    expected.insert(k.clone(), v.clone());
1418                }
1419
1420                // Per-key get matches expected on both trees.
1421                for (k, v) in &expected {
1422                    let baseline_got = baseline.get(k).expect("get() should succeed");
1423                    let candidate_got = candidate.get(k).expect("get() should succeed");
1424                    prop_assert_eq!(baseline_got.as_ref(), Some(v));
1425                    prop_assert_eq!(candidate_got.as_ref(), Some(v));
1426                }
1427
1428                // Full cursor scan yields identical sorted contents.
1429                let collect = |t: &BTree| -> Vec<(Vec<u8>, Vec<u8>)> {
1430                    let mut out = Vec::new();
1431                    let mut cur = t.cursor_first().expect("cursor_first() should succeed");
1432                    while let Some(entry) = cur.next().expect("next() should succeed") {
1433                        out.push(entry);
1434                    }
1435                    out
1436                };
1437                let baseline_contents = collect(&baseline);
1438                let candidate_contents = collect(&candidate);
1439                prop_assert_eq!(&baseline_contents, &candidate_contents);
1440
1441                // And both equal the BTreeMap-derived expectation.
1442                let expected_contents: Vec<(Vec<u8>, Vec<u8>)> =
1443                    expected.into_iter().collect();
1444                prop_assert_eq!(&candidate_contents, &expected_contents);
1445
1446                // Drop the trees and pagers BEFORE cleanup. The pager flushes
1447                // its header (`.db-hdr`) on drop, so cleaning up while an
1448                // `Arc<Pager>` is still alive lets the final drop re-create the
1449                // sidecar AFTER removal — exactly the leak the guard
1450                // (scripts/check-temp-residue.sh) flags (128 `.db-hdr` files,
1451                // one per tree per proptest case). Explicit drops make cleanup
1452                // the last thing to touch these paths.
1453                drop(baseline);
1454                drop(candidate);
1455                drop(_baseline_pager);
1456                drop(_candidate_pager);
1457                cleanup(&baseline_path);
1458                cleanup(&candidate_path);
1459            }
1460        }
1461    }
1462
1463    #[test]
1464    fn test_btree_sorted_iteration() {
1465        let path = temp_db_path();
1466        cleanup(&path);
1467
1468        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1469        let tree = BTree::new(pager);
1470
1471        // Insert in random order
1472        let keys = vec![50, 25, 75, 10, 30, 60, 80, 5, 15, 27, 35, 55, 65, 77, 90];
1473        for k in &keys {
1474            let key = format!("{:03}", k);
1475            tree.insert(key.as_bytes(), key.as_bytes())
1476                .expect("insert() should succeed");
1477        }
1478
1479        // Should iterate in sorted order
1480        let mut cursor = tree.cursor_first().expect("cursor_first() should succeed");
1481        let mut prev: Option<Vec<u8>> = None;
1482
1483        while let Some((key, _)) = cursor.next().expect("next() should succeed") {
1484            if let Some(p) = &prev {
1485                assert!(p < &key, "Keys not in sorted order");
1486            }
1487            prev = Some(key);
1488        }
1489
1490        cleanup(&path);
1491    }
1492
1493    /// #2376: several threads writing one tree (the shape of concurrent
1494    /// DDL emitting `red.control_events` rows) must not lose keys or tear
1495    /// the tree through a racing copy-modify-write of the same leaf or a
1496    /// racing split.
1497    #[test]
1498    fn concurrent_writers_on_one_tree_keep_every_key() {
1499        const WRITER_COUNT: u32 = 4;
1500        const KEY_COUNT_PER_WRITER: u32 = 2_000;
1501
1502        let path = temp_db_path();
1503        cleanup(&path);
1504        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1505        let tree = Arc::new(BTree::new(pager));
1506        let value = vec![7u8; 96];
1507
1508        let writers: Vec<_> = (0..WRITER_COUNT)
1509            .map(|writer| {
1510                let tree = Arc::clone(&tree);
1511                let value = value.clone();
1512                std::thread::spawn(move || {
1513                    for i in 0..KEY_COUNT_PER_WRITER {
1514                        // Interleave the writers' key spaces so they share leaves.
1515                        let key = (i * WRITER_COUNT + writer).to_be_bytes();
1516                        tree.insert(&key, &value).expect("insert() should succeed");
1517                        // Churn: delete and re-insert every fourth key.
1518                        if i % 4 == 0 {
1519                            assert!(tree.delete(&key).expect("delete() should succeed"));
1520                            tree.upsert(&key, &value).expect("upsert() should succeed");
1521                        }
1522                    }
1523                })
1524            })
1525            .collect();
1526        for writer in writers {
1527            writer.join().expect("writer thread panicked");
1528        }
1529
1530        let key_count = WRITER_COUNT * KEY_COUNT_PER_WRITER;
1531        let mut cursor = tree.cursor_first().expect("cursor_first() should succeed");
1532        let mut seen = Vec::new();
1533        while let Some((key, _)) = cursor.next().expect("next() should succeed") {
1534            seen.push(u32::from_be_bytes(
1535                key.as_slice().try_into().expect("invariant: 4-byte keys"),
1536            ));
1537        }
1538        let expected: Vec<u32> = (0..key_count).collect();
1539        assert_eq!(seen, expected, "every key exactly once, in order");
1540        for key in 0..key_count {
1541            assert!(
1542                tree.get(&key.to_be_bytes())
1543                    .expect("get() should succeed")
1544                    .is_some(),
1545                "key {key} must be reachable from the root"
1546            );
1547        }
1548
1549        cleanup(&path);
1550    }
1551}
1552
1553/// Acceptance tests for slice E of PRD #662 — spill pipeline that wires
1554/// the per-cell value layout (`value_layout`), the LZ4 codec
1555/// (`vector_btree::value_codec`), and the overflow chain (`overflow`)
1556/// into the B-tree's `insert` / `bulk_insert_sorted` / `get` paths.
1557#[cfg(test)]
1558mod overflow_pipeline_tests {
1559    use super::value_layout::{MAX_VALUE_SIZE, OVERFLOW_THRESHOLD};
1560    use super::*;
1561    use std::path::PathBuf;
1562
1563    fn temp_db_path(tag: &str) -> PathBuf {
1564        use std::sync::atomic::{AtomicU64, Ordering};
1565        static COUNTER: AtomicU64 = AtomicU64::new(0);
1566        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
1567        let mut path = std::env::temp_dir();
1568        path.push(format!(
1569            "reddb_btree_spill_{}_{}_{}.db",
1570            tag,
1571            std::process::id(),
1572            id
1573        ));
1574        path
1575    }
1576
1577    fn cleanup(path: &std::path::Path) {
1578        let _ = std::fs::remove_file(path);
1579        for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
1580            let _ = std::fs::remove_file(sidecar);
1581        }
1582    }
1583
1584    /// AC #1: a value at `OVERFLOW_THRESHOLD - 1` stores inline (raw or
1585    /// compressed) and **no overflow pages are allocated**. We count
1586    /// the file's pages before and after; the only growth should be the
1587    /// leaf page itself.
1588    #[test]
1589    fn inline_path_allocates_no_overflow_pages() {
1590        let path = temp_db_path("inline_no_overflow");
1591        cleanup(&path);
1592        {
1593            let pager =
1594                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1595            let tree = BTree::new(pager.clone());
1596            let before = pager.page_count().expect("page_count() should succeed");
1597
1598            let value = vec![0x5Au8; OVERFLOW_THRESHOLD - 1];
1599            tree.insert(b"k", &value).expect("insert() should succeed");
1600
1601            let after = pager.page_count().expect("page_count() should succeed");
1602            // Exactly one new page (the root leaf). An overflow path
1603            // would have allocated ≥ 2 pages (leaf + chain head).
1604            assert_eq!(
1605                after - before,
1606                1,
1607                "inline value must allocate only the root leaf"
1608            );
1609            assert_eq!(tree.get(b"k").expect("get() should succeed"), Some(value));
1610        }
1611        cleanup(&path);
1612    }
1613
1614    /// AC #2: a 44 KB highly-compressible markdown payload stores via
1615    /// the compressed-inline path (or via overflow if it still exceeds
1616    /// the threshold after LZ4) and `get` returns byte-identical bytes.
1617    #[test]
1618    fn compressible_44kb_round_trips() {
1619        let path = temp_db_path("compressible_44kb");
1620        cleanup(&path);
1621        {
1622            let pager =
1623                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1624            let tree = BTree::new(pager);
1625
1626            // Repeating markdown chunk — compresses far below threshold.
1627            let snippet = "# heading\n- bullet line that says the same thing over and over\n";
1628            let mut value = Vec::with_capacity(44 * 1024);
1629            while value.len() < 44 * 1024 {
1630                value.extend_from_slice(snippet.as_bytes());
1631            }
1632            assert!(value.len() > OVERFLOW_THRESHOLD);
1633
1634            tree.insert(b"doc", &value)
1635                .expect("insert() should succeed");
1636            assert_eq!(tree.get(b"doc").expect("get() should succeed"), Some(value));
1637        }
1638        cleanup(&path);
1639    }
1640
1641    /// AC #3: a 5 MB incompressible payload spills via overflow chain
1642    /// and `get` returns byte-identical bytes.
1643    #[test]
1644    fn incompressible_5mb_spills_and_round_trips() {
1645        let path = temp_db_path("incompressible_5mb");
1646        cleanup(&path);
1647        {
1648            let pager =
1649                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1650            let tree = BTree::new(pager.clone());
1651
1652            // xorshift produces incompressible bytes deterministically
1653            // so the test never flakes on the codec's heuristic.
1654            let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
1655            let value: Vec<u8> = (0..5 * 1024 * 1024)
1656                .map(|_| {
1657                    state ^= state << 13;
1658                    state ^= state >> 7;
1659                    state ^= state << 17;
1660                    state as u8
1661                })
1662                .collect();
1663
1664            let before = pager.page_count().expect("page_count() should succeed");
1665            tree.insert(b"blob", &value)
1666                .expect("insert() should succeed");
1667            let after = pager.page_count().expect("page_count() should succeed");
1668            // 5 MB / overflow-page payload ≈ many pages — the leaf
1669            // alone could not absorb this.
1670            assert!(
1671                after - before > 2,
1672                "5 MB spill must allocate overflow pages (alloc'd {} pages)",
1673                after - before
1674            );
1675
1676            assert_eq!(
1677                tree.get(b"blob").expect("get() should succeed").as_deref(),
1678                Some(value.as_slice())
1679            );
1680        }
1681        cleanup(&path);
1682    }
1683
1684    /// AC #4: a value just above `MAX_VALUE_SIZE` is rejected with
1685    /// `ValueTooLarge`; nothing is allocated (no LZ4 work, no overflow
1686    /// pages, no leaf page).
1687    #[test]
1688    fn value_above_ceiling_rejected_without_allocation() {
1689        let path = temp_db_path("ceiling_reject");
1690        cleanup(&path);
1691        {
1692            let pager =
1693                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1694            let tree = BTree::new(pager.clone());
1695            let before = pager.page_count().expect("page_count() should succeed");
1696
1697            let value = vec![0u8; MAX_VALUE_SIZE + 1];
1698            let err = tree.insert(b"too_big", &value).unwrap_err();
1699            assert!(matches!(err, BTreeError::ValueTooLarge(_)));
1700
1701            assert_eq!(
1702                pager.page_count().expect("page_count() should succeed"),
1703                before,
1704                "rejected value must not allocate pages"
1705            );
1706            // Tree stays empty — no root leaf was created.
1707            assert!(tree.is_empty());
1708        }
1709        cleanup(&path);
1710    }
1711
1712    /// AC #5: `bulk_insert_sorted` with mixed value sizes inserts all
1713    /// valid rows; an oversized row in the middle is rejected but the
1714    /// rest land. The function still surfaces the violation in its
1715    /// return value so callers do not silently lose data.
1716    #[test]
1717    fn bulk_insert_skips_oversized_row_keeps_rest() {
1718        let path = temp_db_path("bulk_mixed");
1719        cleanup(&path);
1720        {
1721            let pager =
1722                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1723            let tree = BTree::new(pager);
1724
1725            let mut items: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
1726            // Five small rows ...
1727            for i in 0..5u32 {
1728                items.push((format!("k{:04}", i).into_bytes(), vec![b'a' + i as u8; 64]));
1729            }
1730            // ... one oversize row in the middle ...
1731            items.push((b"k0005_huge".to_vec(), vec![0u8; MAX_VALUE_SIZE + 1]));
1732            // ... and five more small rows after it. Keys stay sorted.
1733            for i in 6..11u32 {
1734                items.push((format!("k{:04}", i).into_bytes(), vec![b'z' - i as u8; 32]));
1735            }
1736
1737            let res = tree.bulk_insert_sorted(&items);
1738            assert!(matches!(res, Err(BTreeError::ValueTooLarge(_))));
1739
1740            // All valid rows persisted.
1741            for (k, v) in items.iter().filter(|(_, v)| v.len() <= MAX_VALUE_SIZE) {
1742                assert_eq!(
1743                    tree.get(k).expect("get() should succeed").as_deref(),
1744                    Some(v.as_slice()),
1745                    "valid row {:?} must land",
1746                    k
1747                );
1748            }
1749            // The oversize row did not land.
1750            assert_eq!(tree.get(b"k0005_huge").expect("get() should succeed"), None);
1751        }
1752        cleanup(&path);
1753    }
1754
1755    /// AC #6: values that used to fail (≥ 1024 bytes) now persist
1756    /// transparently. Walk a handful of sizes that straddle the
1757    /// threshold and the compressed-inline / spill boundaries.
1758    #[test]
1759    fn large_values_persist_transparently() {
1760        let path = temp_db_path("large_transparent");
1761        cleanup(&path);
1762        {
1763            let pager =
1764                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1765            let tree = BTree::new(pager);
1766
1767            // Sizes hit: just over threshold, well over threshold,
1768            // and several MB. Bytes pattern is the index — so the
1769            // values differ across keys and are incompressible enough
1770            // that LZ4 falls back to raw on the largest entries.
1771            for (i, size) in [
1772                OVERFLOW_THRESHOLD + 1,
1773                OVERFLOW_THRESHOLD * 2,
1774                64 * 1024,
1775                1 * 1024 * 1024,
1776            ]
1777            .iter()
1778            .enumerate()
1779            {
1780                let value: Vec<u8> = (0..*size).map(|n| ((n + i) & 0xff) as u8).collect();
1781                let key = format!("size{:08}", size).into_bytes();
1782                tree.insert(&key, &value).expect("insert() should succeed");
1783                assert_eq!(
1784                    tree.get(&key).expect("get() should succeed").as_deref(),
1785                    Some(value.as_slice()),
1786                    "size {} must round-trip",
1787                    size
1788                );
1789            }
1790        }
1791        cleanup(&path);
1792    }
1793}
1794
1795/// Acceptance tests for slice F of PRD #662 — freeing overflow chains
1796/// on delete and shrinking update so storage does not leak.
1797#[cfg(test)]
1798mod overflow_free_tests {
1799    use super::value_layout::OVERFLOW_THRESHOLD;
1800    use super::*;
1801    use std::path::PathBuf;
1802
1803    fn temp_db_path(tag: &str) -> PathBuf {
1804        use std::sync::atomic::{AtomicU64, Ordering};
1805        static COUNTER: AtomicU64 = AtomicU64::new(0);
1806        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
1807        let mut path = std::env::temp_dir();
1808        path.push(format!(
1809            "reddb_btree_free_{}_{}_{}.db",
1810            tag,
1811            std::process::id(),
1812            id
1813        ));
1814        path
1815    }
1816
1817    fn cleanup(path: &std::path::Path) {
1818        let _ = std::fs::remove_file(path);
1819        for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
1820            let _ = std::fs::remove_file(sidecar);
1821        }
1822    }
1823
1824    fn incompressible(seed: u64, len: usize) -> Vec<u8> {
1825        let mut state = seed | 1;
1826        (0..len)
1827            .map(|_| {
1828                state ^= state << 13;
1829                state ^= state >> 7;
1830                state ^= state << 17;
1831                state as u8
1832            })
1833            .collect()
1834    }
1835
1836    /// AC #1: deleting a row whose value spilled returns every overflow
1837    /// page to the free list. Observable signal: page_count stays
1838    /// constant when we insert a same-size row right after the delete
1839    /// (no file growth on the second insert), and `tree.get` returns
1840    /// the new value.
1841    #[test]
1842    fn delete_spilled_value_frees_overflow_chain() {
1843        let path = temp_db_path("delete_frees");
1844        cleanup(&path);
1845        {
1846            let pager =
1847                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1848            let tree = BTree::new(pager.clone());
1849
1850            // First spilled insert grows the file.
1851            let v1 = incompressible(0xA11CE, 2 * 1024 * 1024);
1852            tree.insert(b"k", &v1).expect("insert() should succeed");
1853            let after_first = pager.page_count().expect("page_count() should succeed");
1854
1855            // Delete: every overflow page returned to the free list.
1856            assert!(tree.delete(b"k").expect("delete() should succeed"));
1857
1858            // Re-insert the same size. With a working free list, the
1859            // pager reuses the freed pages and the file does not grow.
1860            let v2 = incompressible(0xB0B, 2 * 1024 * 1024);
1861            tree.insert(b"k", &v2).expect("insert() should succeed");
1862            let after_second = pager.page_count().expect("page_count() should succeed");
1863
1864            assert_eq!(
1865                after_second, after_first,
1866                "second spill must reuse freed pages, not grow the file"
1867            );
1868            assert_eq!(
1869                tree.get(b"k").expect("get() should succeed").as_deref(),
1870                Some(v2.as_slice())
1871            );
1872        }
1873        cleanup(&path);
1874    }
1875
1876    /// AC #2: updating a spilled value to a small inline value frees
1877    /// the old chain in full. Same observability trick: insert another
1878    /// spilled value of the original size, see that the file does not
1879    /// grow beyond the original high-water mark.
1880    #[test]
1881    fn shrinking_update_to_inline_frees_chain() {
1882        let path = temp_db_path("shrink_to_inline");
1883        cleanup(&path);
1884        {
1885            let pager =
1886                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1887            let tree = BTree::new(pager.clone());
1888
1889            let big = incompressible(0xDEAD, 2 * 1024 * 1024);
1890            tree.insert(b"k", &big).expect("insert() should succeed");
1891            let hwm = pager.page_count().expect("page_count() should succeed");
1892
1893            // Shrink to inline raw (fits well under the threshold).
1894            let small = b"tiny".to_vec();
1895            tree.upsert(b"k", &small).expect("upsert() should succeed");
1896            assert_eq!(
1897                tree.get(b"k").expect("get() should succeed").as_deref(),
1898                Some(small.as_slice())
1899            );
1900
1901            // If the chain was not freed, this insert would push the
1902            // page count past `hwm`. If it was, the pager reuses freed
1903            // pages and we stay at the high-water mark.
1904            let big2 = incompressible(0xBEEF, 2 * 1024 * 1024);
1905            tree.insert(b"k2", &big2).expect("insert() should succeed");
1906            assert_eq!(
1907                pager.page_count().expect("page_count() should succeed"),
1908                hwm,
1909                "shrinking update must free the chain — replacement spill should reuse"
1910            );
1911            assert_eq!(
1912                tree.get(b"k2").expect("get() should succeed").as_deref(),
1913                Some(big2.as_slice())
1914            );
1915        }
1916        cleanup(&path);
1917    }
1918
1919    /// AC #3: updating a spilled value to a shorter (still spilled)
1920    /// value frees the old chain in full. The new chain is independent
1921    /// — no in-place reuse — so the freed pages must show up in the
1922    /// allocator before any new growth happens.
1923    #[test]
1924    fn shrinking_update_to_shorter_spill_frees_old_chain() {
1925        let path = temp_db_path("shrink_spill_to_spill");
1926        cleanup(&path);
1927        {
1928            let pager =
1929                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1930            let tree = BTree::new(pager.clone());
1931
1932            // Two spilled values, the second clearly shorter than the
1933            // first. Both encode through the pointer path because the
1934            // bytes are incompressible.
1935            let big = incompressible(0xF00D, 3 * 1024 * 1024);
1936            tree.insert(b"k", &big).expect("insert() should succeed");
1937            let hwm_after_big = pager.page_count().expect("page_count() should succeed");
1938
1939            // Per slice F's acceptance the new chain is *independent*
1940            // of the old — i.e. no in-place page reuse — so the
1941            // upsert allocates fresh pages for the smaller chain
1942            // before freeing the old one. The file may grow during
1943            // this window; what we care about is that the freed pages
1944            // re-enter the allocator. We measure that by dropping the
1945            // row entirely (which frees the new chain too) and
1946            // re-inserting at the original size: if both chains were
1947            // freed, the pager reuses freed pages and we do not exceed
1948            // the high-water mark observed during the transient.
1949            let smaller = incompressible(0xCAFE, 1 * 1024 * 1024);
1950            tree.upsert(b"k", &smaller)
1951                .expect("upsert() should succeed");
1952            assert_eq!(
1953                tree.get(b"k").expect("get() should succeed").as_deref(),
1954                Some(smaller.as_slice())
1955            );
1956            let hwm_after_upsert = pager.page_count().expect("page_count() should succeed");
1957
1958            assert!(tree.delete(b"k").expect("delete() should succeed"));
1959
1960            let again = incompressible(0x1234, 3 * 1024 * 1024);
1961            tree.insert(b"k", &again).expect("insert() should succeed");
1962            assert!(
1963                pager.page_count().expect("page_count() should succeed") <= hwm_after_upsert,
1964                "after delete+insert of original size, file must not grow past the upsert transient \
1965                 (page_count = {}, transient high-water = {}, original = {}) — the upsert must have \
1966                 freed the old chain so the re-insert reuses freed pages",
1967                pager.page_count().expect("page_count() should succeed"),
1968                hwm_after_upsert,
1969                hwm_after_big,
1970            );
1971            assert_eq!(
1972                tree.get(b"k").expect("get() should succeed").as_deref(),
1973                Some(again.as_slice())
1974            );
1975        }
1976        cleanup(&path);
1977    }
1978
1979    /// AC #5 regression: insert large → delete → insert same-size large
1980    /// → file does not grow on the second insert.
1981    #[test]
1982    fn insert_delete_reinsert_does_not_grow_file() {
1983        let path = temp_db_path("reinsert_no_grow");
1984        cleanup(&path);
1985        {
1986            let pager =
1987                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1988            let tree = BTree::new(pager.clone());
1989
1990            let v1 = incompressible(0x5EED, 4 * 1024 * 1024);
1991            tree.insert(b"row", &v1).expect("insert() should succeed");
1992            let pages_after_first = pager.page_count().expect("page_count() should succeed");
1993
1994            assert!(tree.delete(b"row").expect("delete() should succeed"));
1995
1996            let v2 = incompressible(0xACE, 4 * 1024 * 1024);
1997            tree.insert(b"row", &v2).expect("insert() should succeed");
1998            let pages_after_second = pager.page_count().expect("page_count() should succeed");
1999
2000            assert_eq!(
2001                pages_after_second, pages_after_first,
2002                "re-inserting at the same size must reuse the freed chain"
2003            );
2004            assert_eq!(
2005                tree.get(b"row").expect("get() should succeed").as_deref(),
2006                Some(v2.as_slice())
2007            );
2008        }
2009        cleanup(&path);
2010    }
2011
2012    /// Belt-and-braces: an in-place shrink via `upsert_batch_sorted`
2013    /// — same in-place fast path as single-key `upsert` — must also
2014    /// free the old chain. Without this hook the batch path would
2015    /// silently leak on every `bulk_update` over spilled rows.
2016    #[test]
2017    fn upsert_batch_in_place_shrink_frees_chain() {
2018        let path = temp_db_path("batch_shrink");
2019        cleanup(&path);
2020        {
2021            let pager =
2022                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
2023            let tree = BTree::new(pager.clone());
2024
2025            // Seed two spilled rows so the batch update lands in-place
2026            // on the same leaf.
2027            let big_a = incompressible(0x1111, 2 * 1024 * 1024);
2028            let big_b = incompressible(0x2222, 2 * 1024 * 1024);
2029            tree.insert(b"a", &big_a).expect("insert() should succeed");
2030            tree.insert(b"b", &big_b).expect("insert() should succeed");
2031            let hwm = pager.page_count().expect("page_count() should succeed");
2032
2033            let updates = vec![
2034                (b"a".to_vec(), b"shrunk-a".to_vec()),
2035                (b"b".to_vec(), b"shrunk-b".to_vec()),
2036            ];
2037            tree.upsert_batch_sorted(&updates)
2038                .expect("upsert_batch_sorted() should succeed");
2039            assert_eq!(
2040                tree.get(b"a").expect("get() should succeed").as_deref(),
2041                Some(b"shrunk-a".as_slice())
2042            );
2043            assert_eq!(
2044                tree.get(b"b").expect("get() should succeed").as_deref(),
2045                Some(b"shrunk-b".as_slice())
2046            );
2047
2048            // Both chains must be freed: a fresh pair of spilled
2049            // inserts re-uses them instead of growing the file.
2050            let again_a = incompressible(0xAAAA, 2 * 1024 * 1024);
2051            let again_b = incompressible(0xBBBB, 2 * 1024 * 1024);
2052            tree.insert(b"c", &again_a)
2053                .expect("insert() should succeed");
2054            tree.insert(b"d", &again_b)
2055                .expect("insert() should succeed");
2056            assert!(
2057                pager.page_count().expect("page_count() should succeed") <= hwm,
2058                "batch in-place shrink must free chains — replacement spills should reuse"
2059            );
2060        }
2061        cleanup(&path);
2062    }
2063}