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