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