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// A split must still fit two maximum-key inline cells, including the value
82// flag, cell headers and slot pointers, after the native page-size change.
83const _: () = assert!(
84    2 * (MAX_KEY_SIZE + value_layout::OVERFLOW_THRESHOLD + 1 + 4 + 2)
85        <= PAGE_SIZE - LEAF_SLOT_ARRAY_OFFSET
86);
87
88/// Kept for source-compat with older code paths (e.g. interior-node
89/// helpers); equivalent to `LEAF_SLOT_ARRAY_OFFSET` for leaves.
90const LEAF_DATA_OFFSET: usize = LEAF_SLOT_ARRAY_OFFSET;
91
92/// Start of cell data in interior pages (right_child is in header)
93const INTERIOR_DATA_OFFSET: usize = HEADER_SIZE;
94
95/// B+ Tree error types
96#[derive(Debug, Clone)]
97pub enum BTreeError {
98    /// Key not found
99    NotFound,
100    /// Key already exists
101    DuplicateKey,
102    /// Key too large
103    KeyTooLarge(usize),
104    /// Value too large
105    ValueTooLarge(usize),
106    /// Tree is corrupted
107    Corrupted(String),
108    /// Pager error
109    Pager(String),
110    /// Internal lock was poisoned by a panicked thread
111    LockPoisoned(String),
112    /// Large-value layout encode/decode failed (overflow chain or LZ4).
113    ValueLayout(String),
114    /// Materializing a value would exceed the caller's read memory allowance.
115    ReadLimitExceeded {
116        required_bytes: usize,
117        limit_bytes: usize,
118    },
119}
120
121impl std::fmt::Display for BTreeError {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        match self {
124            Self::NotFound => write!(f, "Key not found"),
125            Self::DuplicateKey => write!(f, "Key already exists"),
126            Self::KeyTooLarge(size) => {
127                write!(f, "Key too large: {} bytes (max {})", size, MAX_KEY_SIZE)
128            }
129            Self::ValueTooLarge(size) => write!(
130                f,
131                "Value too large: {} bytes (max {})",
132                size, MAX_VALUE_SIZE
133            ),
134            Self::Corrupted(msg) => write!(f, "B-tree corrupted: {}", msg),
135            Self::Pager(msg) => write!(f, "Pager error: {}", msg),
136            Self::LockPoisoned(msg) => write!(f, "Lock poisoned: {}", msg),
137            Self::ValueLayout(msg) => write!(f, "Value layout: {}", msg),
138            Self::ReadLimitExceeded {
139                required_bytes,
140                limit_bytes,
141            } => write!(
142                f,
143                "B-tree read needs {required_bytes} bytes over read memory limit {limit_bytes}"
144            ),
145        }
146    }
147}
148
149impl From<value_layout::ValueLayoutError> for BTreeError {
150    fn from(e: value_layout::ValueLayoutError) -> Self {
151        match e {
152            value_layout::ValueLayoutError::ValueTooLarge(n) => Self::ValueTooLarge(n),
153            value_layout::ValueLayoutError::ReadLimitExceeded {
154                required_bytes,
155                limit_bytes,
156            } => Self::ReadLimitExceeded {
157                required_bytes,
158                limit_bytes,
159            },
160            other => Self::ValueLayout(other.to_string()),
161        }
162    }
163}
164
165impl std::error::Error for BTreeError {}
166
167impl From<PagerError> for BTreeError {
168    fn from(e: PagerError) -> Self {
169        Self::Pager(e.to_string())
170    }
171}
172
173impl From<super::page::PageError> for BTreeError {
174    fn from(e: super::page::PageError) -> Self {
175        Self::Corrupted(e.to_string())
176    }
177}
178
179/// Result type for B+ tree operations
180pub type BTreeResult<T> = Result<T, BTreeError>;
181
182/// B+ Tree cursor for iteration
183pub struct BTreeCursor<P: PageStore> {
184    /// A cursor can outlive the tree handle that created it.
185    _retirement_pin: Arc<()>,
186    /// Current leaf page ID
187    leaf_page_id: u32,
188    /// Current position within leaf
189    position: usize,
190    /// Pager reference
191    pager: Arc<P>,
192    /// A6 — true once we've issued a prefetch hint for the next leaf on
193    /// this leaf. Reset to false every time we move to a new leaf so the
194    /// hint fires once per leaf, at the halfway point.
195    prefetched_next: bool,
196    /// Published collection generations are immutable. Retain at most one
197    /// leaf while walking them; mutable-tree cursors keep re-reading pages.
198    immutable: bool,
199    immutable_leaf: Option<Box<Page>>,
200}
201
202impl<P: PageStore> BTreeCursor<P> {
203    /// Copy the position of an immutable cursor without advancing the original.
204    pub(crate) fn fork_immutable(&self) -> Self {
205        assert!(self.immutable, "only immutable cursors may be forked");
206        Self {
207            _retirement_pin: Arc::clone(&self._retirement_pin),
208            leaf_page_id: self.leaf_page_id,
209            position: self.position,
210            pager: Arc::clone(&self.pager),
211            prefetched_next: self.prefetched_next,
212            immutable: true,
213            immutable_leaf: self.immutable_leaf.clone(),
214        }
215    }
216
217    /// Only collection readers whose host pins an immutable published root
218    /// may retain a leaf across calls. This is not a mutable-tree snapshot.
219    pub(crate) fn with_immutable_leaves(mut self) -> Self {
220        self.immutable = true;
221        self
222    }
223
224    fn prepare_immutable_leaf(&mut self) -> BTreeResult<bool> {
225        if self.leaf_page_id == 0 {
226            return Ok(false);
227        }
228        if self.immutable_leaf.is_none() {
229            let page = self.pager.read_page(self.leaf_page_id)?;
230            if page.page_type()? != PageType::BTreeLeaf {
231                return Err(BTreeError::Corrupted(
232                    "cursor reached a non-leaf page".into(),
233                ));
234            }
235            self.immutable_leaf = Some(Box::new(page));
236        }
237        let page = self.immutable_leaf.as_ref().expect("loaded immutable leaf");
238        if self.position < usize::from(page.cell_count()) {
239            return Ok(true);
240        }
241        let next = read_next_leaf(page);
242        if next == 0 {
243            self.leaf_page_id = 0;
244            self.immutable_leaf = None;
245            return Ok(false);
246        }
247        let next_page = self.pager.read_page(next)?;
248        if next_page.page_type()? != PageType::BTreeLeaf || next_page.cell_count() == 0 {
249            return Err(BTreeError::Corrupted(
250                "leaf chain reached an invalid leaf".into(),
251            ));
252        }
253        let first_key = read_leaf_cell_borrowed(&next_page, 0)?.0;
254        if leaf_high_key(page)?
255            .as_ref()
256            .is_some_and(|key| first_key <= key.as_slice())
257        {
258            return Err(BTreeError::Corrupted(
259                "leaf chain does not advance its key range".into(),
260            ));
261        }
262        **self.immutable_leaf.as_mut().expect("loaded immutable leaf") = next_page;
263        self.leaf_page_id = next;
264        self.position = 0;
265        self.prefetched_next = false;
266        Ok(true)
267    }
268
269    /// Inspect the next key without materializing its value or advancing.
270    pub(crate) fn peek_key_with<T>(
271        &mut self,
272        visit: impl FnOnce(&[u8]) -> T,
273    ) -> BTreeResult<Option<T>> {
274        if self.immutable {
275            if !self.prepare_immutable_leaf()? {
276                return Ok(None);
277            }
278            let page = self.immutable_leaf.as_ref().expect("loaded immutable leaf");
279            return Ok(Some(visit(read_leaf_cell_borrowed(page, self.position)?.0)));
280        }
281        let Some(page) = self.prepare_next_leaf()? else {
282            return Ok(None);
283        };
284        Ok(Some(visit(
285            read_leaf_cell_borrowed(&page, self.position)?.0,
286        )))
287    }
288
289    /// Inspect the next key without materializing its value or advancing.
290    pub fn peek_key(&mut self) -> BTreeResult<Option<Vec<u8>>> {
291        if self.immutable {
292            if !self.prepare_immutable_leaf()? {
293                return Ok(None);
294            }
295            let page = self.immutable_leaf.as_ref().expect("loaded immutable leaf");
296            return Ok(Some(
297                read_leaf_cell_borrowed(page, self.position)?.0.to_vec(),
298            ));
299        }
300        let Some(page) = self.prepare_next_leaf()? else {
301            return Ok(None);
302        };
303        Ok(Some(read_leaf_cell(&page, self.position)?.0))
304    }
305
306    /// Skip a shadowed or deleted base record without reading its overflow value.
307    pub fn skip_next(&mut self) -> BTreeResult<bool> {
308        if self.immutable {
309            if !self.prepare_immutable_leaf()? {
310                return Ok(false);
311            }
312            self.position += 1;
313            return Ok(true);
314        }
315        if self.prepare_next_leaf()?.is_none() {
316            return Ok(false);
317        }
318        self.position += 1;
319        Ok(true)
320    }
321
322    fn prepare_next_leaf(&mut self) -> BTreeResult<Option<Page>> {
323        if self.leaf_page_id == 0 {
324            return Ok(None);
325        }
326        let page = self.pager.read_page(self.leaf_page_id)?;
327        if page.page_type()? != PageType::BTreeLeaf {
328            return Err(BTreeError::Corrupted(
329                "cursor reached a non-leaf page".into(),
330            ));
331        }
332        if self.position < usize::from(page.cell_count()) {
333            return Ok(Some(page));
334        }
335        let next = read_next_leaf(&page);
336        if next == 0 {
337            self.leaf_page_id = 0;
338            return Ok(None);
339        }
340        let next_page = self.pager.read_page(next)?;
341        if next_page.page_type()? != PageType::BTreeLeaf || next_page.cell_count() == 0 {
342            return Err(BTreeError::Corrupted(
343                "leaf chain reached an invalid leaf".into(),
344            ));
345        }
346        let first_key = read_leaf_cell(&next_page, 0)?.0;
347        if leaf_high_key(&page)?
348            .as_ref()
349            .is_some_and(|key| first_key <= *key)
350        {
351            return Err(BTreeError::Corrupted(
352                "leaf chain does not advance its key range".into(),
353            ));
354        }
355        self.leaf_page_id = next;
356        self.position = 0;
357        self.prefetched_next = false;
358        Ok(Some(next_page))
359    }
360
361    /// Read one value within the caller's materialization allowance.
362    pub fn next_with_budget(
363        &mut self,
364        limit_bytes: usize,
365    ) -> BTreeResult<Option<(Vec<u8>, Vec<u8>)>> {
366        self.next_impl(limit_bytes)
367    }
368
369    /// Move to next entry
370    pub fn next(&mut self) -> BTreeResult<Option<(Vec<u8>, Vec<u8>)>> {
371        self.next_impl(usize::MAX)
372    }
373
374    fn next_impl(&mut self, limit_bytes: usize) -> BTreeResult<Option<(Vec<u8>, Vec<u8>)>> {
375        if self.immutable {
376            return self.next_immutable(limit_bytes);
377        }
378        if self.leaf_page_id == 0 {
379            return Ok(None);
380        }
381
382        let page = self.pager.read_page(self.leaf_page_id)?;
383        if page.page_type()? != PageType::BTreeLeaf {
384            return Err(BTreeError::Corrupted(
385                "cursor reached a non-leaf page".into(),
386            ));
387        }
388        let cell_count = page.cell_count() as usize;
389
390        // A6 — prefetch the next leaf when we cross the halfway point of
391        // the current one. The hint fires at most once per leaf (guarded
392        // by `prefetched_next`) so the syscall overhead is amortized over
393        // ~half a leaf's worth of entries. The kernel uses the hint to
394        // start DMA-ing the next page while we finish the current one.
395        if !self.prefetched_next && self.position >= cell_count / 2 && cell_count > 0 {
396            let next_leaf = read_next_leaf(&page);
397            if next_leaf != 0 {
398                self.pager.prefetch_hint(next_leaf);
399            }
400            self.prefetched_next = true;
401        }
402
403        // Check if we have more cells in current page
404        if self.position < cell_count {
405            let (key, stored) = read_leaf_cell(&page, self.position)?;
406            let value =
407                value_layout::decode_with_budget(self.pager.as_ref(), &stored, limit_bytes)?;
408            self.position += 1;
409            return Ok(Some((key, value)));
410        }
411
412        // Move to next leaf
413        let next_leaf = read_next_leaf(&page);
414        if next_leaf == 0 {
415            self.leaf_page_id = 0;
416            return Ok(None);
417        }
418        let previous_high_key = leaf_high_key(&page)?;
419
420        self.leaf_page_id = next_leaf;
421        self.position = 0;
422        self.prefetched_next = false; // reset for the new leaf
423
424        // Read from new leaf
425        let page = self.pager.read_page(self.leaf_page_id)?;
426        if page.page_type()? != PageType::BTreeLeaf {
427            return Err(BTreeError::Corrupted(
428                "leaf chain reached a non-leaf page".into(),
429            ));
430        }
431        if page.cell_count() == 0 {
432            return Ok(None);
433        }
434
435        let (key, stored) = read_leaf_cell(&page, 0)?;
436        // Every leaf transition must advance the key range. This bounds a
437        // corrupt cyclic chain without growing a visited-page set or an
438        // unbounded checkpoint buffer (#2376).
439        if previous_high_key
440            .as_ref()
441            .is_some_and(|previous| key <= *previous)
442        {
443            self.leaf_page_id = 0;
444            return Err(BTreeError::Corrupted(
445                "leaf chain does not advance its key range".into(),
446            ));
447        }
448        let value = value_layout::decode_with_budget(self.pager.as_ref(), &stored, limit_bytes)?;
449        self.position = 1;
450        Ok(Some((key, value)))
451    }
452
453    fn next_immutable(&mut self, limit_bytes: usize) -> BTreeResult<Option<(Vec<u8>, Vec<u8>)>> {
454        if !self.prepare_immutable_leaf()? {
455            return Ok(None);
456        }
457        let page = self.immutable_leaf.as_ref().expect("loaded immutable leaf");
458        if !self.prefetched_next && self.position >= usize::from(page.cell_count()) / 2 {
459            let next = read_next_leaf(page);
460            if next != 0 {
461                self.pager.prefetch_hint(next);
462            }
463            self.prefetched_next = true;
464        }
465        let (key, stored) = read_leaf_cell_borrowed(page, self.position)?;
466        let value = value_layout::decode_with_budget(self.pager.as_ref(), stored, limit_bytes)?;
467        self.position += 1;
468        Ok(Some((key.to_vec(), value)))
469    }
470
471    /// Peek at current entry without advancing
472    pub fn peek(&self) -> BTreeResult<Option<(Vec<u8>, Vec<u8>)>> {
473        if self.leaf_page_id == 0 {
474            return Ok(None);
475        }
476
477        let page = self.pager.read_page(self.leaf_page_id)?;
478        let cell_count = page.cell_count() as usize;
479
480        if self.position >= cell_count {
481            return Ok(None);
482        }
483
484        let (key, stored) = read_leaf_cell(&page, self.position)?;
485        let value = value_layout::decode(self.pager.as_ref(), &stored)?;
486        Ok(Some((key, value)))
487    }
488}
489
490/// B+ Tree implementation
491pub struct BTree<P: PageStore> {
492    retirement_pin: Arc<()>,
493    /// Pager for page I/O
494    pager: Arc<P>,
495    /// Root page ID (0 = empty tree)
496    root_page_id: RwLock<u32>,
497    /// D2 fastpath: cached rightmost leaf (page_id, high_key).
498    ///
499    /// When the next key to insert is strictly greater than `high_key`,
500    /// skip `find_leaf` (tree descent from root) and go directly to the
501    /// cached page. Invalidated on splits and deletes.
502    ///
503    /// Mirrors `BTPageCacheIsValid` / `BTREE_FASTPATH_MIN_LEVEL` in
504    /// `nbtinsert.c:31`.
505    rightmost_leaf: RwLock<Option<(u32, Vec<u8>)>>,
506    /// Serialises structural writers (insert / upsert / delete / bulk).
507    ///
508    /// Writers copy a page, modify the copy and write it back, and there
509    /// are no page latches, so two writers on one tree would lose each
510    /// other's leaf updates or tear a split (#2376). Reentrant because the
511    /// public mutators call each other (`upsert` -> `insert`, the bulk
512    /// paths -> `insert_encoded`). Readers do not take it.
513    writer: parking_lot::ReentrantMutex<()>,
514}
515
516#[path = "btree/impl.rs"]
517mod btree_impl;
518
519#[path = "btree/reclaim.rs"]
520mod reclaim;
521
522#[cfg(test)]
523#[path = "btree/read_budget_tests.rs"]
524mod read_budget_tests;
525
526#[path = "btree/lehman_yao.rs"]
527pub mod lehman_yao;
528
529#[path = "btree/value_layout.rs"]
530pub mod value_layout;
531// ==================== Search Helpers ====================
532
533enum SearchResult {
534    Found(usize),
535    NotFound(usize),
536}
537
538fn search_leaf(page: &Page, key: &[u8]) -> BTreeResult<SearchResult> {
539    let cell_count = page.cell_count() as usize;
540
541    // Binary search
542    let mut low = 0;
543    let mut high = cell_count;
544
545    while low < high {
546        let mid = (low + high) / 2;
547        let (cell_key, _) = read_leaf_cell(page, mid)?;
548
549        match cell_key.as_slice().cmp(key) {
550            Ordering::Less => low = mid + 1,
551            Ordering::Greater => high = mid,
552            Ordering::Equal => return Ok(SearchResult::Found(mid)),
553        }
554    }
555
556    Ok(SearchResult::NotFound(low))
557}
558
559fn find_child(page: &Page, key: &[u8]) -> BTreeResult<u32> {
560    let cell_count = page.cell_count() as usize;
561
562    // Binary search for the correct child
563    for i in 0..cell_count {
564        let (cell_key, child) = read_interior_cell(page, i)?;
565        if key < cell_key.as_slice() {
566            return Ok(child);
567        }
568    }
569
570    // Key is >= all keys, use right child
571    Ok(page.right_child())
572}
573
574fn find_first_child(page: &Page) -> BTreeResult<u32> {
575    if page.cell_count() == 0 {
576        return Ok(page.right_child());
577    }
578    let (_, child) = read_interior_cell(page, 0)?;
579    Ok(child)
580}
581
582fn leaf_min_bytes() -> usize {
583    (PAGE_SIZE - LEAF_DATA_OFFSET) * MIN_FILL_FACTOR / 100
584}
585
586fn interior_min_bytes() -> usize {
587    (PAGE_SIZE - INTERIOR_DATA_OFFSET) * MIN_FILL_FACTOR / 100
588}
589
590/// Bytes consumed by one leaf entry in the slotted layout: one u16
591/// slot in the pointer array plus the cell itself (`[key_len:u16]
592/// [val_len:u16][key][val]`).
593fn leaf_entry_size(entry: &(Vec<u8>, Vec<u8>)) -> usize {
594    2 + 4 + entry.0.len() + entry.1.len()
595}
596
597fn leaf_entries_size(entries: &[(Vec<u8>, Vec<u8>)]) -> usize {
598    entries.iter().map(leaf_entry_size).sum()
599}
600
601#[inline]
602fn leaf_slot_offset_for(index: usize) -> usize {
603    LEAF_SLOT_ARRAY_OFFSET + index * 2
604}
605
606#[inline]
607fn leaf_read_slot(page: &Page, index: usize) -> BTreeResult<usize> {
608    let data = page.as_bytes();
609    let slot_pos = leaf_slot_offset_for(index);
610    if slot_pos + 2 > PAGE_SIZE {
611        return Err(BTreeError::Corrupted("slot array overflows page".into()));
612    }
613    Ok(u16::from_le_bytes([data[slot_pos], data[slot_pos + 1]]) as usize)
614}
615
616#[inline]
617fn leaf_write_slot(page: &mut Page, index: usize, cell_offset: u16) -> BTreeResult<()> {
618    let data = page.as_bytes_mut();
619    let slot_pos = leaf_slot_offset_for(index);
620    if slot_pos + 2 > PAGE_SIZE {
621        return Err(BTreeError::Corrupted("slot array overflows page".into()));
622    }
623    data[slot_pos..slot_pos + 2].copy_from_slice(&cell_offset.to_le_bytes());
624    Ok(())
625}
626
627#[inline]
628fn leaf_slots_end(page: &Page) -> usize {
629    LEAF_SLOT_ARRAY_OFFSET + (page.cell_count() as usize) * 2
630}
631
632#[inline]
633fn leaf_cells_start(page: &Page) -> usize {
634    let end = page.free_end() as usize;
635    if end == 0 {
636        PAGE_SIZE
637    } else {
638        end
639    }
640}
641
642#[inline]
643fn leaf_free_bytes(page: &Page) -> usize {
644    let slot_end = leaf_slots_end(page);
645    let cells = leaf_cells_start(page);
646    cells.saturating_sub(slot_end)
647}
648
649fn interior_key_size(key: &[u8]) -> usize {
650    2 + key.len() + 4
651}
652
653fn interior_entries_size(keys: &[Vec<u8>]) -> usize {
654    keys.iter().map(|k| interior_key_size(k)).sum()
655}
656
657// ==================== Leaf Page Helpers ====================
658
659/// Read the cell at slot `index` in O(1). Follows the u16 slot pointer
660/// into the cell data area; the cell header is `[key_len:u16][val_len:u16]`
661/// followed by the raw key and value bytes.
662fn read_leaf_cell(page: &Page, index: usize) -> BTreeResult<(Vec<u8>, Vec<u8>)> {
663    let (key, value) = read_leaf_cell_borrowed(page, index)?;
664    Ok((key.to_vec(), value.to_vec()))
665}
666
667fn read_leaf_cell_borrowed(page: &Page, index: usize) -> BTreeResult<(&[u8], &[u8])> {
668    let cell_count = page.cell_count() as usize;
669    if index >= cell_count {
670        return Err(BTreeError::Corrupted("Cell index out of range".into()));
671    }
672    let offset = leaf_read_slot(page, index)?;
673    let data = page.as_bytes();
674    if offset + 4 > PAGE_SIZE {
675        return Err(BTreeError::Corrupted("cell header out of range".into()));
676    }
677    let key_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
678    let val_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
679    let end = offset + 4 + key_len + val_len;
680    if end > PAGE_SIZE {
681        return Err(BTreeError::Corrupted("cell body out of range".into()));
682    }
683    let key = &data[offset + 4..offset + 4 + key_len];
684    let value = &data[offset + 4 + key_len..end];
685    Ok((key, value))
686}
687
688fn read_leaf_entries(page: &Page) -> BTreeResult<Vec<(Vec<u8>, Vec<u8>)>> {
689    let cell_count = page.cell_count() as usize;
690    let mut entries = Vec::with_capacity(cell_count);
691    for i in 0..cell_count {
692        entries.push(read_leaf_cell(page, i)?);
693    }
694    Ok(entries)
695}
696
697/// Wipe and re-lay an entire leaf in slotted form. Used by the split
698/// path (which hands us a pre-sorted `entries` vector) and by
699/// `clear_leaf_cells` under the hood. Entries are appended at the end
700/// of the free area so the highest-index slot ends up at the lowest
701/// offset — same shape the single-key insert path produces.
702fn write_leaf_entries(page: &mut Page, entries: &[(Vec<u8>, Vec<u8>)]) -> BTreeResult<()> {
703    clear_leaf_cells(page);
704    for (i, (k, v)) in entries.iter().enumerate() {
705        let cell_size = 4 + k.len() + v.len();
706        let cells_start = leaf_cells_start(page);
707        let slot_end = LEAF_SLOT_ARRAY_OFFSET + (i + 1) * 2;
708        if slot_end + cell_size > cells_start {
709            return Err(BTreeError::Corrupted("leaf rebuild overflowed page".into()));
710        }
711        let cell_offset = cells_start - cell_size;
712        {
713            let data = page.as_bytes_mut();
714            data[cell_offset..cell_offset + 2].copy_from_slice(&(k.len() as u16).to_le_bytes());
715            data[cell_offset + 2..cell_offset + 4].copy_from_slice(&(v.len() as u16).to_le_bytes());
716            data[cell_offset + 4..cell_offset + 4 + k.len()].copy_from_slice(k);
717            data[cell_offset + 4 + k.len()..cell_offset + 4 + k.len() + v.len()].copy_from_slice(v);
718        }
719        page.set_free_end(cell_offset as u16);
720        leaf_write_slot(page, i, cell_offset as u16)?;
721    }
722    page.set_cell_count(entries.len() as u16);
723    page.set_free_start((LEAF_SLOT_ARRAY_OFFSET + entries.len() * 2) as u16);
724    Ok(())
725}
726
727fn can_insert_leaf(page: &Page, key: &[u8], value: &[u8]) -> bool {
728    let needed = 2 + 4 + key.len() + value.len();
729    leaf_free_bytes(page) >= needed
730}
731
732/// Insert `(key, value)` into the slotted leaf in O(log M) search +
733/// O(M) slot-array memmove. Cell data is appended at the tail of the
734/// free area (backward from `free_end`); the slot pointer is inserted
735/// Overwrite the value portion of cell at `index` in-place. Valid when
736/// `new_value.len() <= existing_value.len()` — the caller
737/// (typically `BTree::upsert`) checks this before calling.
738///
739/// Same-length writes are trivial (byte copy). Shrinks update the cell's
740/// `v_len` header and leave the trailing bytes as dead space until the
741/// page is naturally rewritten; the slotted layout keeps cells
742/// addressable by their own offset+len so the gap is harmless.
743///
744/// Fixed-size column updates (SET score = X on a typed schema) hit this
745/// path, avoiding the delete + re-insert + rebalance cycle that
746/// dominates bulk_update.
747pub(crate) fn overwrite_leaf_value(
748    page: &mut Page,
749    index: usize,
750    new_value: &[u8],
751) -> BTreeResult<()> {
752    let cell_offset = leaf_read_slot(page, index)?;
753    let data = page.as_bytes();
754    if cell_offset + 4 > data.len() {
755        return Err(BTreeError::Corrupted(
756            "leaf cell header out of bounds".into(),
757        ));
758    }
759    let k_len = u16::from_le_bytes([data[cell_offset], data[cell_offset + 1]]) as usize;
760    let old_v_len = u16::from_le_bytes([data[cell_offset + 2], data[cell_offset + 3]]) as usize;
761    if new_value.len() > old_v_len {
762        return Err(BTreeError::Corrupted(
763            "overwrite_leaf_value: new value larger than slot".into(),
764        ));
765    }
766    let value_start = cell_offset + 4 + k_len;
767    if value_start + new_value.len() > data.len() {
768        return Err(BTreeError::Corrupted(
769            "leaf cell value out of bounds".into(),
770        ));
771    }
772    let data = page.as_bytes_mut();
773    // Update the v_len header if the value shrank. read_leaf_cell reads
774    // v_len from this header so the shortened value deserializes correctly.
775    if new_value.len() != old_v_len {
776        let new_len = new_value.len() as u16;
777        data[cell_offset + 2..cell_offset + 4].copy_from_slice(&new_len.to_le_bytes());
778    }
779    data[value_start..value_start + new_value.len()].copy_from_slice(new_value);
780    Ok(())
781}
782
783/// at the sorted position.
784fn insert_into_leaf(page: &mut Page, key: &[u8], value: &[u8]) -> BTreeResult<()> {
785    // 1. Binary search the slot array to find the insertion index.
786    let cell_count = page.cell_count() as usize;
787    let mut low = 0;
788    let mut high = cell_count;
789    while low < high {
790        let mid = (low + high) / 2;
791        let (cell_key, _) = read_leaf_cell(page, mid)?;
792        match cell_key.as_slice().cmp(key) {
793            Ordering::Less => low = mid + 1,
794            Ordering::Greater => high = mid,
795            Ordering::Equal => {
796                // Duplicate keys are tolerated by the B-tree (caller
797                // decides semantics); append after the existing run.
798                low = mid + 1;
799                break;
800            }
801        }
802    }
803    let insert_pos = low;
804
805    // 2. Reserve the cell at the tail of the free area.
806    let cell_size = 4 + key.len() + value.len();
807    let slot_end_after = LEAF_SLOT_ARRAY_OFFSET + (cell_count + 1) * 2;
808    let cells_start = leaf_cells_start(page);
809    if slot_end_after + cell_size > cells_start {
810        return Err(BTreeError::Corrupted("leaf page full".into()));
811    }
812    let cell_offset = cells_start - cell_size;
813    {
814        let data = page.as_bytes_mut();
815        data[cell_offset..cell_offset + 2].copy_from_slice(&(key.len() as u16).to_le_bytes());
816        data[cell_offset + 2..cell_offset + 4].copy_from_slice(&(value.len() as u16).to_le_bytes());
817        data[cell_offset + 4..cell_offset + 4 + key.len()].copy_from_slice(key);
818        data[cell_offset + 4 + key.len()..cell_offset + 4 + key.len() + value.len()]
819            .copy_from_slice(value);
820    }
821    page.set_free_end(cell_offset as u16);
822
823    // 3. Shift the slot-array tail right by one slot, then write the
824    //    new pointer into the freed slot. This is a single memmove on
825    //    a couple dozen u16s — far cheaper than the O(M²) rebuild.
826    {
827        let shift_from = leaf_slot_offset_for(insert_pos);
828        let shift_to = shift_from + 2;
829        let shift_len = (cell_count - insert_pos) * 2;
830        if shift_len > 0 {
831            let data = page.as_bytes_mut();
832            data.copy_within(shift_from..shift_from + shift_len, shift_to);
833        }
834    }
835    leaf_write_slot(page, insert_pos, cell_offset as u16)?;
836
837    // 4. Bump counters.
838    page.set_cell_count((cell_count + 1) as u16);
839    page.set_free_start((LEAF_SLOT_ARRAY_OFFSET + (cell_count + 1) * 2) as u16);
840    Ok(())
841}
842
843/// Remove the slot at `index`. Cell bytes are left in place (lazy
844/// compaction); the slot-array tail is shifted left to close the gap.
845/// The caller is expected to call `clear_leaf_cells` + rebuild if the
846/// page wants its free space reclaimed.
847fn delete_from_leaf(page: &mut Page, index: usize) -> BTreeResult<()> {
848    let cell_count = page.cell_count() as usize;
849    if index >= cell_count {
850        return Err(BTreeError::Corrupted("delete index out of range".into()));
851    }
852    if index + 1 < cell_count {
853        let shift_from = leaf_slot_offset_for(index + 1);
854        let shift_to = leaf_slot_offset_for(index);
855        let shift_len = (cell_count - index - 1) * 2;
856        let data = page.as_bytes_mut();
857        data.copy_within(shift_from..shift_from + shift_len, shift_to);
858    }
859    page.set_cell_count((cell_count - 1) as u16);
860    page.set_free_start((LEAF_SLOT_ARRAY_OFFSET + (cell_count - 1) * 2) as u16);
861    Ok(())
862}
863
864/// Reset the leaf to an empty slotted state — cell count 0, free
865/// cursors at the extremes. Zeroes the slot array + cell data area so
866/// stale bytes never leak through a corrupted read.
867fn clear_leaf_cells(page: &mut Page) {
868    {
869        let data = page.as_bytes_mut();
870        for byte in &mut data[LEAF_SLOT_ARRAY_OFFSET..] {
871            *byte = 0;
872        }
873    }
874    page.set_cell_count(0);
875    page.set_free_start(LEAF_SLOT_ARRAY_OFFSET as u16);
876    page.set_free_end(PAGE_SIZE as u16);
877}
878
879fn init_leaf_links(page: &mut Page, prev: u32, next: u32) {
880    let data = page.as_bytes_mut();
881    data[LEAF_PREV_OFFSET..LEAF_PREV_OFFSET + 4].copy_from_slice(&prev.to_le_bytes());
882    data[LEAF_NEXT_OFFSET..LEAF_NEXT_OFFSET + 4].copy_from_slice(&next.to_le_bytes());
883}
884
885fn read_next_leaf(page: &Page) -> u32 {
886    let data = page.as_bytes();
887    u32::from_le_bytes([
888        data[LEAF_NEXT_OFFSET],
889        data[LEAF_NEXT_OFFSET + 1],
890        data[LEAF_NEXT_OFFSET + 2],
891        data[LEAF_NEXT_OFFSET + 3],
892    ])
893}
894
895/// Returns the right-sibling page ID for a leaf page.
896/// 0 means this is the rightmost leaf — used by the D2 fastpath cache.
897#[inline]
898fn leaf_right_sibling(page: &Page) -> u32 {
899    read_next_leaf(page)
900}
901
902/// Returns the maximum (last) key stored in a leaf page, or `None` if empty.
903/// This is the Lehman-Yao "high_key" — any key > this value belongs on a
904/// right-sibling page. Reads only the last slot so it is O(1).
905fn leaf_high_key(page: &Page) -> BTreeResult<Option<Vec<u8>>> {
906    let cell_count = page.cell_count() as usize;
907    if cell_count == 0 {
908        return Ok(None);
909    }
910    let (key, _) = read_leaf_cell(page, cell_count - 1)?;
911    Ok(Some(key))
912}
913
914fn set_prev_leaf(page: &mut Page, prev: u32) {
915    let data = page.as_bytes_mut();
916    data[LEAF_PREV_OFFSET..LEAF_PREV_OFFSET + 4].copy_from_slice(&prev.to_le_bytes());
917}
918
919fn set_next_leaf(page: &mut Page, next: u32) {
920    let data = page.as_bytes_mut();
921    data[LEAF_NEXT_OFFSET..LEAF_NEXT_OFFSET + 4].copy_from_slice(&next.to_le_bytes());
922}
923
924// ==================== Interior Page Helpers ====================
925//
926// Slotted-page layout mirroring the leaf format:
927//
928// ```text
929// [PageHeader 32B]
930// [slot 0: u16][slot 1: u16]...[slot N-1: u16]  ← grows forward
931// ... free space ...
932// [cell N-1][cell N-2]...[cell 0]               ← grows backward from free_end
933// ```
934//
935// Each cell is `[key_len:u16][key][child:u32]`. The right-most child
936// pointer lives in `PageHeader.right_child` exactly as before.
937
938#[inline]
939fn interior_slot_offset_for(index: usize) -> usize {
940    INTERIOR_DATA_OFFSET + index * 2
941}
942
943#[inline]
944fn interior_read_slot(page: &Page, index: usize) -> BTreeResult<usize> {
945    let data = page.as_bytes();
946    let slot_pos = interior_slot_offset_for(index);
947    if slot_pos + 2 > PAGE_SIZE {
948        return Err(BTreeError::Corrupted(
949            "interior slot array overflows page".into(),
950        ));
951    }
952    Ok(u16::from_le_bytes([data[slot_pos], data[slot_pos + 1]]) as usize)
953}
954
955#[inline]
956fn interior_write_slot(page: &mut Page, index: usize, cell_offset: u16) -> BTreeResult<()> {
957    let data = page.as_bytes_mut();
958    let slot_pos = interior_slot_offset_for(index);
959    if slot_pos + 2 > PAGE_SIZE {
960        return Err(BTreeError::Corrupted(
961            "interior slot array overflows page".into(),
962        ));
963    }
964    data[slot_pos..slot_pos + 2].copy_from_slice(&cell_offset.to_le_bytes());
965    Ok(())
966}
967
968#[inline]
969fn interior_cells_start(page: &Page) -> usize {
970    let end = page.free_end() as usize;
971    if end == 0 {
972        PAGE_SIZE
973    } else {
974        end
975    }
976}
977
978#[inline]
979fn interior_free_bytes(page: &Page) -> usize {
980    let slot_end = INTERIOR_DATA_OFFSET + (page.cell_count() as usize) * 2;
981    interior_cells_start(page).saturating_sub(slot_end)
982}
983
984fn read_interior_cell(page: &Page, index: usize) -> BTreeResult<(Vec<u8>, u32)> {
985    let cell_count = page.cell_count() as usize;
986    if index >= cell_count {
987        return Err(BTreeError::Corrupted("Cell index out of range".into()));
988    }
989    let offset = interior_read_slot(page, index)?;
990    let data = page.as_bytes();
991    if offset + 2 > PAGE_SIZE {
992        return Err(BTreeError::Corrupted(
993            "interior cell header out of range".into(),
994        ));
995    }
996    let key_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
997    let end = offset + 2 + key_len + 4;
998    if end > PAGE_SIZE {
999        return Err(BTreeError::Corrupted(
1000            "interior cell body out of range".into(),
1001        ));
1002    }
1003    let key = data[offset + 2..offset + 2 + key_len].to_vec();
1004    let child = u32::from_le_bytes([
1005        data[offset + 2 + key_len],
1006        data[offset + 2 + key_len + 1],
1007        data[offset + 2 + key_len + 2],
1008        data[offset + 2 + key_len + 3],
1009    ]);
1010    Ok((key, child))
1011}
1012
1013fn read_interior_keys_children(page: &Page) -> BTreeResult<(Vec<Vec<u8>>, Vec<u32>)> {
1014    let cell_count = page.cell_count() as usize;
1015    let mut keys = Vec::with_capacity(cell_count);
1016    let mut children = Vec::with_capacity(cell_count + 1);
1017
1018    for i in 0..cell_count {
1019        let (key, child) = read_interior_cell(page, i)?;
1020        keys.push(key);
1021        children.push(child);
1022    }
1023
1024    if cell_count == 0 {
1025        let right_child = page.right_child();
1026        if right_child != 0 {
1027            children.push(right_child);
1028        }
1029    } else {
1030        children.push(page.right_child());
1031    }
1032
1033    Ok((keys, children))
1034}
1035
1036/// Bulk-write an interior page from scratch in slotted form. Used by
1037/// the split path. `keys` and `children` follow the B+ tree contract:
1038/// `children.len() == keys.len() + 1`, with the last child landing in
1039/// `PageHeader.right_child` (as before).
1040fn write_interior_entries(page: &mut Page, keys: &[Vec<u8>], children: &[u32]) -> BTreeResult<()> {
1041    if !keys.is_empty() && children.len() != keys.len() + 1 {
1042        return Err(BTreeError::Corrupted(
1043            "Interior keys/children length mismatch".into(),
1044        ));
1045    }
1046
1047    clear_interior_cells(page);
1048    if keys.is_empty() {
1049        let right_child = children.first().copied().unwrap_or(0);
1050        page.set_right_child(right_child);
1051        return Ok(());
1052    }
1053
1054    for (i, key) in keys.iter().enumerate() {
1055        let cell_size = 2 + key.len() + 4;
1056        let cells_start = interior_cells_start(page);
1057        let slot_end = INTERIOR_DATA_OFFSET + (i + 1) * 2;
1058        if slot_end + cell_size > cells_start {
1059            return Err(BTreeError::Corrupted(
1060                "interior rebuild overflowed page".into(),
1061            ));
1062        }
1063        let cell_offset = cells_start - cell_size;
1064        {
1065            let data = page.as_bytes_mut();
1066            data[cell_offset..cell_offset + 2].copy_from_slice(&(key.len() as u16).to_le_bytes());
1067            data[cell_offset + 2..cell_offset + 2 + key.len()].copy_from_slice(key);
1068            data[cell_offset + 2 + key.len()..cell_offset + 2 + key.len() + 4]
1069                .copy_from_slice(&children[i].to_le_bytes());
1070        }
1071        page.set_free_end(cell_offset as u16);
1072        interior_write_slot(page, i, cell_offset as u16)?;
1073    }
1074    page.set_cell_count(keys.len() as u16);
1075    page.set_free_start((INTERIOR_DATA_OFFSET + keys.len() * 2) as u16);
1076    page.set_right_child(*children.last().ok_or_else(|| {
1077        BTreeError::Corrupted("write_interior_entries: children empty with non-empty keys".into())
1078    })?);
1079    Ok(())
1080}
1081
1082fn can_insert_interior(page: &Page, key: &[u8]) -> bool {
1083    let needed = 2 + 2 + key.len() + 4;
1084    interior_free_bytes(page) >= needed
1085}
1086
1087/// Insert a `(key, left_child)` separator into the interior page.
1088/// `right_child` replaces whatever child used to sit to the right of
1089/// the inserted key (either a middle child's pointer or the page's
1090/// `right_child` when the key is the new maximum).
1091fn insert_into_interior(
1092    page: &mut Page,
1093    key: &[u8],
1094    left_child: u32,
1095    right_child: u32,
1096) -> BTreeResult<()> {
1097    // 1. Binary search the slot array for the insert position.
1098    let cell_count = page.cell_count() as usize;
1099    let mut low = 0;
1100    let mut high = cell_count;
1101    while low < high {
1102        let mid = (low + high) / 2;
1103        let (cell_key, _) = read_interior_cell(page, mid)?;
1104        match cell_key.as_slice().cmp(key) {
1105            Ordering::Less => low = mid + 1,
1106            Ordering::Greater | Ordering::Equal => high = mid,
1107        }
1108    }
1109    let insert_pos = low;
1110
1111    // 2. Redirect the previous owner of the split to `right_child`.
1112    //    If the insertion is at the tail, the previous owner was the
1113    //    page's right_child pointer; otherwise it was the child slot
1114    //    of the cell currently at `insert_pos`.
1115    if insert_pos < cell_count {
1116        let old_offset = interior_read_slot(page, insert_pos)?;
1117        let data = page.as_bytes();
1118        let key_len = u16::from_le_bytes([data[old_offset], data[old_offset + 1]]) as usize;
1119        let child_pos = old_offset + 2 + key_len;
1120        let data = page.as_bytes_mut();
1121        data[child_pos..child_pos + 4].copy_from_slice(&right_child.to_le_bytes());
1122    } else {
1123        page.set_right_child(right_child);
1124    }
1125
1126    // 3. Reserve the new cell at the tail of the free area.
1127    let cell_size = 2 + key.len() + 4;
1128    let slot_end_after = INTERIOR_DATA_OFFSET + (cell_count + 1) * 2;
1129    let cells_start = interior_cells_start(page);
1130    if slot_end_after + cell_size > cells_start {
1131        return Err(BTreeError::Corrupted("interior page full".into()));
1132    }
1133    let cell_offset = cells_start - cell_size;
1134    {
1135        let data = page.as_bytes_mut();
1136        data[cell_offset..cell_offset + 2].copy_from_slice(&(key.len() as u16).to_le_bytes());
1137        data[cell_offset + 2..cell_offset + 2 + key.len()].copy_from_slice(key);
1138        data[cell_offset + 2 + key.len()..cell_offset + 2 + key.len() + 4]
1139            .copy_from_slice(&left_child.to_le_bytes());
1140    }
1141    page.set_free_end(cell_offset as u16);
1142
1143    // 4. Shift the slot-array tail right by one slot, write the new
1144    //    pointer into the freed slot, bump counters.
1145    {
1146        let shift_from = interior_slot_offset_for(insert_pos);
1147        let shift_to = shift_from + 2;
1148        let shift_len = (cell_count - insert_pos) * 2;
1149        if shift_len > 0 {
1150            let data = page.as_bytes_mut();
1151            data.copy_within(shift_from..shift_from + shift_len, shift_to);
1152        }
1153    }
1154    interior_write_slot(page, insert_pos, cell_offset as u16)?;
1155    page.set_cell_count((cell_count + 1) as u16);
1156    page.set_free_start((INTERIOR_DATA_OFFSET + (cell_count + 1) * 2) as u16);
1157    Ok(())
1158}
1159
1160fn clear_interior_cells(page: &mut Page) {
1161    {
1162        let data = page.as_bytes_mut();
1163        for byte in &mut data[INTERIOR_DATA_OFFSET..] {
1164            *byte = 0;
1165        }
1166    }
1167    page.set_cell_count(0);
1168    page.set_free_start(INTERIOR_DATA_OFFSET as u16);
1169    page.set_free_end(PAGE_SIZE as u16);
1170}