Skip to main content

reddb_engine/storage/
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::page_store::PageStore;
37use super::pager::PagerError;
38
39/// Maximum key size (to ensure at least 2 keys per node)
40pub const MAX_KEY_SIZE: usize = 1024;
41
42/// Hard upper bound on logical value size.
43///
44/// Per ADR 0023 (large-value overflow), values up to 256 MiB are stored
45/// transparently — inline below [`value_layout::OVERFLOW_THRESHOLD`],
46/// inline+compressed when LZ4 shrinks them under the same threshold, or
47/// spilled into an [`overflow::OverflowChain`] otherwise. Values above
48/// this ceiling are rejected with [`BTreeError::ValueTooLarge`] before
49/// any compression or page allocation runs.
50///
51/// Re-exported from [`value_layout::MAX_VALUE_SIZE`] so callers that
52/// historically read this constant (`unified::store::impl_pages`,
53/// `query::planner::stats_catalog`) continue to compile.
54pub const MAX_VALUE_SIZE: usize = value_layout::MAX_VALUE_SIZE;
55
56/// Minimum fill factor before merge (as percentage)
57pub const MIN_FILL_FACTOR: usize = 25;
58
59/// Offset of prev_leaf in leaf page
60const LEAF_PREV_OFFSET: usize = HEADER_SIZE;
61
62/// Offset of next_leaf in leaf page
63const LEAF_NEXT_OFFSET: usize = HEADER_SIZE + 4;
64
65/// Start of the slot array in leaf pages.
66///
67/// Slotted-page layout:
68/// ```text
69/// [PageHeader 32B][prev u32 | next u32 | 8B]
70/// [slot 0: u16][slot 1: u16]...[slot N-1: u16]  ← grows forward
71/// ... free space ...
72/// [cell N-1][cell N-2]...[cell 0]               ← grows backward from free_end
73/// ```
74/// Each cell is laid out as `[key_len:u16][val_len:u16][key][val]`.
75/// `page.cell_count()` is N; `page.free_end()` is the offset of the
76/// lowest (most recently written) cell. The slot array lives right
77/// after the leaf-chain links and each u16 slot is the absolute page
78/// offset of its cell.
79const LEAF_SLOT_ARRAY_OFFSET: usize = HEADER_SIZE + 8;
80
81/// Kept for source-compat with older code paths (e.g. interior-node
82/// helpers); equivalent to `LEAF_SLOT_ARRAY_OFFSET` for leaves.
83const LEAF_DATA_OFFSET: usize = LEAF_SLOT_ARRAY_OFFSET;
84
85/// Start of cell data in interior pages (right_child is in header)
86const INTERIOR_DATA_OFFSET: usize = HEADER_SIZE;
87
88/// B+ Tree error types
89#[derive(Debug, Clone)]
90pub enum BTreeError {
91    /// Key not found
92    NotFound,
93    /// Key already exists
94    DuplicateKey,
95    /// Key too large
96    KeyTooLarge(usize),
97    /// Value too large
98    ValueTooLarge(usize),
99    /// Tree is corrupted
100    Corrupted(String),
101    /// Pager error
102    Pager(String),
103    /// Internal lock was poisoned by a panicked thread
104    LockPoisoned(String),
105    /// Large-value layout encode/decode failed (overflow chain or LZ4).
106    ValueLayout(String),
107}
108
109impl std::fmt::Display for BTreeError {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self {
112            Self::NotFound => write!(f, "Key not found"),
113            Self::DuplicateKey => write!(f, "Key already exists"),
114            Self::KeyTooLarge(size) => {
115                write!(f, "Key too large: {} bytes (max {})", size, MAX_KEY_SIZE)
116            }
117            Self::ValueTooLarge(size) => write!(
118                f,
119                "Value too large: {} bytes (max {})",
120                size, MAX_VALUE_SIZE
121            ),
122            Self::Corrupted(msg) => write!(f, "B-tree corrupted: {}", msg),
123            Self::Pager(msg) => write!(f, "Pager error: {}", msg),
124            Self::LockPoisoned(msg) => write!(f, "Lock poisoned: {}", msg),
125            Self::ValueLayout(msg) => write!(f, "Value layout: {}", msg),
126        }
127    }
128}
129
130impl From<value_layout::ValueLayoutError> for BTreeError {
131    fn from(e: value_layout::ValueLayoutError) -> Self {
132        match e {
133            value_layout::ValueLayoutError::ValueTooLarge(n) => Self::ValueTooLarge(n),
134            other => Self::ValueLayout(other.to_string()),
135        }
136    }
137}
138
139impl std::error::Error for BTreeError {}
140
141impl From<PagerError> for BTreeError {
142    fn from(e: PagerError) -> Self {
143        Self::Pager(e.to_string())
144    }
145}
146
147impl From<super::page::PageError> for BTreeError {
148    fn from(e: super::page::PageError) -> Self {
149        Self::Corrupted(e.to_string())
150    }
151}
152
153/// Result type for B+ tree operations
154pub type BTreeResult<T> = Result<T, BTreeError>;
155
156/// B+ Tree cursor for iteration
157pub struct BTreeCursor<P: PageStore> {
158    /// A cursor can outlive the tree handle that created it.
159    _retirement_pin: Arc<()>,
160    /// Current leaf page ID
161    leaf_page_id: u32,
162    /// Current position within leaf
163    position: usize,
164    /// Pager reference
165    pager: Arc<P>,
166    /// A6 — true once we've issued a prefetch hint for the next leaf on
167    /// this leaf. Reset to false every time we move to a new leaf so the
168    /// hint fires once per leaf, at the halfway point.
169    prefetched_next: bool,
170}
171
172impl<P: PageStore> BTreeCursor<P> {
173    /// Move to next entry
174    pub fn next(&mut self) -> BTreeResult<Option<(Vec<u8>, Vec<u8>)>> {
175        if self.leaf_page_id == 0 {
176            return Ok(None);
177        }
178
179        let page = self.pager.read_page(self.leaf_page_id)?;
180        if page.page_type()? != PageType::BTreeLeaf {
181            return Err(BTreeError::Corrupted(
182                "cursor reached a non-leaf page".into(),
183            ));
184        }
185        let cell_count = page.cell_count() as usize;
186
187        // A6 — prefetch the next leaf when we cross the halfway point of
188        // the current one. The hint fires at most once per leaf (guarded
189        // by `prefetched_next`) so the syscall overhead is amortized over
190        // ~half a leaf's worth of entries. The kernel uses the hint to
191        // start DMA-ing the next page while we finish the current one.
192        if !self.prefetched_next && self.position >= cell_count / 2 && cell_count > 0 {
193            let next_leaf = read_next_leaf(&page);
194            if next_leaf != 0 {
195                self.pager.prefetch_hint(next_leaf);
196            }
197            self.prefetched_next = true;
198        }
199
200        // Check if we have more cells in current page
201        if self.position < cell_count {
202            let (key, stored) = read_leaf_cell(&page, self.position)?;
203            let value = value_layout::decode(self.pager.as_ref(), &stored)?;
204            self.position += 1;
205            return Ok(Some((key, value)));
206        }
207
208        // Move to next leaf
209        let next_leaf = read_next_leaf(&page);
210        if next_leaf == 0 {
211            self.leaf_page_id = 0;
212            return Ok(None);
213        }
214        let previous_high_key = leaf_high_key(&page)?;
215
216        self.leaf_page_id = next_leaf;
217        self.position = 0;
218        self.prefetched_next = false; // reset for the new leaf
219
220        // Read from new leaf
221        let page = self.pager.read_page(self.leaf_page_id)?;
222        if page.page_type()? != PageType::BTreeLeaf {
223            return Err(BTreeError::Corrupted(
224                "leaf chain reached a non-leaf page".into(),
225            ));
226        }
227        if page.cell_count() == 0 {
228            return Ok(None);
229        }
230
231        let (key, stored) = read_leaf_cell(&page, 0)?;
232        // Every leaf transition must advance the key range. This bounds a
233        // corrupt cyclic chain without growing a visited-page set or an
234        // unbounded checkpoint buffer (#2376).
235        if previous_high_key
236            .as_ref()
237            .is_some_and(|previous| key <= *previous)
238        {
239            self.leaf_page_id = 0;
240            return Err(BTreeError::Corrupted(
241                "leaf chain does not advance its key range".into(),
242            ));
243        }
244        let value = value_layout::decode(self.pager.as_ref(), &stored)?;
245        self.position = 1;
246        Ok(Some((key, value)))
247    }
248
249    /// Peek at current entry without advancing
250    pub fn peek(&self) -> BTreeResult<Option<(Vec<u8>, Vec<u8>)>> {
251        if self.leaf_page_id == 0 {
252            return Ok(None);
253        }
254
255        let page = self.pager.read_page(self.leaf_page_id)?;
256        let cell_count = page.cell_count() as usize;
257
258        if self.position >= cell_count {
259            return Ok(None);
260        }
261
262        let (key, stored) = read_leaf_cell(&page, self.position)?;
263        let value = value_layout::decode(self.pager.as_ref(), &stored)?;
264        Ok(Some((key, value)))
265    }
266}
267
268/// B+ Tree implementation
269pub struct BTree<P: PageStore> {
270    retirement_pin: Arc<()>,
271    /// Pager for page I/O
272    pager: Arc<P>,
273    /// Root page ID (0 = empty tree)
274    root_page_id: RwLock<u32>,
275    /// D2 fastpath: cached rightmost leaf (page_id, high_key).
276    ///
277    /// When the next key to insert is strictly greater than `high_key`,
278    /// skip `find_leaf` (tree descent from root) and go directly to the
279    /// cached page. Invalidated on splits and deletes.
280    ///
281    /// Mirrors `BTPageCacheIsValid` / `BTREE_FASTPATH_MIN_LEVEL` in
282    /// `nbtinsert.c:31`.
283    rightmost_leaf: RwLock<Option<(u32, Vec<u8>)>>,
284    /// Serialises structural writers (insert / upsert / delete / bulk).
285    ///
286    /// Writers copy a page, modify the copy and write it back, and there
287    /// are no page latches, so two writers on one tree would lose each
288    /// other's leaf updates or tear a split (#2376). Reentrant because the
289    /// public mutators call each other (`upsert` -> `insert`, the bulk
290    /// paths -> `insert_encoded`). Readers do not take it.
291    writer: parking_lot::ReentrantMutex<()>,
292}
293
294#[path = "btree/impl.rs"]
295mod btree_impl;
296
297#[path = "btree/reclaim.rs"]
298mod reclaim;
299
300#[path = "btree/lehman_yao.rs"]
301pub mod lehman_yao;
302
303#[path = "btree/value_layout.rs"]
304pub mod value_layout;
305// ==================== Search Helpers ====================
306
307enum SearchResult {
308    Found(usize),
309    NotFound(usize),
310}
311
312fn search_leaf(page: &Page, key: &[u8]) -> BTreeResult<SearchResult> {
313    let cell_count = page.cell_count() as usize;
314
315    // Binary search
316    let mut low = 0;
317    let mut high = cell_count;
318
319    while low < high {
320        let mid = (low + high) / 2;
321        let (cell_key, _) = read_leaf_cell(page, mid)?;
322
323        match cell_key.as_slice().cmp(key) {
324            Ordering::Less => low = mid + 1,
325            Ordering::Greater => high = mid,
326            Ordering::Equal => return Ok(SearchResult::Found(mid)),
327        }
328    }
329
330    Ok(SearchResult::NotFound(low))
331}
332
333fn find_child(page: &Page, key: &[u8]) -> BTreeResult<u32> {
334    let cell_count = page.cell_count() as usize;
335
336    // Binary search for the correct child
337    for i in 0..cell_count {
338        let (cell_key, child) = read_interior_cell(page, i)?;
339        if key < cell_key.as_slice() {
340            return Ok(child);
341        }
342    }
343
344    // Key is >= all keys, use right child
345    Ok(page.right_child())
346}
347
348fn find_first_child(page: &Page) -> BTreeResult<u32> {
349    if page.cell_count() == 0 {
350        return Ok(page.right_child());
351    }
352    let (_, child) = read_interior_cell(page, 0)?;
353    Ok(child)
354}
355
356fn leaf_min_bytes() -> usize {
357    (PAGE_SIZE - LEAF_DATA_OFFSET) * MIN_FILL_FACTOR / 100
358}
359
360fn interior_min_bytes() -> usize {
361    (PAGE_SIZE - INTERIOR_DATA_OFFSET) * MIN_FILL_FACTOR / 100
362}
363
364/// Bytes consumed by one leaf entry in the slotted layout: one u16
365/// slot in the pointer array plus the cell itself (`[key_len:u16]
366/// [val_len:u16][key][val]`).
367fn leaf_entry_size(entry: &(Vec<u8>, Vec<u8>)) -> usize {
368    2 + 4 + entry.0.len() + entry.1.len()
369}
370
371fn leaf_entries_size(entries: &[(Vec<u8>, Vec<u8>)]) -> usize {
372    entries.iter().map(leaf_entry_size).sum()
373}
374
375#[inline]
376fn leaf_slot_offset_for(index: usize) -> usize {
377    LEAF_SLOT_ARRAY_OFFSET + index * 2
378}
379
380#[inline]
381fn leaf_read_slot(page: &Page, index: usize) -> BTreeResult<usize> {
382    let data = page.as_bytes();
383    let slot_pos = leaf_slot_offset_for(index);
384    if slot_pos + 2 > PAGE_SIZE {
385        return Err(BTreeError::Corrupted("slot array overflows page".into()));
386    }
387    Ok(u16::from_le_bytes([data[slot_pos], data[slot_pos + 1]]) as usize)
388}
389
390#[inline]
391fn leaf_write_slot(page: &mut Page, index: usize, cell_offset: u16) -> BTreeResult<()> {
392    let data = page.as_bytes_mut();
393    let slot_pos = leaf_slot_offset_for(index);
394    if slot_pos + 2 > PAGE_SIZE {
395        return Err(BTreeError::Corrupted("slot array overflows page".into()));
396    }
397    data[slot_pos..slot_pos + 2].copy_from_slice(&cell_offset.to_le_bytes());
398    Ok(())
399}
400
401#[inline]
402fn leaf_slots_end(page: &Page) -> usize {
403    LEAF_SLOT_ARRAY_OFFSET + (page.cell_count() as usize) * 2
404}
405
406#[inline]
407fn leaf_cells_start(page: &Page) -> usize {
408    let end = page.free_end() as usize;
409    if end == 0 {
410        PAGE_SIZE
411    } else {
412        end
413    }
414}
415
416#[inline]
417fn leaf_free_bytes(page: &Page) -> usize {
418    let slot_end = leaf_slots_end(page);
419    let cells = leaf_cells_start(page);
420    cells.saturating_sub(slot_end)
421}
422
423fn interior_key_size(key: &[u8]) -> usize {
424    2 + key.len() + 4
425}
426
427fn interior_entries_size(keys: &[Vec<u8>]) -> usize {
428    keys.iter().map(|k| interior_key_size(k)).sum()
429}
430
431// ==================== Leaf Page Helpers ====================
432
433/// Read the cell at slot `index` in O(1). Follows the u16 slot pointer
434/// into the cell data area; the cell header is `[key_len:u16][val_len:u16]`
435/// followed by the raw key and value bytes.
436fn read_leaf_cell(page: &Page, index: usize) -> BTreeResult<(Vec<u8>, Vec<u8>)> {
437    let cell_count = page.cell_count() as usize;
438    if index >= cell_count {
439        return Err(BTreeError::Corrupted("Cell index out of range".into()));
440    }
441    let offset = leaf_read_slot(page, index)?;
442    let data = page.as_bytes();
443    if offset + 4 > PAGE_SIZE {
444        return Err(BTreeError::Corrupted("cell header out of range".into()));
445    }
446    let key_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
447    let val_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
448    let end = offset + 4 + key_len + val_len;
449    if end > PAGE_SIZE {
450        return Err(BTreeError::Corrupted("cell body out of range".into()));
451    }
452    let key = data[offset + 4..offset + 4 + key_len].to_vec();
453    let value = data[offset + 4 + key_len..end].to_vec();
454    Ok((key, value))
455}
456
457fn read_leaf_entries(page: &Page) -> BTreeResult<Vec<(Vec<u8>, Vec<u8>)>> {
458    let cell_count = page.cell_count() as usize;
459    let mut entries = Vec::with_capacity(cell_count);
460    for i in 0..cell_count {
461        entries.push(read_leaf_cell(page, i)?);
462    }
463    Ok(entries)
464}
465
466/// Wipe and re-lay an entire leaf in slotted form. Used by the split
467/// path (which hands us a pre-sorted `entries` vector) and by
468/// `clear_leaf_cells` under the hood. Entries are appended at the end
469/// of the free area so the highest-index slot ends up at the lowest
470/// offset — same shape the single-key insert path produces.
471fn write_leaf_entries(page: &mut Page, entries: &[(Vec<u8>, Vec<u8>)]) -> BTreeResult<()> {
472    clear_leaf_cells(page);
473    for (i, (k, v)) in entries.iter().enumerate() {
474        let cell_size = 4 + k.len() + v.len();
475        let cells_start = leaf_cells_start(page);
476        let slot_end = LEAF_SLOT_ARRAY_OFFSET + (i + 1) * 2;
477        if slot_end + cell_size > cells_start {
478            return Err(BTreeError::Corrupted("leaf rebuild overflowed page".into()));
479        }
480        let cell_offset = cells_start - cell_size;
481        {
482            let data = page.as_bytes_mut();
483            data[cell_offset..cell_offset + 2].copy_from_slice(&(k.len() as u16).to_le_bytes());
484            data[cell_offset + 2..cell_offset + 4].copy_from_slice(&(v.len() as u16).to_le_bytes());
485            data[cell_offset + 4..cell_offset + 4 + k.len()].copy_from_slice(k);
486            data[cell_offset + 4 + k.len()..cell_offset + 4 + k.len() + v.len()].copy_from_slice(v);
487        }
488        page.set_free_end(cell_offset as u16);
489        leaf_write_slot(page, i, cell_offset as u16)?;
490    }
491    page.set_cell_count(entries.len() as u16);
492    page.set_free_start((LEAF_SLOT_ARRAY_OFFSET + entries.len() * 2) as u16);
493    Ok(())
494}
495
496fn can_insert_leaf(page: &Page, key: &[u8], value: &[u8]) -> bool {
497    let needed = 2 + 4 + key.len() + value.len();
498    leaf_free_bytes(page) >= needed
499}
500
501/// Insert `(key, value)` into the slotted leaf in O(log M) search +
502/// O(M) slot-array memmove. Cell data is appended at the tail of the
503/// free area (backward from `free_end`); the slot pointer is inserted
504/// Overwrite the value portion of cell at `index` in-place. Valid when
505/// `new_value.len() <= existing_value.len()` — the caller
506/// (typically `BTree::upsert`) checks this before calling.
507///
508/// Same-length writes are trivial (byte copy). Shrinks update the cell's
509/// `v_len` header and leave the trailing bytes as dead space until the
510/// page is naturally rewritten; the slotted layout keeps cells
511/// addressable by their own offset+len so the gap is harmless.
512///
513/// Fixed-size column updates (SET score = X on a typed schema) hit this
514/// path, avoiding the delete + re-insert + rebalance cycle that
515/// dominates bulk_update.
516pub(crate) fn overwrite_leaf_value(
517    page: &mut Page,
518    index: usize,
519    new_value: &[u8],
520) -> BTreeResult<()> {
521    let cell_offset = leaf_read_slot(page, index)?;
522    let data = page.as_bytes();
523    if cell_offset + 4 > data.len() {
524        return Err(BTreeError::Corrupted(
525            "leaf cell header out of bounds".into(),
526        ));
527    }
528    let k_len = u16::from_le_bytes([data[cell_offset], data[cell_offset + 1]]) as usize;
529    let old_v_len = u16::from_le_bytes([data[cell_offset + 2], data[cell_offset + 3]]) as usize;
530    if new_value.len() > old_v_len {
531        return Err(BTreeError::Corrupted(
532            "overwrite_leaf_value: new value larger than slot".into(),
533        ));
534    }
535    let value_start = cell_offset + 4 + k_len;
536    if value_start + new_value.len() > data.len() {
537        return Err(BTreeError::Corrupted(
538            "leaf cell value out of bounds".into(),
539        ));
540    }
541    let data = page.as_bytes_mut();
542    // Update the v_len header if the value shrank. read_leaf_cell reads
543    // v_len from this header so the shortened value deserializes correctly.
544    if new_value.len() != old_v_len {
545        let new_len = new_value.len() as u16;
546        data[cell_offset + 2..cell_offset + 4].copy_from_slice(&new_len.to_le_bytes());
547    }
548    data[value_start..value_start + new_value.len()].copy_from_slice(new_value);
549    Ok(())
550}
551
552/// at the sorted position.
553fn insert_into_leaf(page: &mut Page, key: &[u8], value: &[u8]) -> BTreeResult<()> {
554    // 1. Binary search the slot array to find the insertion index.
555    let cell_count = page.cell_count() as usize;
556    let mut low = 0;
557    let mut high = cell_count;
558    while low < high {
559        let mid = (low + high) / 2;
560        let (cell_key, _) = read_leaf_cell(page, mid)?;
561        match cell_key.as_slice().cmp(key) {
562            Ordering::Less => low = mid + 1,
563            Ordering::Greater => high = mid,
564            Ordering::Equal => {
565                // Duplicate keys are tolerated by the B-tree (caller
566                // decides semantics); append after the existing run.
567                low = mid + 1;
568                break;
569            }
570        }
571    }
572    let insert_pos = low;
573
574    // 2. Reserve the cell at the tail of the free area.
575    let cell_size = 4 + key.len() + value.len();
576    let slot_end_after = LEAF_SLOT_ARRAY_OFFSET + (cell_count + 1) * 2;
577    let cells_start = leaf_cells_start(page);
578    if slot_end_after + cell_size > cells_start {
579        return Err(BTreeError::Corrupted("leaf page full".into()));
580    }
581    let cell_offset = cells_start - cell_size;
582    {
583        let data = page.as_bytes_mut();
584        data[cell_offset..cell_offset + 2].copy_from_slice(&(key.len() as u16).to_le_bytes());
585        data[cell_offset + 2..cell_offset + 4].copy_from_slice(&(value.len() as u16).to_le_bytes());
586        data[cell_offset + 4..cell_offset + 4 + key.len()].copy_from_slice(key);
587        data[cell_offset + 4 + key.len()..cell_offset + 4 + key.len() + value.len()]
588            .copy_from_slice(value);
589    }
590    page.set_free_end(cell_offset as u16);
591
592    // 3. Shift the slot-array tail right by one slot, then write the
593    //    new pointer into the freed slot. This is a single memmove on
594    //    a couple dozen u16s — far cheaper than the O(M²) rebuild.
595    {
596        let shift_from = leaf_slot_offset_for(insert_pos);
597        let shift_to = shift_from + 2;
598        let shift_len = (cell_count - insert_pos) * 2;
599        if shift_len > 0 {
600            let data = page.as_bytes_mut();
601            data.copy_within(shift_from..shift_from + shift_len, shift_to);
602        }
603    }
604    leaf_write_slot(page, insert_pos, cell_offset as u16)?;
605
606    // 4. Bump counters.
607    page.set_cell_count((cell_count + 1) as u16);
608    page.set_free_start((LEAF_SLOT_ARRAY_OFFSET + (cell_count + 1) * 2) as u16);
609    Ok(())
610}
611
612/// Remove the slot at `index`. Cell bytes are left in place (lazy
613/// compaction); the slot-array tail is shifted left to close the gap.
614/// The caller is expected to call `clear_leaf_cells` + rebuild if the
615/// page wants its free space reclaimed.
616fn delete_from_leaf(page: &mut Page, index: usize) -> BTreeResult<()> {
617    let cell_count = page.cell_count() as usize;
618    if index >= cell_count {
619        return Err(BTreeError::Corrupted("delete index out of range".into()));
620    }
621    if index + 1 < cell_count {
622        let shift_from = leaf_slot_offset_for(index + 1);
623        let shift_to = leaf_slot_offset_for(index);
624        let shift_len = (cell_count - index - 1) * 2;
625        let data = page.as_bytes_mut();
626        data.copy_within(shift_from..shift_from + shift_len, shift_to);
627    }
628    page.set_cell_count((cell_count - 1) as u16);
629    page.set_free_start((LEAF_SLOT_ARRAY_OFFSET + (cell_count - 1) * 2) as u16);
630    Ok(())
631}
632
633/// Reset the leaf to an empty slotted state — cell count 0, free
634/// cursors at the extremes. Zeroes the slot array + cell data area so
635/// stale bytes never leak through a corrupted read.
636fn clear_leaf_cells(page: &mut Page) {
637    {
638        let data = page.as_bytes_mut();
639        for byte in &mut data[LEAF_SLOT_ARRAY_OFFSET..] {
640            *byte = 0;
641        }
642    }
643    page.set_cell_count(0);
644    page.set_free_start(LEAF_SLOT_ARRAY_OFFSET as u16);
645    page.set_free_end(PAGE_SIZE as u16);
646}
647
648fn init_leaf_links(page: &mut Page, prev: u32, next: u32) {
649    let data = page.as_bytes_mut();
650    data[LEAF_PREV_OFFSET..LEAF_PREV_OFFSET + 4].copy_from_slice(&prev.to_le_bytes());
651    data[LEAF_NEXT_OFFSET..LEAF_NEXT_OFFSET + 4].copy_from_slice(&next.to_le_bytes());
652}
653
654fn read_next_leaf(page: &Page) -> u32 {
655    let data = page.as_bytes();
656    u32::from_le_bytes([
657        data[LEAF_NEXT_OFFSET],
658        data[LEAF_NEXT_OFFSET + 1],
659        data[LEAF_NEXT_OFFSET + 2],
660        data[LEAF_NEXT_OFFSET + 3],
661    ])
662}
663
664/// Returns the right-sibling page ID for a leaf page.
665/// 0 means this is the rightmost leaf — used by the D2 fastpath cache.
666#[inline]
667fn leaf_right_sibling(page: &Page) -> u32 {
668    read_next_leaf(page)
669}
670
671/// Returns the maximum (last) key stored in a leaf page, or `None` if empty.
672/// This is the Lehman-Yao "high_key" — any key > this value belongs on a
673/// right-sibling page. Reads only the last slot so it is O(1).
674fn leaf_high_key(page: &Page) -> BTreeResult<Option<Vec<u8>>> {
675    let cell_count = page.cell_count() as usize;
676    if cell_count == 0 {
677        return Ok(None);
678    }
679    let (key, _) = read_leaf_cell(page, cell_count - 1)?;
680    Ok(Some(key))
681}
682
683fn set_prev_leaf(page: &mut Page, prev: u32) {
684    let data = page.as_bytes_mut();
685    data[LEAF_PREV_OFFSET..LEAF_PREV_OFFSET + 4].copy_from_slice(&prev.to_le_bytes());
686}
687
688fn set_next_leaf(page: &mut Page, next: u32) {
689    let data = page.as_bytes_mut();
690    data[LEAF_NEXT_OFFSET..LEAF_NEXT_OFFSET + 4].copy_from_slice(&next.to_le_bytes());
691}
692
693// ==================== Interior Page Helpers ====================
694//
695// Slotted-page layout mirroring the leaf format:
696//
697// ```text
698// [PageHeader 32B]
699// [slot 0: u16][slot 1: u16]...[slot N-1: u16]  ← grows forward
700// ... free space ...
701// [cell N-1][cell N-2]...[cell 0]               ← grows backward from free_end
702// ```
703//
704// Each cell is `[key_len:u16][key][child:u32]`. The right-most child
705// pointer lives in `PageHeader.right_child` exactly as before.
706
707#[inline]
708fn interior_slot_offset_for(index: usize) -> usize {
709    INTERIOR_DATA_OFFSET + index * 2
710}
711
712#[inline]
713fn interior_read_slot(page: &Page, index: usize) -> BTreeResult<usize> {
714    let data = page.as_bytes();
715    let slot_pos = interior_slot_offset_for(index);
716    if slot_pos + 2 > PAGE_SIZE {
717        return Err(BTreeError::Corrupted(
718            "interior slot array overflows page".into(),
719        ));
720    }
721    Ok(u16::from_le_bytes([data[slot_pos], data[slot_pos + 1]]) as usize)
722}
723
724#[inline]
725fn interior_write_slot(page: &mut Page, index: usize, cell_offset: u16) -> BTreeResult<()> {
726    let data = page.as_bytes_mut();
727    let slot_pos = interior_slot_offset_for(index);
728    if slot_pos + 2 > PAGE_SIZE {
729        return Err(BTreeError::Corrupted(
730            "interior slot array overflows page".into(),
731        ));
732    }
733    data[slot_pos..slot_pos + 2].copy_from_slice(&cell_offset.to_le_bytes());
734    Ok(())
735}
736
737#[inline]
738fn interior_cells_start(page: &Page) -> usize {
739    let end = page.free_end() as usize;
740    if end == 0 {
741        PAGE_SIZE
742    } else {
743        end
744    }
745}
746
747#[inline]
748fn interior_free_bytes(page: &Page) -> usize {
749    let slot_end = INTERIOR_DATA_OFFSET + (page.cell_count() as usize) * 2;
750    interior_cells_start(page).saturating_sub(slot_end)
751}
752
753fn read_interior_cell(page: &Page, index: usize) -> BTreeResult<(Vec<u8>, u32)> {
754    let cell_count = page.cell_count() as usize;
755    if index >= cell_count {
756        return Err(BTreeError::Corrupted("Cell index out of range".into()));
757    }
758    let offset = interior_read_slot(page, index)?;
759    let data = page.as_bytes();
760    if offset + 2 > PAGE_SIZE {
761        return Err(BTreeError::Corrupted(
762            "interior cell header out of range".into(),
763        ));
764    }
765    let key_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
766    let end = offset + 2 + key_len + 4;
767    if end > PAGE_SIZE {
768        return Err(BTreeError::Corrupted(
769            "interior cell body out of range".into(),
770        ));
771    }
772    let key = data[offset + 2..offset + 2 + key_len].to_vec();
773    let child = u32::from_le_bytes([
774        data[offset + 2 + key_len],
775        data[offset + 2 + key_len + 1],
776        data[offset + 2 + key_len + 2],
777        data[offset + 2 + key_len + 3],
778    ]);
779    Ok((key, child))
780}
781
782fn read_interior_keys_children(page: &Page) -> BTreeResult<(Vec<Vec<u8>>, Vec<u32>)> {
783    let cell_count = page.cell_count() as usize;
784    let mut keys = Vec::with_capacity(cell_count);
785    let mut children = Vec::with_capacity(cell_count + 1);
786
787    for i in 0..cell_count {
788        let (key, child) = read_interior_cell(page, i)?;
789        keys.push(key);
790        children.push(child);
791    }
792
793    if cell_count == 0 {
794        let right_child = page.right_child();
795        if right_child != 0 {
796            children.push(right_child);
797        }
798    } else {
799        children.push(page.right_child());
800    }
801
802    Ok((keys, children))
803}
804
805/// Bulk-write an interior page from scratch in slotted form. Used by
806/// the split path. `keys` and `children` follow the B+ tree contract:
807/// `children.len() == keys.len() + 1`, with the last child landing in
808/// `PageHeader.right_child` (as before).
809fn write_interior_entries(page: &mut Page, keys: &[Vec<u8>], children: &[u32]) -> BTreeResult<()> {
810    if !keys.is_empty() && children.len() != keys.len() + 1 {
811        return Err(BTreeError::Corrupted(
812            "Interior keys/children length mismatch".into(),
813        ));
814    }
815
816    clear_interior_cells(page);
817    if keys.is_empty() {
818        let right_child = children.first().copied().unwrap_or(0);
819        page.set_right_child(right_child);
820        return Ok(());
821    }
822
823    for (i, key) in keys.iter().enumerate() {
824        let cell_size = 2 + key.len() + 4;
825        let cells_start = interior_cells_start(page);
826        let slot_end = INTERIOR_DATA_OFFSET + (i + 1) * 2;
827        if slot_end + cell_size > cells_start {
828            return Err(BTreeError::Corrupted(
829                "interior rebuild overflowed page".into(),
830            ));
831        }
832        let cell_offset = cells_start - cell_size;
833        {
834            let data = page.as_bytes_mut();
835            data[cell_offset..cell_offset + 2].copy_from_slice(&(key.len() as u16).to_le_bytes());
836            data[cell_offset + 2..cell_offset + 2 + key.len()].copy_from_slice(key);
837            data[cell_offset + 2 + key.len()..cell_offset + 2 + key.len() + 4]
838                .copy_from_slice(&children[i].to_le_bytes());
839        }
840        page.set_free_end(cell_offset as u16);
841        interior_write_slot(page, i, cell_offset as u16)?;
842    }
843    page.set_cell_count(keys.len() as u16);
844    page.set_free_start((INTERIOR_DATA_OFFSET + keys.len() * 2) as u16);
845    page.set_right_child(*children.last().ok_or_else(|| {
846        BTreeError::Corrupted("write_interior_entries: children empty with non-empty keys".into())
847    })?);
848    Ok(())
849}
850
851fn can_insert_interior(page: &Page, key: &[u8]) -> bool {
852    let needed = 2 + 2 + key.len() + 4;
853    interior_free_bytes(page) >= needed
854}
855
856/// Insert a `(key, left_child)` separator into the interior page.
857/// `right_child` replaces whatever child used to sit to the right of
858/// the inserted key (either a middle child's pointer or the page's
859/// `right_child` when the key is the new maximum).
860fn insert_into_interior(
861    page: &mut Page,
862    key: &[u8],
863    left_child: u32,
864    right_child: u32,
865) -> BTreeResult<()> {
866    // 1. Binary search the slot array for the insert position.
867    let cell_count = page.cell_count() as usize;
868    let mut low = 0;
869    let mut high = cell_count;
870    while low < high {
871        let mid = (low + high) / 2;
872        let (cell_key, _) = read_interior_cell(page, mid)?;
873        match cell_key.as_slice().cmp(key) {
874            Ordering::Less => low = mid + 1,
875            Ordering::Greater | Ordering::Equal => high = mid,
876        }
877    }
878    let insert_pos = low;
879
880    // 2. Redirect the previous owner of the split to `right_child`.
881    //    If the insertion is at the tail, the previous owner was the
882    //    page's right_child pointer; otherwise it was the child slot
883    //    of the cell currently at `insert_pos`.
884    if insert_pos < cell_count {
885        let old_offset = interior_read_slot(page, insert_pos)?;
886        let data = page.as_bytes();
887        let key_len = u16::from_le_bytes([data[old_offset], data[old_offset + 1]]) as usize;
888        let child_pos = old_offset + 2 + key_len;
889        let data = page.as_bytes_mut();
890        data[child_pos..child_pos + 4].copy_from_slice(&right_child.to_le_bytes());
891    } else {
892        page.set_right_child(right_child);
893    }
894
895    // 3. Reserve the new cell at the tail of the free area.
896    let cell_size = 2 + key.len() + 4;
897    let slot_end_after = INTERIOR_DATA_OFFSET + (cell_count + 1) * 2;
898    let cells_start = interior_cells_start(page);
899    if slot_end_after + cell_size > cells_start {
900        return Err(BTreeError::Corrupted("interior page full".into()));
901    }
902    let cell_offset = cells_start - cell_size;
903    {
904        let data = page.as_bytes_mut();
905        data[cell_offset..cell_offset + 2].copy_from_slice(&(key.len() as u16).to_le_bytes());
906        data[cell_offset + 2..cell_offset + 2 + key.len()].copy_from_slice(key);
907        data[cell_offset + 2 + key.len()..cell_offset + 2 + key.len() + 4]
908            .copy_from_slice(&left_child.to_le_bytes());
909    }
910    page.set_free_end(cell_offset as u16);
911
912    // 4. Shift the slot-array tail right by one slot, write the new
913    //    pointer into the freed slot, bump counters.
914    {
915        let shift_from = interior_slot_offset_for(insert_pos);
916        let shift_to = shift_from + 2;
917        let shift_len = (cell_count - insert_pos) * 2;
918        if shift_len > 0 {
919            let data = page.as_bytes_mut();
920            data.copy_within(shift_from..shift_from + shift_len, shift_to);
921        }
922    }
923    interior_write_slot(page, insert_pos, cell_offset as u16)?;
924    page.set_cell_count((cell_count + 1) as u16);
925    page.set_free_start((INTERIOR_DATA_OFFSET + (cell_count + 1) * 2) as u16);
926    Ok(())
927}
928
929fn clear_interior_cells(page: &mut Page) {
930    {
931        let data = page.as_bytes_mut();
932        for byte in &mut data[INTERIOR_DATA_OFFSET..] {
933            *byte = 0;
934        }
935    }
936    page.set_cell_count(0);
937    page.set_free_start(INTERIOR_DATA_OFFSET as u16);
938    page.set_free_end(PAGE_SIZE as u16);
939}