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