kernel/btree.rs
1//! A B+tree over the buffer pool.
2//!
3//! Leaves hold every key and value; interior pages hold separator keys and child
4//! page numbers and are wholly derivable from the leaves. That derivability is
5//! what makes Law 5 cheap: recovery sweeps for intact leaves and repacks.
6//!
7//! SACRIFICE (Law 4): a split leaves two leaves about half full, so the file is
8//! roughly 1.3-1.5x the size of a packed layout. Bought: an insert lands in a page
9//! that already exists, so adding one row never rewrites the store.
10
11use crate::page::{PageKind, PageMut, PageRef, PAGE_SIZE};
12use crate::pool::{BufferPool, PinnedRead, PinnedWrite};
13use crate::verify::decode_record;
14use crate::{Error, Result};
15use std::cell::Cell;
16
17/// Open a resident page through the frame's validated bit (2f-A1): the first
18/// open of a residency pays the full slot validation and sets the bit; every
19/// later open skips the O(entries) loop. See `PageRef::open_resident_validated`.
20fn open_cached<'a>(r: &'a crate::pool::PinnedRead<'_>, no: u32) -> Result<PageRef<'a>> {
21 if r.validated() {
22 PageRef::open_resident_validated(r, no)
23 } else {
24 let p = PageRef::open_resident(r, no)?;
25 validate_records(&p)?;
26 r.set_validated();
27 Ok(p)
28 }
29}
30
31/// Validate every record once per buffer residency. The pool's `validated`
32/// bit makes this page-local pass disappear on hot reopens, preserving the
33/// scan path's existing cost while still ensuring no record field is used
34/// before the shared fallible decoder has accepted it.
35fn validate_records(page: &PageRef<'_>) -> Result<()> {
36 if !matches!(page.kind(), PageKind::Leaf | PageKind::Interior) {
37 return Ok(());
38 }
39 for i in 0..page.nentries() {
40 decode_record(page.slot(i), page.page_no(), page.kind())?;
41 }
42 Ok(())
43}
44
45/// 2f copy-on-write: duplicate a frozen page under a fresh page number.
46/// Byte-identical except its own number; the old version stays on disk,
47/// still serving any snapshot reader pinned at a published root. The old
48/// FRAME may remain cached -- it is clean (frozen pages cannot be dirtied)
49/// and reads of the old number still deserve it; the new page starts dirty
50/// and reaches disk by eviction or checkpoint like any other write.
51fn shadow_page(pool: &BufferPool, page: u32) -> Result<u32> {
52 let content = { pool.get(page)?.to_vec() };
53 let mut w = pool.allocate()?;
54 let no = w.page_no();
55 let b = w.bytes_mut();
56 b.copy_from_slice(&content);
57 b[12..16].copy_from_slice(&no.to_le_bytes());
58 PageMut::reopen(b).finalise(0);
59 // 2n: the original is superseded the moment this epoch publishes --
60 // record it for recycling (safe once the dual-slot fallback and every
61 // live reader have moved past it; see pool's `free` field).
62 let birth = u64::from_le_bytes(content[24..32].try_into().unwrap());
63 pool.free_shadow_page(page, birth)?;
64 Ok(no)
65}
66
67
68/// 2n: detach `child_no` from `parent_no` (both this epoch's writable
69/// copies). Returns the removed child POSITION (always >= 1) so a walk
70/// holding saved indices into this parent can shift them; None -- nothing
71/// removed -- when the child is child0 or the parent's last child: those
72/// leaves are kept cleared-in-place instead. (Child0 removal would need a
73/// saved-index of -1 to express "revisit position 0"; a skipped child left
74/// rows undeleted -- the model test measured 70 of 10000 -- so child0
75/// simply stays, one kept-empty leaf per parent per pass.)
76fn unlink_child(pool: &BufferPool, tree_id: u16, parent_no: u32, child_no: u32) -> Result<Option<usize>> {
77 let pos = {
78 let r = pool.get(parent_no)?;
79 let p = open_cached(&r, parent_no)?;
80 if p.kind() != PageKind::Interior || p.tree_id() != tree_id { return Ok(None); }
81 let n = p.nentries();
82 if p.child0() == child_no {
83 None // child0 stays (see the doc comment)
84 } else {
85 let mut found = None;
86 for i in 0..n {
87 if validated_child(p.slot(i)) == child_no {
88 found = Some(i + 1);
89 break;
90 }
91 }
92 found
93 }
94 };
95 let Some(pos) = pos else { return Ok(None) };
96 let mut w = pool.get_mut(parent_no)?;
97 let mut p = PageMut::reopen(w.bytes_mut());
98 p.remove_slot(pos - 1);
99 p.finalise(0);
100 Ok(Some(pos))
101}
102
103/// Point entry `i` of interior `page` at `new_child` (i == 0 is child0,
104/// otherwise slot i-1). `page` must already be unfrozen -- the shadowed
105/// descent guarantees it, and `get_mut` asserts it.
106fn patch_child(pool: &BufferPool, page: u32, i: usize, new_child: u32) -> Result<()> {
107 let mut w = pool.get_mut(page)?;
108 if i == 0 {
109 let p = PageRef::open_resident(w.bytes(), page)?;
110 validate_records(&p)?;
111 if p.kind() != PageKind::Interior {
112 return Err(Error::Corrupt { page_no: page, why: "child patch reached a non-interior page" });
113 }
114 let mut p = PageMut::reopen(w.bytes_mut());
115 p.set_child0(new_child);
116 p.finalise(0);
117 return Ok(());
118 }
119 // The child is the trailing 4 bytes of the slot's record (enc_interior).
120 // Patch it in place: same length, same key, only the pointer changes.
121 let (off, len) = {
122 let p = PageRef::open_resident(w.bytes(), page)?;
123 validate_records(&p)?;
124 if p.kind() != PageKind::Interior || i > p.nentries() {
125 return Err(Error::Corrupt { page_no: page, why: "child patch slot is out of bounds" });
126 }
127 validated_child(p.slot(i - 1));
128 let b = w.bytes();
129 let base = crate::page::HEADER_LEN + (i - 1) * 4;
130 let off = u16::from_le_bytes([b[base], b[base + 1]]) as usize;
131 let len = u16::from_le_bytes([b[base + 2], b[base + 3]]) as usize;
132 (off, len)
133 };
134 let b = w.bytes_mut();
135 b[off + len - 4..off + len].copy_from_slice(&new_child.to_le_bytes());
136 PageMut::reopen(b).finalise(0);
137 Ok(())
138}
139
140/// How many leaves the per-keyspace append hint remembers at once.
141///
142/// Bounded and constant (Law 1): the cache never grows with the store, and a
143/// slot it cannot spare costs one ordinary descent, never an answer. Sixteen
144/// is chosen from the shape of the write, not from taste: the multimodel load
145/// writes at most a handful of tags per document (mapping, row, vector,
146/// forward edge, reverse edge, field index) and the reverse-edge tag alone
147/// needs two -- a near-ascending run over people and a second, far lower run
148/// over the hundred organizations -- so the working set is single digits with
149/// room left for the indexes a collection adds.
150pub const TAG_HINTS: usize = 16;
151
152/// The longest fence a hint will hold, inline.
153///
154/// INLINE, not a `Vec`, and that is a measured requirement rather than a
155/// preference: `tests/edge_write_budget.rs` counts the allocations one
156/// relationship is allowed, and two owned fences per armed leaf pushed a
157/// two-put edge from 8 allocations to 10. Fences are cached on the descending
158/// path, which is the path that is supposed to be getting cheaper.
159///
160/// 48 bytes holds every key this cache is for: an edge row is at most 41 bytes
161/// (tag, two entities, context, type), a data row about 12, a mapping its
162/// external key. A separator longer than this simply does not arm a hint --
163/// the run keeps descending, which is what it did before.
164const FENCE_MAX: usize = 48;
165
166/// One fence, inline. `len` is meaningful only when `present`.
167#[derive(Clone, Copy)]
168struct Fence { present: bool, len: u8, bytes: [u8; FENCE_MAX] }
169
170impl Fence {
171 const ABSENT: Fence = Fence { present: false, len: 0, bytes: [0; FENCE_MAX] };
172 /// `None` when the key does not fit, so the caller can decline to arm
173 /// rather than remember a truncated fence -- a truncated upper fence is
174 /// LOWER than the real one, which would only ever refuse keys, but a
175 /// truncated LOWER fence is lower too, which would accept them.
176 fn of(key: &[u8]) -> Option<Fence> {
177 if key.len() > FENCE_MAX { return None; }
178 let mut f = Fence { present: true, len: key.len() as u8, bytes: [0; FENCE_MAX] };
179 f.bytes[..key.len()].copy_from_slice(key);
180 Some(f)
181 }
182 fn get(&self) -> Option<&[u8]> {
183 if self.present { Some(&self.bytes[..self.len as usize]) } else { None }
184 }
185}
186
187/// The fences of one leaf, as a descent found them. `armable` is false when a
188/// separator was too long to hold inline.
189#[derive(Clone, Copy)]
190pub(crate) struct LeafFences { lower: Fence, upper: Fence, armable: bool }
191
192impl LeafFences {
193 const NONE: LeafFences = LeafFences { lower: Fence::ABSENT, upper: Fence::ABSENT, armable: true };
194 fn narrow_lower(&mut self, key: &[u8]) {
195 match Fence::of(key) { Some(f) => self.lower = f, None => self.armable = false }
196 }
197 fn narrow_upper(&mut self, key: &[u8]) {
198 match Fence::of(key) { Some(f) => self.upper = f, None => self.armable = false }
199 }
200 /// The half of this interval below `sep`, which is what the LEFT page of a
201 /// split at `sep` inherits.
202 fn below(mut self, sep: &[u8]) -> Self { self.narrow_upper(sep); self }
203 /// The half at or above `sep` -- the RIGHT page's interval. Both halves
204 /// are SUBintervals of this one, which is why arming either is sound: a
205 /// narrower interval can only refuse keys, never misplace one.
206 fn above(mut self, sep: &[u8]) -> Self { self.narrow_lower(sep); self }
207}
208
209/// One remembered leaf, and the exact interval of keys that may be appended
210/// to it without descending.
211///
212/// The fences are the SEPARATORS the arming descent walked through, not the
213/// leaf's own first and last keys: they are the fences the tree itself would
214/// use to route a key to this leaf.
215#[derive(Clone, Copy)]
216struct TagHint {
217 /// 0 marks a free slot; tree ids start at 1.
218 tree: u16,
219 /// The leading byte of the keys this slot serves -- the keyspace tag (D4).
220 tag: u8,
221 leaf: u32,
222 /// The leaf's `next_leaf` when the hint was armed. A split of THIS leaf
223 /// relinks it to the new right page, so comparing one `u32` tells the
224 /// subdivision of this interval -- the one case a cached fence cannot
225 /// survive -- from the ordinary appends that leave the chain alone. It
226 /// doubles as identity: a recycled page number would have to be relinked
227 /// identically to pass.
228 next: u32,
229 /// Lower fence: the separator below this leaf, absent for the leftmost.
230 /// SELECTION ONLY. Nothing is placed on the strength of it; it exists so
231 /// two ascending runs under the SAME tag (the reverse-edge rows of people
232 /// and of organizations) can hold two slots and each find its own.
233 lower: Fence,
234 /// THE FENCE. Absent means this leaf is the tree's rightmost and has no
235 /// separator above it; otherwise a key may be appended here only if it is
236 /// STRICTLY below this. A key equal to the separator belongs to the next
237 /// leaf: `upper_bound` routes it there, so appending it here would make it
238 /// reachable by a scan and invisible to `get`.
239 upper: Fence,
240 /// When this slot was last used or armed, on the cache's own counter.
241 /// The victim of an eviction is the smallest of these.
242 used: u64,
243}
244
245impl TagHint {
246 const FREE: TagHint = TagHint {
247 tree: 0, tag: 0, leaf: 0, next: 0, lower: Fence::ABSENT, upper: Fence::ABSENT, used: 0,
248 };
249 /// Does this slot claim `key`? Lower fence inclusive, upper exclusive.
250 ///
251 /// `relaxed` makes the upper fence inclusive, which is WRONG -- see
252 /// `fast_path_tag_leaf`. It exists only so the byte-equivalence oracle has
253 /// a negative control: a test that proves two files identical proves
254 /// nothing unless a deliberately different placement makes them differ.
255 /// It is settable under `cfg(test)` alone and is a constant `false`
256 /// everywhere else.
257 fn claims(&self, tree: u16, tag: u8, key: &[u8], relaxed: bool) -> bool {
258 self.tree == tree
259 && self.tag == tag
260 && self.lower.get().is_none_or(|l| key >= l)
261 && self.upper.get().is_none_or(|u| if relaxed { key <= u } else { key < u })
262 }
263}
264
265/// The per-keyspace append hints for one handle.
266///
267/// D9 widened the append SPLIT from "rightmost leaf of the tree" to "rightmost
268/// leaf of the key's own tag". This widens the append HINT the same way, which
269/// is the other half: the split policy was already right for a tag that is not
270/// the tree's last, but every one of those inserts still paid a full
271/// root-to-leaf descent to reach the leaf the policy then acted on.
272///
273/// WHY A CACHED FENCE IS SAFE. `fast_path_leaf`'s `next_leaf() == 0` test is
274/// self-validating -- it reads the truth off the page every time -- and a
275/// cached separator is not. It is exact only while the tree's shape is the one
276/// the arming descent walked, so every way that shape can move is answered:
277/// * a split of the hinted leaf subdivides its interval -- the cached
278/// `next_leaf` changes, so the page-side check catches it, and the split
279/// path forgets the slot as well;
280/// * a neighbour redistribution moves separators BETWEEN siblings without
281/// touching the chain, so it forgets the slots of every page in its
282/// window itself;
283/// * a delete, a graft, a bulk pack, a rollback and a checkpoint can merge
284/// leaves, collapse the root or hand a page back to the allocator, and
285/// each of those clears the whole cache.
286///
287/// SACRIFICE (Law 4): about 1.5 KB of fixed state per handle, and a linear
288/// scan of sixteen slots per insert. Bought: an ascending run that is not the
289/// tree's rightmost -- which is every run but one, in a store where D4 makes
290/// every feature a tag in the same tree -- inserts with one page access
291/// instead of the tree's full height.
292pub struct TagHints {
293 slots: [Cell<TagHint>; TAG_HINTS],
294 /// Monotonic use counter. Evicting the LEAST RECENTLY USED slot, rather
295 /// than the next one round-robin, is what lets a few hot runs share the
296 /// cache with many cold ones: the relationship load writes two ascending
297 /// runs (forward rows, reverse rows over people) beside a HUNDRED cold
298 /// ones (reverse rows, one run per organization), and round-robin handed
299 /// a cold organization the hot forward-edge slot every sixteenth write.
300 clock: Cell<u64>,
301 hits: Cell<u64>,
302 attempts: Cell<u64>,
303 /// Inserts for which no slot claimed the key at all. Separate from a
304 /// refused probe because the two have different costs and different cures:
305 /// a miss here costs nothing but the descent it was going to pay anyway,
306 /// while a refused probe costs one page read on top of it.
307 misses: Cell<u64>,
308 /// Off means the handle descends for every insert, exactly as it did
309 /// before this cache existed. Not a cargo feature: one build has to be
310 /// able to run a workload BOTH ways and compare the files it produced,
311 /// which is the only evidence that a hint changes where a write descends
312 /// from and not where it lands.
313 enabled: Cell<bool>,
314 #[cfg(test)]
315 relaxed_upper: Cell<bool>,
316}
317
318impl Default for TagHints {
319 fn default() -> Self {
320 TagHints {
321 slots: std::array::from_fn(|_| Cell::new(TagHint::FREE)),
322 clock: Cell::new(0),
323 hits: Cell::new(0),
324 attempts: Cell::new(0),
325 misses: Cell::new(0),
326 enabled: Cell::new(true),
327 #[cfg(test)]
328 relaxed_upper: Cell::new(false),
329 }
330 }
331}
332
333impl TagHints {
334 /// The leaf that may hold `key` without a descent, and the `next_leaf` it
335 /// had when armed. Pure memory: the fences are compared here so that a
336 /// miss never costs a page read, and only the winner is opened.
337 fn find(&self, tree: u16, key: &[u8]) -> Option<(u32, u32)> {
338 if !self.enabled.get() { return None; }
339 let &tag = key.first()?;
340 let relaxed = self.relaxed();
341 for slot in &self.slots {
342 let mut h = slot.get();
343 if h.claims(tree, tag, key, relaxed) {
344 h.used = self.tick();
345 slot.set(h);
346 return Some((h.leaf, h.next));
347 }
348 }
349 None
350 }
351
352 fn tick(&self) -> u64 {
353 let now = self.clock.get() + 1;
354 self.clock.set(now);
355 now
356 }
357
358 /// Remember `leaf` for `key`'s tag between the fences a descent found.
359 /// Any slot naming the same leaf, or covering an overlapping interval of
360 /// the same tag, is replaced rather than duplicated: two slots that both
361 /// claim one key would make which leaf gets tried depend on scan order.
362 fn arm(&self, tree: u16, key: &[u8], leaf: u32, next: u32, fences: LeafFences) {
363 if !self.enabled.get() { return; }
364 let Some(&tag) = key.first() else { return };
365 if !fences.armable { return; }
366 let overlaps = |h: &TagHint| {
367 h.tree == tree
368 && (h.leaf == leaf
369 || (h.tag == tag
370 && fences.upper.get().is_none_or(|u| h.lower.get().is_none_or(|l| l < u))
371 && h.upper.get().is_none_or(|u| fences.lower.get().is_none_or(|l| l < u))))
372 };
373 let mut at = None;
374 for (i, slot) in self.slots.iter().enumerate() {
375 if overlaps(&slot.get()) {
376 slot.set(TagHint::FREE);
377 if at.is_none() { at = Some(i); }
378 }
379 }
380 let at = at
381 .or_else(|| self.slots.iter().position(|s| s.get().tree == 0))
382 .unwrap_or_else(|| {
383 let mut victim = 0;
384 let mut oldest = u64::MAX;
385 for (i, slot) in self.slots.iter().enumerate() {
386 let used = slot.get().used;
387 if used < oldest { oldest = used; victim = i; }
388 }
389 victim
390 });
391 self.slots[at].set(TagHint {
392 tree, tag, leaf, next, lower: fences.lower, upper: fences.upper, used: self.tick(),
393 });
394 }
395
396 /// Forget one slot after its leaf refused the record. PostgreSQL's
397 /// `_bt_search_insert` disarms on any rejection; this disarms only the
398 /// slot that was wrong, because the other fifteen describe other runs.
399 fn forget_leaf(&self, tree: u16, leaf: u32) {
400 for slot in &self.slots {
401 let h = slot.get();
402 if h.tree == tree && h.leaf == leaf { slot.set(TagHint::FREE); }
403 }
404 }
405
406 /// Forget everything. The answer to every structural change this cache
407 /// does not track page by page.
408 pub fn clear(&self) {
409 for slot in &self.slots { slot.set(TagHint::FREE); }
410 }
411
412 /// Forget one tree's slots, for a tree whose root is being freed or whose
413 /// handle slot is being recycled.
414 pub fn clear_tree(&self, tree: u16) {
415 for slot in &self.slots {
416 if slot.get().tree == tree { slot.set(TagHint::FREE); }
417 }
418 }
419
420 /// Diagnostics: inserts that found a slot, and inserts that then landed on
421 /// its leaf. Counted, not reasoned about -- the budget tests read these.
422 pub fn attempts(&self) -> u64 { self.attempts.get() }
423 pub fn hits(&self) -> u64 { self.hits.get() }
424 /// Inserts no slot claimed.
425 pub fn misses(&self) -> u64 { self.misses.get() }
426
427 /// A cache that is off. Every insert descends, as it did before K1.
428 pub fn disabled() -> Self {
429 let t = TagHints::default();
430 t.set_enabled(false);
431 t
432 }
433
434 /// Turn the cache on or off for this handle. Turning it off forgets
435 /// everything it held, so a handle cannot come back to a stale fence.
436 pub fn set_enabled(&self, on: bool) {
437 self.enabled.set(on);
438 if !on { self.clear(); }
439 }
440
441 pub fn enabled(&self) -> bool { self.enabled.get() }
442
443 /// Negative control for the byte-equivalence oracle; see `TagHint::claims`.
444 #[cfg(test)]
445 pub(crate) fn set_relaxed_upper_fence(&self, on: bool) { self.relaxed_upper.set(on); }
446 #[cfg(test)]
447 fn relaxed(&self) -> bool { self.relaxed_upper.get() }
448 #[cfg(not(test))]
449 fn relaxed(&self) -> bool { false }
450}
451
452pub struct BTree<'p> {
453 pool: &'p BufferPool,
454 tree_id: u16,
455 root: u32,
456 /// The last leaf inserted into, cached the way PostgreSQL caches
457 /// `RelationGetTargetBlock`. Tried before descending on the next insert;
458 /// see `fast_path_leaf`. One `u32` — Law 1 (no allocation proportional to
459 /// the store).
460 ///
461 /// Borrowed, not owned: `Store` builds a fresh `BTree` on every `put`,
462 /// `delete`, `get` and `scan` and drops it immediately (Task 16), so a
463 /// hint owned by the `BTree` itself would reset to `None` on every call
464 /// and the fast path would never fire through `Store`. `Store` owns the
465 /// `Cell` and lends it here for the `BTree`'s lifetime — the equivalent
466 /// of PostgreSQL keeping the hint on the `Relation`, not on a per-insert
467 /// scan state.
468 last_leaf: &'p Cell<Option<u32>>,
469 /// Times `insert` used `last_leaf` instead of a full descent. Borrowed
470 /// the same way as `last_leaf` and for the same reason: a per-`BTree`
471 /// counter would reset every time `Store` opened a fresh one, and the
472 /// new `Store`-level tests need a count that survives across calls.
473 fast_path_hits: &'p Cell<u64>,
474 /// Times `insert` found `last_leaf` armed and called `fast_path_leaf` at
475 /// all, hit or miss. Borrowed the same way as `fast_path_hits`, for the
476 /// same reason (Task 18): a counter owned by the transient `BTree` would
477 /// reset on every `Store::put`, and the disarm test needs a count of
478 /// attempts -- not just hits -- that survives across calls to prove the
479 /// hint stops trying after the first rejection instead of retrying
480 /// forever on a workload it can never satisfy.
481 fast_path_attempts: &'p Cell<u64>,
482 /// The per-keyspace append hints, borrowed from the owning handle for the
483 /// same reason `last_leaf` is: `Store` and `PageWalStore` build a fresh
484 /// `BTree` for every single operation, so a cache owned here would be
485 /// empty on arrival every time. `None` is a tree opened without them --
486 /// every path that can invalidate a hint clears through this same borrow,
487 /// so a handle either passes it everywhere or nowhere.
488 tags: Option<&'p TagHints>,
489}
490
491/// Immutable plan for replacing exactly one live boundary leaf with a
492/// verified candidate subtree. The old leaf's records are copied into the
493/// candidate, so keys outside the empty graft interval are preserved without
494/// being reinserted.
495pub(crate) struct GraftBoundary {
496 leaf: u32,
497 path: Vec<(u32, usize)>,
498 records: Vec<Vec<u8>>,
499 split: usize,
500 pub(crate) old_next: u32,
501 pub(crate) right_page: Option<u32>,
502}
503
504pub(crate) struct GraftCandidate {
505 pub(crate) root: u32,
506 pub(crate) rows: u64,
507 pub(crate) min: Vec<u8>,
508 pub(crate) max: Vec<u8>,
509 pub(crate) last_next: u32,
510}
511
512/// vlen sentinel marking a spilled value. Unambiguous: a real inline value
513/// never exceeds MAX_RECORD_LEN (~4KB), far below 0xFFFF.
514pub(crate) const OVERFLOW_VLEN: u16 = 0xFFFF;
515
516/// The 12-byte body a marker record carries in place of its value:
517/// [total_len u32][head_page u32][crc32 of the FULL value].
518/// The crc is Law 5 for the chain as a WHOLE: each overflow page already
519/// carries its own page checksum, but a chain crossed with another record's
520/// chain (or truncated by a lost page) is made of individually-valid pages --
521/// only a checksum over the assembled value catches that. Blast radius: this
522/// one record.
523pub(crate) fn enc_marker(total: u32, head: u32, crc: u32) -> [u8; 12] {
524 let mut m = [0u8; 12];
525 m[0..4].copy_from_slice(&total.to_le_bytes());
526 m[4..8].copy_from_slice(&head.to_le_bytes());
527 m[8..12].copy_from_slice(&crc.to_le_bytes());
528 m
529}
530
531/// Overflow page layout, after the standard 40-byte page header:
532/// [next_page u32][used u16][content ...]
533/// Ordinary pages: PageKind::Overflow, sealed+checksummed at the pool write
534/// like every other page.
535pub(crate) const OV_NEXT: usize = crate::page::HEADER_LEN;
536pub(crate) const OV_USED: usize = OV_NEXT + 4;
537pub(crate) const OV_DATA: usize = OV_USED + 2;
538pub(crate) const OV_CAP: usize = crate::page::PAGE_SIZE - OV_DATA;
539
540/// Write `val` as a chain, return (head_page, crc). Pages are allocated
541/// forward and linked as built; the LAST page's next is 0.
542pub(crate) fn write_overflow(pool: &BufferPool, val: &[u8]) -> Result<(u32, u32)> {
543 if pool.resource_limits().is_some_and(|l| val.len() > l.record_bytes as usize) {
544 return Err(Error::ResourceLimit("overflow value exceeds record allowance"));
545 }
546 let crc = crc32c::crc32c(val);
547 let mut chunks: Vec<&[u8]> = val.chunks(OV_CAP).collect();
548 if chunks.is_empty() { chunks.push(&[]); }
549 let mut pages = Vec::with_capacity(chunks.len());
550 for _ in &chunks {
551 let w = pool.allocate()?;
552 pages.push(w.page_no());
553 drop(w);
554 }
555 for (i, chunk) in chunks.iter().enumerate() {
556 let mut w = pool.get_mut(pages[i])?;
557 let b = w.bytes_mut();
558 crate::page::PageMut::init(b, crate::page::PageKind::Overflow, 0, pages[i]).finalise(0);
559 let next = if i + 1 < pages.len() { pages[i + 1] } else { 0 };
560 b[OV_NEXT..OV_NEXT + 4].copy_from_slice(&next.to_le_bytes());
561 b[OV_USED..OV_USED + 2].copy_from_slice(&(chunk.len() as u16).to_le_bytes());
562 b[OV_DATA..OV_DATA + chunk.len()].copy_from_slice(chunk);
563 }
564 Ok((pages[0], crc))
565}
566
567/// Assemble a spilled value from its marker. Verifies the whole-value crc and
568/// every structural field before believing anything (Law 5): a wrong page
569/// kind, a length past the page, a chain longer than the file, or a crc
570/// mismatch all refuse the RECORD -- never return partial bytes.
571pub(crate) fn read_overflow(pool: &BufferPool, marker: &[u8]) -> Result<Vec<u8>> {
572 if marker.len() != 12 {
573 return Err(Error::Corrupt { page_no: 0, why: "overflow marker wrong size" });
574 }
575 let total = u32::from_le_bytes(marker[0..4].try_into().unwrap()) as usize;
576 if pool.resource_limits().is_some_and(|l| total > l.record_bytes as usize) {
577 return Err(Error::ResourceLimit("stored overflow value exceeds record allowance"));
578 }
579 let head = u32::from_le_bytes(marker[4..8].try_into().unwrap());
580 let want_crc = u32::from_le_bytes(marker[8..12].try_into().unwrap());
581
582 // Fast path (2e ablation A1): `write_overflow` allocates its pages in one
583 // tight loop off a monotone counter, so a chain's page numbers are always
584 // consecutive -- the whole chain is one speculative pread, verified page
585 // by page, never cached. Falls back to the per-page pool path when any
586 // page is resident (the frame may be dirtier than disk) or when anything
587 // read fails to look like exactly the chain the marker promised. The
588 // fallback re-reads from page 0 of the chain: this path trusts NOTHING it
589 // saw here.
590 let n_pages = total.div_ceil(OV_CAP).max(1);
591 if n_pages <= u32::MAX as usize {
592 let mut buf = Vec::new();
593 if let Ok(true) = pool.read_run_uncached(head, n_pages as u32, &mut buf) {
594 let mut out = Vec::with_capacity(total);
595 let mut contiguous = true;
596 for i in 0..n_pages {
597 let b = &buf[i * crate::page::PAGE_SIZE..(i + 1) * crate::page::PAGE_SIZE];
598 let page_no = head + i as u32;
599 let Ok(p) = PageRef::open_resident(b, page_no) else { contiguous = false; break };
600 if p.kind() != crate::page::PageKind::Overflow { contiguous = false; break; }
601 let next = u32::from_le_bytes(b[OV_NEXT..OV_NEXT + 4].try_into().unwrap());
602 let want_next = if i + 1 < n_pages { page_no + 1 } else { 0 };
603 if next != want_next { contiguous = false; break; }
604 let used = u16::from_le_bytes(b[OV_USED..OV_USED + 2].try_into().unwrap()) as usize;
605 if used > OV_CAP || out.len() + used > total { contiguous = false; break; }
606 out.extend_from_slice(&b[OV_DATA..OV_DATA + used]);
607 }
608 if contiguous && out.len() == total && crc32c::crc32c(&out) == want_crc {
609 return Ok(out);
610 }
611 // else: fall through to the authoritative per-page walk, which
612 // will refuse the record with a precise reason if it is corrupt.
613 }
614 }
615
616 let mut page = head;
617 let mut out = Vec::with_capacity(total);
618 let mut hops = 0u32;
619 let cap = pool.page_count();
620 while page != 0 {
621 hops += 1;
622 if hops > cap {
623 return Err(Error::Corrupt { page_no: page, why: "overflow chain cycles" });
624 }
625 let r = pool.get(page)?;
626 let p = PageRef::open_resident(&r, page)?;
627 if p.kind() != crate::page::PageKind::Overflow {
628 return Err(Error::Corrupt { page_no: page, why: "chain points at a non-overflow page" });
629 }
630 let b = &r[..];
631 let next = u32::from_le_bytes(b[OV_NEXT..OV_NEXT + 4].try_into().unwrap());
632 let used = u16::from_le_bytes(b[OV_USED..OV_USED + 2].try_into().unwrap()) as usize;
633 if used > OV_CAP || out.len() + used > total {
634 return Err(Error::Corrupt { page_no: page, why: "overflow length out of bounds" });
635 }
636 out.extend_from_slice(&b[OV_DATA..OV_DATA + used]);
637 drop(r);
638 page = next;
639 }
640 if out.len() != total || crc32c::crc32c(&out) != want_crc {
641 return Err(Error::Corrupt { page_no: 0, why: "overflow value fails its checksum" });
642 }
643 Ok(out)
644}
645
646/// Validate an old chain completely before allowing its pages onto the
647/// retirement list. No value buffer: one (page, birth-generation) pair per page of
648/// the changed value. The caller removes the marker first, then retires these page IDs
649/// through the existing generation/reader horizon; nothing is freed on an
650/// incomplete or corrupt chain. Cost is proportional to the old large value.
651fn replaced_overflow_pages(pool: &BufferPool, rec: &[u8]) -> Result<Vec<(u32, u64)>> {
652 let (_, marker, overflow) = validated_leaf(rec);
653 if !overflow { return Ok(Vec::new()); }
654 let total = u32::from_le_bytes(marker[..4].try_into().unwrap()) as usize;
655 if pool.resource_limits().is_some_and(|l| total > l.record_bytes as usize) {
656 return Err(Error::ResourceLimit("stored overflow value exceeds record allowance"));
657 }
658 let mut no = u32::from_le_bytes(marker[4..8].try_into().unwrap());
659 let want = u32::from_le_bytes(marker[8..12].try_into().unwrap());
660 let bound = total.div_ceil(OV_CAP).max(1);
661 let mut pages = Vec::new();
662 let (mut bytes, mut crc) = (0usize, 0u32);
663 while no != 0 {
664 if no < 2 || no >= pool.page_count() || pages.len() >= bound {
665 return Err(Error::Corrupt { page_no: no, why: "retired overflow chain bounds" });
666 }
667 let r = pool.get(no)?;
668 let p = PageRef::open_resident(&r, no)?;
669 if p.kind() != PageKind::Overflow || p.tree_id() != 0 {
670 return Err(Error::Corrupt { page_no: no, why: "retired overflow page kind" });
671 }
672 let used = u16::from_le_bytes(r[OV_USED..OV_USED+2].try_into().unwrap()) as usize;
673 // Writer chains fill every non-final page. This also bounds traversal
674 // without a database-sized visited set, even for forged cycles.
675 if used != (total-bytes).min(OV_CAP) {
676 return Err(Error::Corrupt { page_no: no, why: "retired overflow length" });
677 }
678 crc = crc32c::crc32c_append(crc, &r[OV_DATA..OV_DATA+used]);
679 bytes += used;
680 let birth = if pool.is_frozen(no) { p.lsn() } else { pool.stamp_generation() };
681 pages.push((no, birth));
682 no = u32::from_le_bytes(r[OV_NEXT..OV_NEXT+4].try_into().unwrap());
683 }
684 if pages.len() != bound || bytes != total || crc != want {
685 return Err(Error::Corrupt { page_no: 0, why: "retired overflow whole-value checksum or length" });
686 }
687 Ok(pages)
688}
689
690/// A leaf record whose value is an overflow marker: vlen = OVERFLOW_VLEN,
691/// body = enc_marker(). dec_val() on such a record returns the 12 marker
692/// bytes; is_marker() is how readers know to resolve them.
693pub(crate) fn enc_leaf_marker(key: &[u8], marker: &[u8; 12]) -> Vec<u8> {
694 let mut r = Vec::with_capacity(4 + key.len() + 12);
695 r.extend_from_slice(&(key.len() as u16).to_le_bytes());
696 r.extend_from_slice(key);
697 r.extend_from_slice(&OVERFLOW_VLEN.to_le_bytes());
698 r.extend_from_slice(marker);
699 r
700}
701
702/// `compact` is the encoding THIS DATABASE declares (see
703/// `BufferPool::compact_cells`), not what this build was compiled with. A
704/// database that declares the plain encoding gets plain cells even from a
705/// `compact-cells` build, and the other way round; both families decode
706/// everywhere, so a page may legitimately hold a mixture after an encoding
707/// was declared differently at some point in its history.
708pub(crate) fn enc_leaf(key: &[u8], val: &[u8], compact: bool) -> Vec<u8> {
709 if compact && key.first().is_some_and(|b|(0x81..=0x88).contains(b))
710 && key.len()==1+(key[0]-0x80)as usize {
711 // The width-tagged integer key gives its own length; the validated
712 // page slot gives the cell's end. FF + 81..88 cannot be a legal v1
713 // u16 key length in a 4 KiB page. Large values keep overflow markers.
714 let mut r=Vec::with_capacity(1+key.len()+val.len());
715 r.push(0xff);r.extend_from_slice(key);r.extend_from_slice(val);return r;
716 }
717 if compact && key.len() <= 0x0fff {
718 // 0x4xxx cannot be a legal legacy key length in a 4KiB page. The
719 // CRC/bounds-checked slot already provides the value's end, so ordinary
720 // keys need no duplicate u16 value length. Overflow markers stay distinct.
721 let mut r=Vec::with_capacity(2+key.len()+val.len());
722 r.extend_from_slice(&(0x4000|key.len() as u16).to_le_bytes());
723 r.extend_from_slice(key);r.extend_from_slice(val);return r;
724 }
725 let mut r = Vec::with_capacity(4 + key.len() + val.len());
726 r.extend_from_slice(&(key.len() as u16).to_le_bytes());
727 r.extend_from_slice(key);
728 r.extend_from_slice(&(val.len() as u16).to_le_bytes());
729 r.extend_from_slice(val);
730 r
731}
732/// Fast access after `validate_records` accepted the complete page through the
733/// shared fallible decoder. These helpers never receive unvalidated disk bytes:
734/// `open_cached` owns that boundary, and writable-page callers validate before
735/// their first access. Keeping the proof on the buffer residency avoids paying
736/// the same bounds checks again for every row of every hot scan.
737#[inline(always)]
738fn validated_key(rec: &[u8]) -> &[u8] {
739 if rec[0]==0xff && (0x81..=0x88).contains(&rec[1]) {
740 return &rec[1..2+(rec[1]-0x80)as usize];
741 }
742 // Unconditional: the cell family is stored in the bytes, so a build
743 // without the `compact-cells` feature must still read one.
744 if rec[1]&0xf0==0x40 {
745 let k=(u16::from_le_bytes([rec[0],rec[1]])&0x0fff) as usize;
746 return &rec[2..2+k];
747 }
748 let k = u16::from_le_bytes([rec[0], rec[1]]) as usize;
749 &rec[2..2 + k]
750}
751
752#[inline(always)]
753fn validated_leaf(rec: &[u8]) -> (&[u8], &[u8], bool) {
754 if rec[0]==0xff && (0x81..=0x88).contains(&rec[1]) {
755 let end=2+(rec[1]-0x80)as usize;return (&rec[1..end],&rec[end..],false);
756 }
757 if rec[1]&0xf0==0x40 {
758 let end=2+(u16::from_le_bytes([rec[0],rec[1]])&0x0fff) as usize;
759 return (&rec[2..end],&rec[end..],false);
760 }
761 let k = u16::from_le_bytes([rec[0], rec[1]]) as usize;
762 let v = u16::from_le_bytes([rec[2 + k], rec[3 + k]]);
763 let value_len = if v == OVERFLOW_VLEN { 12 } else { v as usize };
764 (&rec[2..2 + k], &rec[4 + k..4 + k + value_len], v == OVERFLOW_VLEN)
765}
766/// Does this leaf hold an overflow marker anywhere? Read ONCE per leaf pin so
767/// a pull cursor can serve each row with one page open and one record decode
768/// instead of asking the page again per row. Only the general record encoding
769/// can carry a marker at all -- the two short forms declare their own value
770/// inline -- so the common leaf costs two comparisons per record and stops
771/// there.
772fn leaf_has_marker(page: &PageRef<'_>) -> bool {
773 for i in 0..page.nentries() {
774 let rec = page.slot(i);
775 if rec[0] == 0xff && (0x81..=0x88).contains(&rec[1]) {
776 continue;
777 }
778 if rec[1] & 0xf0 == 0x40 {
779 continue;
780 }
781 let k = u16::from_le_bytes([rec[0], rec[1]]) as usize;
782 if u16::from_le_bytes([rec[2 + k], rec[3 + k]]) == OVERFLOW_VLEN {
783 return true;
784 }
785 }
786 false
787}
788fn enc_interior(key: &[u8], child: u32) -> Vec<u8> {
789 let mut r = Vec::with_capacity(6 + key.len());
790 r.extend_from_slice(&(key.len() as u16).to_le_bytes());
791 r.extend_from_slice(key);
792 r.extend_from_slice(&child.to_le_bytes());
793 r
794}
795#[inline(always)]
796fn validated_child(rec: &[u8]) -> u32 {
797 let k = u16::from_le_bytes([rec[0], rec[1]]) as usize;
798 u32::from_le_bytes(rec[2 + k..6 + k].try_into().unwrap())
799}
800
801fn child_at(pool: &BufferPool, page: &PageRef<'_>, index: usize) -> Result<u32> {
802 let child = if index == 0 {
803 page.child0()
804 } else {
805 validated_child(page.slot(index - 1))
806 };
807 if child == 0 || child >= pool.page_count() {
808 return Err(Error::Corrupt {
809 page_no: page.page_no(),
810 why: "interior child pointer is outside the data file",
811 });
812 }
813 Ok(child)
814}
815
816/// Build a page's contents in scratch, then commit them to the frame in one copy.
817///
818/// `PageMut::init` zeroes the page. Writing straight into a live frame and then
819/// hitting a fallible `insert_slot` therefore leaves that frame wiped, dirty and
820/// never finalised -- a stale checksum over a blank page, which the next read
821/// refuses, losing every key on it. That is exactly the replace-path defect this
822/// task already fixed once, relocated into the split.
823///
824/// Today `split_point`'s `+4` per-record accounting is the same arithmetic
825/// `insert_slot` checks, so these inserts provably cannot fail. But that is an
826/// invariant spanning two functions with nothing enforcing it, and a change to
827/// `SLOT_LEN` or `HEADER_LEN` would reintroduce total leaf loss with no test
828/// able to catch it. Building in scratch removes the class rather than relying
829/// on the coincidence.
830///
831/// SACRIFICE (Law 4): one page of scratch per split plus one extra copy.
832/// Bought: no error path can destroy a live page.
833pub(crate) fn build_page(
834 kind: PageKind,
835 tree_id: u16,
836 page_no: u32,
837 recs: &[Vec<u8>],
838 child0_or_next: u32,
839) -> Result<Vec<u8>> {
840 let mut scratch = vec![0u8; PAGE_SIZE];
841 build_page_into(&mut scratch, kind, tree_id, page_no, recs.iter().map(|r| r.as_slice()), child0_or_next)?;
842 Ok(scratch)
843}
844
845/// The same page build, into a buffer the caller already owns and over
846/// records the caller does not have to own. This is the ONE inner insert
847/// loop: `build_page` allocates a fresh page for it, the `slotref-split`
848/// path hands it a reusable scratch page and an iterator over records that
849/// are still sitting in their original page images. Keeping one loop is the
850/// point -- two copies of "append every record, then stamp next_leaf" is two
851/// places for the split's byte-for-byte contract to drift.
852pub(crate) fn build_page_into<'r>(
853 dst: &mut [u8],
854 kind: PageKind,
855 tree_id: u16,
856 page_no: u32,
857 recs: impl Iterator<Item = &'r [u8]>,
858 child0_or_next: u32,
859) -> Result<()> {
860 let mut p = PageMut::init(dst, kind, tree_id, page_no);
861 for r in recs {
862 let at = p.nentries_pub();
863 p.insert_slot(at, r)?;
864 }
865 p.set_next_leaf(child0_or_next);
866 p.finalise(0);
867 Ok(())
868}
869
870/// Index of the first entry whose key is >= `key`.
871fn lower_bound(p: &PageRef, key: &[u8]) -> Result<usize> {
872 let (mut lo, mut hi) = (0usize, p.nentries());
873 while lo < hi {
874 let mid = (lo + hi) / 2;
875 if validated_key(p.slot(mid)) < key { lo = mid + 1 } else { hi = mid }
876 }
877 Ok(lo)
878}
879
880/// Index of the first entry whose key is > `key`.
881fn upper_bound(p: &PageRef, key: &[u8]) -> Result<usize> {
882 let (mut lo, mut hi) = (0usize, p.nentries());
883 while lo < hi {
884 let mid = (lo + hi) / 2;
885 if validated_key(p.slot(mid)) <= key { lo = mid + 1 } else { hi = mid }
886 }
887 Ok(lo)
888}
889
890/// Where to cut a page's entries so that **both** halves fit in a page.
891///
892/// Splitting at `len / 2` by COUNT is wrong for variable-length records. So is
893/// cutting at the first prefix to cross half the bytes: that prefix can exceed
894/// a page on its own. Concretely, with a 4056-byte usable page, forty 100-byte
895/// entries and one 4000-byte entry inserted in the middle, the first prefix to
896/// reach half the bytes is 6000 bytes — a page and a half.
897///
898/// So this checks the constraint directly rather than approximating it: among
899/// the cuts where the left side AND the right side each fit, take the most
900/// balanced. Returns `None` when no two-way cut exists, which happens only when
901/// one record is so large that no arrangement of the rest leaves room. The
902/// caller refuses that insert rather than corrupting a page; the real answer is
903/// overflow pages for large values, which the spec defers.
904#[cfg(all(feature = "sqlite-balance", any(test, not(feature = "slotref-split"))))]
905fn neighbor_cell_cuts(sizes: &[usize], usable: usize, existing: usize) -> Option<Vec<usize>> {
906 if sizes.is_empty() || sizes.iter().any(|&v| v==0 || v>usable) { return None; }
907 let n=sizes.len();
908 let mut prefix=vec![0usize];
909 for &size in sizes { prefix.push(prefix.last()?.checked_add(size)?); }
910 // Positive, indivisible cells: the longest fitting prefix minimizes the
911 // number of contiguous pages. Compute every suffix's minimum in O(cells).
912 let mut ends=vec![0;n];let mut end=0;
913 for i in 0..n {
914 while end<n && prefix[end+1]-prefix[i]<=usable { end+=1; }
915 ends[i]=end;
916 }
917 let mut needed=vec![0;n+1];
918 for i in (0..n).rev() { needed[i]=1+needed[ends[i]]; }
919 let groups=existing.max(needed[0]);
920 if groups>existing+1 || n<groups { return None; }
921 let mut cuts=vec![0];
922 for group in 0..groups-1 {
923 let begin=*cuts.last()?;let left=groups-group-1;
924 let target=(prefix[n]-prefix[begin])/(left+1);
925 let mut best=None;
926 for end in begin+1..=ends[begin].min(n-left) {
927 if needed[end]>left { continue; }
928 let delta=(prefix[end]-prefix[begin]).abs_diff(target);
929 if best.is_none_or(|(_,old)|delta<old) { best=Some((end,delta)); }
930 }
931 cuts.push(best?.0);
932 }
933 cuts.push(n);Some(cuts)
934}
935
936#[cfg(all(test,feature="sqlite-balance"))]
937mod neighbor_capacity_tests {
938 use super::neighbor_cell_cuts;
939 #[test]
940 fn full_leaf_and_one_sibling_use_at_most_three_pages() {
941 let sizes=vec![272;29];
942 let cuts=neighbor_cell_cuts(&sizes,4056,2).unwrap();
943 assert_eq!(cuts.len(),4);
944 for w in cuts.windows(2) { assert!((w[1]-w[0])*272<=4056); }
945 }
946 #[test]
947 fn forty_three_indivisible_cells_need_four_pages() {
948 let sizes=vec![272;43];
949 assert!(sizes.iter().sum::<usize>()<3*4056);
950 let cuts=neighbor_cell_cuts(&sizes,4056,3).unwrap();
951 assert_eq!(cuts.len(),5);
952 for w in cuts.windows(2) { assert!((w[1]-w[0])*272<=4056); }
953 }
954 #[test]
955 fn bounded_planner_matches_exhaustive_partition_oracle() {
956 fn possible(s:&[usize],pages:usize)->bool {
957 if pages==0 { return s.is_empty(); }
958 let mut sum=0;
959 for end in 1..=s.len() {
960 sum+=s[end-1];if sum>7 { break; }
961 if possible(&s[end..],pages-1) { return true; }
962 }
963 false
964 }
965 for n in 2..=7 {
966 for mut code in 0..3usize.pow(n as u32) {
967 let s:Vec<_>=(0..n).map(|_| {let v=[1,3,5][code%3];code/=3;v}).collect();
968 let expected=(2..=3).find(|&p|possible(&s,p));
969 let result=neighbor_cell_cuts(&s,7,2);
970 assert_eq!(result.as_ref().map(|v|v.len()-1),expected,"{s:?}");
971 if let Some(cuts)=result {
972 assert_eq!(*cuts.last().unwrap(),n);
973 for w in cuts.windows(2) {assert!(w[1]>w[0]);assert!(s[w[0]..w[1]].iter().sum::<usize>()<=7);}
974 }
975 }
976 }
977 }
978}
979
980fn split_point(recs: &[Vec<u8>], usable: usize) -> Option<usize> {
981 let sizes: Vec<usize> = recs.iter().map(|r| r.len() + 4).collect();
982 let total: usize = sizes.iter().sum();
983 let mut acc = 0usize;
984 let mut best: Option<(usize, usize)> = None; // (imbalance, mid)
985 for (j, size) in sizes.iter().enumerate().take(recs.len().saturating_sub(1)) {
986 acc += size;
987 let (left, right) = (acc, total - acc);
988 if left <= usable && right <= usable {
989 let imbalance = left.abs_diff(right);
990 if best.is_none_or(|(b, _)| imbalance < b) {
991 best = Some((imbalance, j + 1));
992 }
993 }
994 }
995 best.map(|(_, mid)| mid)
996}
997
998// ===================================================================== L2.3-2
999// Allocation-free leaf split and redistribution.
1000//
1001// THE FINDING. On a Pi, a typed put spends about 46% of its CPU in the split
1002// path, and malloc+free together are a quarter of all cycles. The reason is
1003// visible in one line of the old split: `(0..p.nentries()).map(|j|
1004// p.slot(j).to_vec())`. Every record on the page becomes its own heap
1005// allocation -- about 86 of them for an ordinary leaf -- and a three-sibling
1006// redistribution does that for three leaves plus the parent's separators,
1007// about 320. None of those copies is needed: the records are already sitting,
1008// contiguous and bounds-checked, in pinned buffer-pool frames.
1009//
1010// THE SHAPE. SQLite's `balance_nonroot` does not copy cells either; it builds
1011// an array of cell POINTERS into the existing page images plus one scratch
1012// allocation, and assembles the new pages from that. `SlotRef` is that
1013// pointer array, written in the terms this file already uses: which page image
1014// (an index into a small `frames` array) and the slot directory's own
1015// (offset, length) pair, taken verbatim, with no decoding at all.
1016//
1017// WHAT DOES NOT CHANGE. Every decision: `split_point`, `neighbor_cell_cuts`,
1018// the `at_point` append test, the window order, the fit probe, the shadowing.
1019// The page images and separator bytes are identical, which is a claim about
1020// output and is therefore tested as one -- see `split_byte_equivalence`.
1021//
1022// SACRIFICE (Law 4): about 44 KiB of scratch, per thread that ever splits,
1023// held for the process's life instead of being allocated and freed per split
1024// (10 reusable pages + two SlotRef arrays that grow to the largest window
1025// seen, bounded by three pages of records). It is RAM proportional to a page,
1026// not to the store, so Law 1 is untouched -- but it is no longer transient,
1027// and that is the cost. Bought: a steady-state split allocates a small
1028// bounded constant instead of one allocation per record on the page.
1029
1030/// A record left where it is. `page_idx` selects one of the `frames` the
1031/// caller passes alongside; `off`/`len` are the slot directory's own numbers.
1032#[cfg(any(test, feature = "slotref-split"))]
1033#[derive(Clone, Copy, Debug)]
1034pub(crate) struct SlotRef { page_idx: u8, off: u16, len: u16 }
1035
1036/// Frame indices. Fixed, because a `SlotRef` built while gathering the
1037/// splitting leaf is later resolved against the redistribution's larger frame
1038/// array, and the two must agree about what index 0 and 1 mean.
1039#[cfg(any(test, feature = "slotref-split"))]
1040mod frame {
1041 pub const LEAF: u8 = 0; // the splitting leaf's image, copied to scratch
1042 pub const REC: u8 = 1; // the incoming record
1043 pub const SIB0: u8 = 2; // first redistribution sibling
1044 pub const SIB1: u8 = 3; // second redistribution sibling
1045 pub const PARENT: u8 = 4; // the parent's image, copied to scratch
1046 pub const SEPS: u8 = 5; // separators this redistribution had to rebuild
1047}
1048
1049#[cfg(any(test, feature = "slotref-split"))]
1050#[inline(always)]
1051fn rec_of<'f>(frames: &[&'f [u8]], s: SlotRef) -> &'f [u8] {
1052 let b = frames[s.page_idx as usize];
1053 &b[s.off as usize..s.off as usize + s.len as usize]
1054}
1055
1056/// The reusable buffers a split borrows instead of allocating.
1057///
1058/// Three thread-locals rather than one struct with three fields, because the
1059/// leaf image, the leaf's slot list and everything else are borrowed at the
1060/// same time by different parameters of the same call; splitting them is how
1061/// the borrow checker is told they never alias. A `BTree` is rebuilt per
1062/// operation (`Store::put` opens one and drops it), so the scratch cannot live
1063/// on it; the buffer pool is single-threaded by construction (`RefCell`
1064/// inside), so a thread-local is exactly one scratch per live writer.
1065#[cfg(any(test, feature = "slotref-split"))]
1066pub(crate) struct SplitScratch {
1067 /// Sibling page images, two pages: a redistribution window is at most
1068 /// three leaves and one of them is the splitting leaf itself.
1069 sib: Vec<u8>,
1070 /// The parent's page image.
1071 parent: Vec<u8>,
1072 /// Built page images, four pages: up to three leaves plus the parent.
1073 out: Vec<u8>,
1074 /// Interior records a redistribution had to rebuild (new key, or same key
1075 /// and a new child). Three pages: at most three such records, each at most
1076 /// one page's worth.
1077 seps: Vec<u8>,
1078 /// The window's records, in order, across the whole window.
1079 win: Vec<SlotRef>,
1080 /// The parent's separators as they stand, and as they will stand.
1081 pr0: Vec<SlotRef>,
1082 pr: Vec<SlotRef>,
1083 sizes: Vec<usize>,
1084 prefix: Vec<usize>,
1085 ends: Vec<usize>,
1086 needed: Vec<usize>,
1087 cuts: Vec<usize>,
1088 /// The parent's children as they stand, and after any shadowing.
1089 ids: Vec<u32>,
1090 newids: Vec<u32>,
1091}
1092
1093#[cfg(any(test, feature = "slotref-split"))]
1094impl SplitScratch {
1095 fn new() -> Self {
1096 SplitScratch {
1097 sib: vec![0u8; 2 * PAGE_SIZE],
1098 parent: vec![0u8; PAGE_SIZE],
1099 out: vec![0u8; 4 * PAGE_SIZE],
1100 seps: vec![0u8; 3 * PAGE_SIZE],
1101 win: Vec::with_capacity(2048),
1102 pr0: Vec::with_capacity(1024),
1103 pr: Vec::with_capacity(1024),
1104 sizes: Vec::with_capacity(2048),
1105 prefix: Vec::with_capacity(2049),
1106 ends: Vec::with_capacity(2048),
1107 needed: Vec::with_capacity(2049),
1108 cuts: Vec::with_capacity(8),
1109 ids: Vec::with_capacity(1024),
1110 newids: Vec::with_capacity(1024),
1111 }
1112 }
1113}
1114
1115#[cfg(any(test, feature = "slotref-split"))]
1116thread_local! {
1117 /// The splitting leaf's page image. Copied out of the frame once so the
1118 /// write guard can be handed to `redistribute_neighbors_ref` as `&mut`
1119 /// while its records are still readable -- 4 KiB of memcpy in place of one
1120 /// allocation per record.
1121 static SPLIT_LEAF: std::cell::RefCell<Vec<u8>> =
1122 std::cell::RefCell::new(vec![0u8; PAGE_SIZE]);
1123 /// The splitting leaf's records with the incoming one already in place.
1124 static SPLIT_SLOTS: std::cell::RefCell<Vec<SlotRef>> =
1125 std::cell::RefCell::new(Vec::with_capacity(1024));
1126 static SPLIT_SCRATCH: std::cell::RefCell<SplitScratch> =
1127 std::cell::RefCell::new(SplitScratch::new());
1128}
1129
1130/// Write one interior record -- `[klen u16][key][child u32]`, byte for byte
1131/// what `enc_interior` builds -- into the separator arena, and return the
1132/// `SlotRef` that names it.
1133#[cfg(all(feature = "sqlite-balance", any(test, feature = "slotref-split")))]
1134fn push_sep(seps: &mut [u8], at: &mut usize, key: &[u8], child: u32) -> SlotRef {
1135 let off = *at;
1136 let len = 6 + key.len();
1137 seps[off..off + 2].copy_from_slice(&(key.len() as u16).to_le_bytes());
1138 seps[off + 2..off + 2 + key.len()].copy_from_slice(key);
1139 seps[off + 2 + key.len()..off + len].copy_from_slice(&child.to_le_bytes());
1140 *at = off + len;
1141 SlotRef { page_idx: frame::SEPS, off: off as u16, len: len as u16 }
1142}
1143
1144/// Re-point an arena separator at a different child. `enc_interior` puts the
1145/// child in the record's trailing four bytes, so this is that patch -- the
1146/// same one `patch_child` performs on a live page.
1147#[cfg(all(feature = "sqlite-balance", any(test, feature = "slotref-split")))]
1148fn patch_sep_child(seps: &mut [u8], s: SlotRef, child: u32) {
1149 let end = s.off as usize + s.len as usize;
1150 seps[end - 4..end].copy_from_slice(&child.to_le_bytes());
1151}
1152
1153/// `split_point` over borrowed records. Identical arithmetic: a record costs
1154/// its length plus the four bytes of its slot-directory entry, and among the
1155/// cuts where both sides fit a page the most balanced one wins.
1156#[cfg(any(test, feature = "slotref-split"))]
1157fn split_point_ref(slots: &[SlotRef], usable: usize) -> Option<usize> {
1158 let total: usize = slots.iter().map(|s| s.len as usize + 4).sum();
1159 let mut acc = 0usize;
1160 let mut best: Option<(usize, usize)> = None; // (imbalance, mid)
1161 for (j, s) in slots.iter().enumerate().take(slots.len().saturating_sub(1)) {
1162 acc += s.len as usize + 4;
1163 let (left, right) = (acc, total - acc);
1164 if left <= usable && right <= usable {
1165 let imbalance = left.abs_diff(right);
1166 if best.is_none_or(|(b, _)| imbalance < b) {
1167 best = Some((imbalance, j + 1));
1168 }
1169 }
1170 }
1171 best.map(|(_, mid)| mid)
1172}
1173
1174/// `neighbor_cell_cuts` writing into buffers the caller reuses. Line for line
1175/// the same planner; only the four `vec![]`s became `clear()`s. `cuts` holds
1176/// the answer; `false` is the original's `None`.
1177#[cfg(all(feature = "sqlite-balance", any(test, feature = "slotref-split")))]
1178#[allow(clippy::too_many_arguments)]
1179fn neighbor_cell_cuts_into(
1180 sizes: &[usize], usable: usize, existing: usize,
1181 prefix: &mut Vec<usize>, ends: &mut Vec<usize>, needed: &mut Vec<usize>, cuts: &mut Vec<usize>,
1182) -> bool {
1183 cuts.clear();
1184 if sizes.is_empty() || sizes.iter().any(|&v| v == 0 || v > usable) { return false; }
1185 let n = sizes.len();
1186 prefix.clear();
1187 prefix.push(0);
1188 for &size in sizes {
1189 match prefix.last().and_then(|p| p.checked_add(size)) {
1190 Some(v) => prefix.push(v),
1191 None => return false,
1192 }
1193 }
1194 ends.clear();
1195 ends.resize(n, 0);
1196 let mut end = 0;
1197 for i in 0..n {
1198 while end < n && prefix[end + 1] - prefix[i] <= usable { end += 1; }
1199 ends[i] = end;
1200 }
1201 needed.clear();
1202 needed.resize(n + 1, 0);
1203 for i in (0..n).rev() { needed[i] = 1 + needed[ends[i]]; }
1204 let groups = existing.max(needed[0]);
1205 if groups > existing + 1 || n < groups { return false; }
1206 cuts.push(0);
1207 for group in 0..groups - 1 {
1208 let begin = *cuts.last().expect("cuts always holds its first entry");
1209 let left = groups - group - 1;
1210 let target = (prefix[n] - prefix[begin]) / (left + 1);
1211 let mut best = None;
1212 for end in begin + 1..=ends[begin].min(n - left) {
1213 if needed[end] > left { continue; }
1214 let delta = (prefix[end] - prefix[begin]).abs_diff(target);
1215 if best.is_none_or(|(_, old)| delta < old) { best = Some((end, delta)); }
1216 }
1217 match best {
1218 Some((e, _)) => cuts.push(e),
1219 None => { cuts.clear(); return false; }
1220 }
1221 }
1222 cuts.push(n);
1223 true
1224}
1225
1226pub struct RangeIter<'p> {
1227 pool: &'p BufferPool,
1228 tree_id: u16,
1229 page: u32,
1230 idx: usize,
1231 done: bool,
1232 /// The descent path to the current leaf: (interior page, chosen child
1233 /// index), root first. Child index 0 means child0, k means slot k-1.
1234 ///
1235 /// 2f: scans advance THROUGH THE PARENT instead of following the
1236 /// on-page sibling pointer. Under copy-on-write a modified leaf moves
1237 /// to a fresh page number and its left neighbour's stored `next_leaf`
1238 /// silently names the STALE version -- the neighbour is not on any
1239 /// descent path, so nothing can fix it. The parent path has no such
1240 /// problem, and unlike re-descending from the root per leaf (measured:
1241 /// range_filter 0.53 -> 1.13 ms, rejected under D25) it costs one page
1242 /// open per crossing, the same as the chain did. Path staleness cannot
1243 /// occur: mutating the tree during a scan is already forbidden for the
1244 /// writer, and snapshot readers hold an immutable root. `next_leaf`
1245 /// stays on disk only as the rightmost flag (zero vs nonzero survives
1246 /// shadowing) for the append fast path.
1247 path: Vec<(u32, usize)>,
1248 /// One leaf's records, drained per `next()`, but only once a scan has
1249 /// PROVED long. Per-entry re-pinning cost ~132ns/key (`open_resident`
1250 /// bounds-checks every slot per open -- O(entries^2) per leaf) and made
1251 /// index ranges 7x slower than SQLite; whole-leaf batching fixed that and
1252 /// then cost graph hops 2-3x, because a 3-edge hop copied a ~130-entry
1253 /// leaf. So: the first `SHORT_SCAN` entries are served per-entry (a hop
1254 /// never notices), and batching starts at the threshold (a range scan
1255 /// amortises everything after). Bounded by leaf capacity; Law 1 intact.
1256 buf: std::collections::VecDeque<(Vec<u8>, Vec<u8>, bool)>,
1257 served: u32,
1258 /// Leaves visited, and the ceiling. A sibling chain cannot legitimately visit
1259 /// more leaves than the file has pages, so exceeding it means it cycles.
1260 leaves: u32,
1261 max_leaves: u32,
1262 /// Live pin of `page` for [`RangeIter::peek_at_or_after`]. Held across
1263 /// successive peeks into the same leaf so a lockstep cursor pays one
1264 /// `pool.get` per leaf, not per row. Dropped before `advance()` and
1265 /// before overflow-chain walks. `next()` / `for_each_ref` drop it on
1266 /// entry so their existing pin accounting is unchanged.
1267 pin: Option<PinnedRead<'p>>,
1268 /// The pinned leaf's entry count and whether any of its records is an
1269 /// overflow marker, read ONCE when the pin is taken. A pull cursor asks
1270 /// "am I still standing on a record of this leaf?" once per ROW, and
1271 /// answering it from the page needed a second page open and a second
1272 /// record decode every time. Meaningful only while `pin` is `Some`.
1273 leaf_entries: usize,
1274 leaf_markers: bool,
1275 /// Seeks: how many times this cursor has had to ASK where it is, rather
1276 /// than read the record it was already standing on. One per leaf is the
1277 /// shape a streaming walk should have; one per row is the shape a pull
1278 /// cursor had. Diagnostic, so the property can be asserted rather than
1279 /// timed.
1280 seeks: u64,
1281}
1282
1283/// A descending range cursor. Unlike reversing a forward [`RangeIter`], this
1284/// pins one leaf at a time and never materialises the range it is walking.
1285pub struct ReverseRangeIter<'p> {
1286 pool: &'p BufferPool,
1287 tree_id: u16,
1288 page: u32,
1289 /// Exclusive slot bound in the current leaf; the next slot is `idx - 1`.
1290 idx: usize,
1291 done: bool,
1292 path: Vec<(u32, usize)>,
1293 leaves: u32,
1294 max_leaves: u32,
1295 /// Live pin of `page` for [`ReverseRangeIter::peek_ref`], held across
1296 /// successive peeks into the same leaf exactly as the forward cursor holds
1297 /// its own. `for_each_ref` never sets it and is unaffected.
1298 pin: Option<PinnedRead<'p>>,
1299 /// An overflow record resolved out of its chain. It cannot be borrowed
1300 /// from the pinned leaf, so it is parked here and served from here; the
1301 /// slot index has already stepped past it when it lands.
1302 pending: Option<(Vec<u8>, Vec<u8>)>,
1303}
1304
1305impl<'p> BTree<'p> {
1306 pub fn create(
1307 pool: &'p BufferPool,
1308 tree_id: u16,
1309 last_leaf: &'p Cell<Option<u32>>,
1310 fast_path_hits: &'p Cell<u64>,
1311 fast_path_attempts: &'p Cell<u64>,
1312 ) -> Result<Self> {
1313 let mut w = pool.allocate()?;
1314 let no = w.page_no();
1315 // Page 0 is the superblock, and `next_leaf == 0` is how a leaf says it
1316 // has no sibling. A tree page numbered 0 would make that sentinel
1317 // ambiguous, so the caller must have reserved page 0 before creating any
1318 // tree. This is the bootstrap order `Store::build` follows.
1319 assert_ne!(no, crate::meta::META_PAGE, "page 0 is reserved for the superblock");
1320 let mut p = PageMut::init(w.bytes_mut(), PageKind::Leaf, tree_id, no);
1321 p.finalise(0);
1322 drop(w);
1323 Ok(BTree { pool, tree_id, root: no, last_leaf, fast_path_hits, fast_path_attempts, tags: None })
1324 }
1325
1326 pub fn open(
1327 pool: &'p BufferPool,
1328 tree_id: u16,
1329 root: u32,
1330 last_leaf: &'p Cell<Option<u32>>,
1331 fast_path_hits: &'p Cell<u64>,
1332 fast_path_attempts: &'p Cell<u64>,
1333 ) -> Self {
1334 BTree { pool, tree_id, root, last_leaf, fast_path_hits, fast_path_attempts, tags: None }
1335 }
1336
1337 /// Attach the owning handle's per-keyspace append hints. A handle must do
1338 /// this on EVERY tree it opens, not only the ones it means to speed up:
1339 /// the clearing a split or a delete performs happens through this borrow,
1340 /// so a mutation that arrives on an unattached `BTree` would leave a hint
1341 /// standing over a tree whose shape it no longer describes.
1342 pub fn with_tags(mut self, tags: &'p TagHints) -> Self { self.tags = Some(tags); self }
1343
1344 pub fn root(&self) -> u32 { self.root }
1345
1346 /// Drop every per-keyspace hint. Called wherever a separator can move or a
1347 /// page can be recycled.
1348 fn forget_tag_hints(&self) { if let Some(t) = self.tags { t.clear(); } }
1349
1350 /// Re-arm the per-keyspace hint on the page a SPLIT left the record in.
1351 ///
1352 /// Without this a leaf fill costs the run two descents, not one: the probe
1353 /// is refused for want of room and disarms the slot, the descent splits,
1354 /// and then the NEXT key of that run finds no slot and descends again.
1355 /// Measured on the 200K relationship load, forward-edge rows: 6,383 of
1356 /// 99,840 puts found no slot and each paid about 7.4 page accesses --
1357 /// 1.774 accesses per put against 1.36 with this.
1358 ///
1359 /// Sound because a split's halves are SUBintervals of the interval the
1360 /// descent walked: the left page keeps everything below the new separator
1361 /// and the right page everything at or above it, so naming one of them is
1362 /// a narrowing of what was already exact.
1363 fn arm_after_split(&self, key: &[u8], fences: Option<LeafFences>, page: u32, next: u32,
1364 side: impl FnOnce(LeafFences) -> LeafFences) {
1365 if let (Some(tags), Some(f)) = (self.tags, fences) {
1366 tags.arm(self.tree_id, key, page, next, side(f));
1367 }
1368 }
1369
1370 /// Choose the leaf immediately to the left of `min` (or the leftmost leaf
1371 /// when no predecessor exists), copy its encoded records, and allocate the
1372 /// optional right fragment. Everything allocated here is unreachable;
1373 /// the standing tree is read only until `install_graft` runs after an
1374 /// independent verification.
1375 pub(crate) fn plan_graft(&self, min: &[u8]) -> Result<GraftBoundary> {
1376 self.plan_graft_inner(min, true)
1377 }
1378
1379 /// Reconstruct only the standing-tree path needed to publish a candidate
1380 /// that was already packed before a crash. Its right fragment is already
1381 /// part of that verified candidate, so allocating it again would leak a
1382 /// fresh page on every resume-of-resume.
1383 pub(crate) fn plan_existing_graft(&self, min: &[u8]) -> Result<GraftBoundary> {
1384 self.plan_graft_inner(min, false)
1385 }
1386
1387 fn plan_graft_inner(&self, min: &[u8], allocate_right: bool) -> Result<GraftBoundary> {
1388 // The boundary must be the leaf whose INTERVAL owns `min` — which is
1389 // exactly where `descend(min)` lands. An earlier draft descended to the
1390 // PREDECESSOR key's leaf instead; the two agree everywhere except when
1391 // an interior separator equals `min` — then the predecessor sits one
1392 // child to the LEFT, and installing there pushes the packed range past
1393 // that child's separator. A rebuild makes this real: deleting a grafted
1394 // range empties its leaf but deliberately keeps the leaf and its
1395 // separator, so the next graft of the same range anchored left of a
1396 // stale separator that still claimed the interval — the tree then held
1397 // keys its own root said could not be there, and every mid-range seek
1398 // (a spatial cover, a btree range) silently skipped them while full
1399 // scans still saw them. An index must change speed, never the answer.
1400 let (leaf, path) = self.descend_with_path(min)?;
1401 let (records, split, old_next) = {
1402 let r = self.pool.get(leaf)?;
1403 let page = open_cached(&r, leaf)?;
1404 if page.kind() != PageKind::Leaf || page.tree_id() != self.tree_id {
1405 return Err(Error::Corrupt { page_no: leaf, why: "graft boundary is not a tree leaf" });
1406 }
1407 let split = lower_bound(&page, min)?;
1408 let records = (0..page.nentries()).map(|i| page.slot(i).to_vec()).collect::<Vec<_>>();
1409 (records, split, page.next_leaf())
1410 };
1411
1412 let right_page = if allocate_right && split < records.len() {
1413 let mut w = self.pool.allocate()?;
1414 let no = w.page_no();
1415 let page = build_page(
1416 PageKind::Leaf,
1417 self.tree_id,
1418 no,
1419 &records[split..],
1420 old_next,
1421 )?;
1422 w.bytes_mut().copy_from_slice(&page);
1423 Some(no)
1424 } else {
1425 None
1426 };
1427
1428 Ok(GraftBoundary { leaf, path, records, split, old_next, right_page })
1429 }
1430
1431 /// Assemble the verified unit that will replace the boundary leaf. The
1432 /// packed pages themselves are not modified: `pack_range` received the
1433 /// right continuation up front, so every final page is written once.
1434 pub(crate) fn build_graft_candidate(
1435 &self,
1436 boundary: &GraftBoundary,
1437 packed: &crate::bulk::PackedRange,
1438 ) -> Result<GraftCandidate> {
1439 let packed_min = packed.min.as_deref().ok_or(Error::TooLarge)?;
1440 let packed_max = packed.max.as_deref().ok_or(Error::TooLarge)?;
1441 let left = &boundary.records[..boundary.split];
1442 let right = &boundary.records[boundary.split..];
1443
1444 let left_page = if left.is_empty() {
1445 None
1446 } else {
1447 let mut w = self.pool.allocate()?;
1448 let no = w.page_no();
1449 let page = build_page(
1450 PageKind::Leaf,
1451 self.tree_id,
1452 no,
1453 left,
1454 packed.first_leaf,
1455 )?;
1456 w.bytes_mut().copy_from_slice(&page);
1457 Some(no)
1458 };
1459
1460 let root = if left_page.is_none() && boundary.right_page.is_none() {
1461 packed.root
1462 } else {
1463 let child0 = left_page.unwrap_or(packed.root);
1464 let mut entries = Vec::with_capacity(2);
1465 if left_page.is_some() {
1466 entries.push(enc_interior(packed_min, packed.root));
1467 }
1468 if let Some(right_page) = boundary.right_page {
1469 let right_min = validated_key(&right[0]);
1470 entries.push(enc_interior(right_min, right_page));
1471 }
1472 let mut w = self.pool.allocate()?;
1473 let no = w.page_no();
1474 let page = build_page(PageKind::Interior, self.tree_id, no, &entries, child0)?;
1475 w.bytes_mut().copy_from_slice(&page);
1476 no
1477 };
1478
1479 let min = left.first()
1480 .map(|record| validated_key(record).to_vec())
1481 .unwrap_or_else(|| packed_min.to_vec());
1482 let max = right.last()
1483 .map(|record| validated_key(record).to_vec())
1484 .unwrap_or_else(|| packed_max.to_vec());
1485 let rows = packed.rows
1486 .checked_add(boundary.records.len() as u64)
1487 .ok_or(Error::TooLarge)?;
1488
1489 Ok(GraftCandidate {
1490 root,
1491 rows,
1492 min,
1493 max,
1494 last_next: boundary.old_next,
1495 })
1496 }
1497
1498 /// Copy the boundary's parent path bottom-up and patch one child pointer
1499 /// in each fresh page. The only logical mutation is replacing the boundary
1500 /// child with the already-verified candidate; no published page is edited.
1501 pub(crate) fn install_graft(
1502 &mut self,
1503 boundary: &GraftBoundary,
1504 candidate_root: u32,
1505 candidate_min: &[u8],
1506 ) -> Result<Vec<u32>> {
1507 let mut replacement = candidate_root;
1508 let mut expected_child = boundary.leaf;
1509 let mut retired = vec![boundary.leaf];
1510 // With no left record retained, this subtree's minimum becomes the
1511 // packed range's minimum. Carry that change through child0 edges; the
1512 // first non-child0 edge owns the separator that names it.
1513 let mut propagate_min = boundary.split == 0;
1514
1515 for &(parent, child_index) in boundary.path.iter().rev() {
1516 let (child0, mut records) = {
1517 let r = self.pool.get(parent)?;
1518 let page = open_cached(&r, parent)?;
1519 if page.kind() != PageKind::Interior || page.tree_id() != self.tree_id {
1520 return Err(Error::Corrupt { page_no: parent, why: "graft path reaches a non-interior page" });
1521 }
1522 if child_at(self.pool, &page, child_index)? != expected_child {
1523 return Err(Error::Corrupt { page_no: parent, why: "graft path child changed before publication" });
1524 }
1525 (page.child0(), (0..page.nentries())
1526 .map(|i| page.slot(i).to_vec()).collect::<Vec<_>>())
1527 };
1528
1529 let mut w = self.pool.allocate()?;
1530 let no = w.page_no();
1531 let next_child0 = if child_index == 0 {
1532 replacement
1533 } else {
1534 let slot = child_index - 1;
1535 let key = if propagate_min {
1536 candidate_min
1537 } else {
1538 validated_key(&records[slot])
1539 };
1540 records[slot] = enc_interior(key, replacement);
1541 child0
1542 };
1543 let page = build_page(PageKind::Interior, self.tree_id, no, &records, next_child0)?;
1544 w.bytes_mut().copy_from_slice(&page);
1545 if child_index != 0 { propagate_min = false; }
1546 replacement = no;
1547 expected_child = parent;
1548 retired.push(parent);
1549 }
1550 self.root = replacement;
1551 Ok(retired)
1552 }
1553
1554 /// Pack a sorted run into full pages and splice it into an EMPTY key
1555 /// interval of this tree, instead of inserting its keys one at a time.
1556 ///
1557 /// This is the shape a `CREATE INDEX` has: every key of a new index shares
1558 /// one contiguous keyspace (`[tag][index id]...`) that holds nothing yet,
1559 /// so the run has no existing neighbours to interleave with. SQLite builds
1560 /// the same run into a separate, empty index B-tree with a sorter and
1561 /// `OP_IdxInsert`; E4 has one tree per database (D1), so the equivalent is
1562 /// to pack the pages and graft them in.
1563 ///
1564 /// The four steps:
1565 ///
1566 /// 1. **Refuse a non-empty interval.** One seek to `min` and one key
1567 /// comparison. A key in `[min, max]` means the caller's assumption is
1568 /// wrong, and the graft is refused with `RangeNotEmpty` before anything
1569 /// is allocated or written.
1570 /// 2. **Plan the boundary.** `plan_graft` descends to the leaf whose
1571 /// interval owns `min` and splits it at the insertion point: records
1572 /// below `min` become the copied left fragment, records above it the
1573 /// copied right fragment. Both are fresh pages; the standing leaf is
1574 /// read only.
1575 /// 3. **Pack.** `pack_range_pooled` fills leaves to 90% and builds the
1576 /// run's OWN interior levels bottom-up, allocating every page through
1577 /// the ordinary buffer pool. The right continuation is known before the
1578 /// last leaf is written, so the leaf chain is correct on the first and
1579 /// only write of each page.
1580 /// 4. **Verify, then splice.** The packed subtree is walked
1581 /// (`verify_range_pool`) before any standing page is touched. Then one
1582 /// two-or-three-child wrapper joins {left fragment, packed root, right
1583 /// fragment} and `install_graft` copies the O(height) parent path,
1584 /// replacing exactly one child pointer. The retired page numbers are
1585 /// returned for the caller to free.
1586 ///
1587 /// **Interior strategy.** The run brings its own interior levels and
1588 /// enters the standing tree as ONE separator. Inserting one separator per
1589 /// packed leaf was the alternative, and it is O(leaves) interior inserts
1590 /// with their own splits -- the cost this call exists to remove -- so the
1591 /// subtree is grafted whole. The cost is that the grafted subtree's height
1592 /// is independent of the standing tree's, so the tree is no longer
1593 /// uniformly deep; `descend_with_path` already reserves for that.
1594 ///
1595 /// **Page LAYOUT is not preserved, the ENTRY SET is.** Which key sits on
1596 /// which page differs from the one-at-a-time insert path (packed leaves
1597 /// are 90% full; split-built leaves are not), and so do page numbers and
1598 /// tree height. Every persisted key and value is identical, which is what
1599 /// `tests/index_build_equivalence.rs` digests.
1600 ///
1601 /// Nothing here bypasses the log or swaps a root: every page is an
1602 /// ordinary pooled page, so a page-WAL store logs each one as a normal
1603 /// frame and publishes the whole graft with the caller's commit. A crash
1604 /// before that commit leaves the standing tree exactly as it was.
1605 pub fn graft_sorted_range<I>(
1606 &mut self,
1607 sorted: I,
1608 expected_rows: u64,
1609 min: &[u8],
1610 max: &[u8],
1611 scratch_dir: &std::path::Path,
1612 ) -> Result<Vec<u32>>
1613 where I: Iterator<Item = Result<(Vec<u8>, Vec<u8>, bool)>> {
1614 if expected_rows == 0 { return Ok(Vec::new()); }
1615 if min > max { return Err(Error::TooLarge); }
1616 // The bounded probe. `range` seeks once; the first key it returns is
1617 // the smallest at or above `min`, so one comparison settles the whole
1618 // interval.
1619 if let Some(row) = self.range(min)?.next() {
1620 let (key, _) = row?;
1621 if key.as_slice() <= max { return Err(Error::RangeNotEmpty); }
1622 }
1623 let boundary = self.plan_graft(min)?;
1624 let last_next = boundary.right_page.unwrap_or(boundary.old_next);
1625 // Fill factor 1.0, not the whole-tree load's 0.9.
1626 //
1627 // The path this replaces is the per-keyspace APPEND split (D9), which
1628 // leaves each completed leaf of an ascending run essentially full. A
1629 // 90% pack would therefore cost about one leaf in ten MORE than
1630 // inserting the same run key by key, and in a page-WAL store every
1631 // extra page is another logged frame and another page folded at the
1632 // next checkpoint -- measured as a 26% frame increase over the insert
1633 // path at 0.9, which moved a whole checkpoint into the next build
1634 // stage. SQLite's `CREATE INDEX` fills its index pages the same way.
1635 //
1636 // SACRIFICE (Law 4): a packed leaf has no slack, so the first live
1637 // write that lands inside one splits it. That is the ordinary split
1638 // path, once per leaf, and the run was just built from data that was
1639 // already there; the alternative was paying the extra page for every
1640 // leaf up front, whether or not anything ever writes to it.
1641 let packed = crate::bulk::pack_range_pooled(
1642 self.pool, self.tree_id, sorted, 1.0, scratch_dir, last_next)?;
1643 // The stream is the caller's; `pack_range_pooled` recomputes all three
1644 // of these from the bytes it actually packed, and a disagreement means
1645 // the interval that was proved empty is not the interval that was
1646 // packed.
1647 if packed.rows != expected_rows
1648 || packed.min.as_deref() != Some(min)
1649 || packed.max.as_deref() != Some(max) {
1650 return Err(Error::Corrupt { page_no: packed.root,
1651 why: "packed range disagrees with its sorted-stream manifest" });
1652 }
1653 crate::verify::verify_range_pool(self.pool, packed.root, self.tree_id,
1654 packed.rows, min, max, last_next)?;
1655 let candidate = self.build_graft_candidate(&boundary, &packed)?;
1656 let candidate_min = candidate.min.clone();
1657 let retired = self.install_graft(&boundary, candidate.root, &candidate_min)?;
1658 // The append hint names a leaf this graft may have just retired, and a
1659 // retired page number can be handed straight back out by the allocator.
1660 // `fast_path_leaf`'s checks are about shape, not identity, so a
1661 // recycled leaf of the SAME tree could pass all five. The hint has no
1662 // reason to survive a graft; drop it rather than rely on those checks.
1663 self.last_leaf.set(None);
1664 self.forget_tag_hints();
1665 Ok(retired)
1666 }
1667
1668 /// Descend to the leaf that would hold `key`, recording the path.
1669 fn descend(&self, key: &[u8]) -> Result<(u32, Vec<u32>)> {
1670 let mut path = Vec::new();
1671 let mut cur = self.root;
1672 loop {
1673 let r = self.pool.get(cur)?;
1674 let p = open_cached(&r, cur)?;
1675 // PageRef::open proves the page is intact and is the page we asked
1676 // for. It cannot know which TREE we meant, and five trees share this
1677 // file, so a stale root would descend into another tree's pages and
1678 // answer confidently from them.
1679 if p.tree_id() != self.tree_id {
1680 return Err(Error::Corrupt { page_no: cur, why: "page belongs to another tree" });
1681 }
1682 if p.kind() == PageKind::Leaf { return Ok((cur, path)); }
1683 // INTERIOR CONVENTION. Entry i is (min_key_i, child_i), and child_i
1684 // holds keys in [min_key_i, min_key_{i+1}). The LEFTMOST child has
1685 // no minimum and lives in the header as child0, so the slot array
1686 // stays strictly sorted and binary search over it is valid.
1687 //
1688 // A sentinel entry with an empty key placed LAST would NOT be sorted
1689 // — the empty string compares smallest — so binary search would
1690 // silently return the wrong child for any key below the first
1691 // separator. That is why child0 is a header field, not a sentinel.
1692 let i = upper_bound(&p, key)?;
1693 let child = child_at(self.pool, &p, i)?;
1694 path.push(cur);
1695 cur = child;
1696 }
1697 }
1698
1699 /// Descend to the leaf for `key` and, while that leaf is still pinned,
1700 /// answer where in it the scan starts.
1701 ///
1702 /// `range` used to take the path from `descend_with_path` and then pin the
1703 /// SAME leaf a second time purely to run `lower_bound` on it. Every scan in
1704 /// the engine paid that second `pool.get` -- a borrow of the pool's table
1705 /// and a hash lookup -- for a page the descent had open one line earlier.
1706 fn descend_positioned(&self, key: &[u8]) -> Result<(u32, usize, Vec<(u32, usize)>)> {
1707 let mut cur = self.root;
1708 let mut path = Vec::with_capacity(8);
1709 loop {
1710 let r = self.pool.get(cur)?;
1711 let p = open_cached(&r, cur)?;
1712 if p.tree_id() != self.tree_id {
1713 return Err(Error::Corrupt { page_no: cur, why: "page belongs to another tree" });
1714 }
1715 if p.kind() == PageKind::Leaf { return Ok((cur, lower_bound(&p, key)?, path)); }
1716 let i = upper_bound(&p, key)?;
1717 let child = child_at(self.pool, &p, i)?;
1718 path.push((cur, i));
1719 cur = child;
1720 }
1721 }
1722
1723 /// Descend to the leaf for `key`, recording the path as (interior page,
1724 /// chosen child index) pairs for the scan cursor's parent-walk advance.
1725 fn descend_with_path(&self, key: &[u8]) -> Result<(u32, Vec<(u32, usize)>)> {
1726 let mut cur = self.root;
1727 // Grafted subtrees can add a shallow wrapper level. Reserve the
1728 // ordinary maximum up front so a bounded query does not acquire one
1729 // extra heap allocation merely because an index was bulk-built.
1730 let mut path = Vec::with_capacity(8);
1731 loop {
1732 let r = self.pool.get(cur)?;
1733 let p = open_cached(&r, cur)?;
1734 if p.tree_id() != self.tree_id {
1735 return Err(Error::Corrupt { page_no: cur, why: "page belongs to another tree" });
1736 }
1737 if p.kind() == PageKind::Leaf { return Ok((cur, path)); }
1738 let i = upper_bound(&p, key)?;
1739 let child = child_at(self.pool, &p, i)?;
1740 path.push((cur, i));
1741 cur = child;
1742 }
1743 }
1744
1745 pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
1746 let (leaf, _) = self.descend(key)?;
1747 let r = self.pool.get(leaf)?;
1748 let p = open_cached(&r, leaf)?;
1749 let i = lower_bound(&p, key)?;
1750 if i < p.nentries() {
1751 let rec = p.slot(i);
1752 let (stored_key, value, overflow) = validated_leaf(rec);
1753 if stored_key != key { return Ok(None); }
1754 if overflow {
1755 let marker = value.to_vec();
1756 drop(r); // release the leaf before walking the chain
1757 return Ok(Some(read_overflow(self.pool, &marker)?));
1758 }
1759 return Ok(Some(value.to_vec()));
1760 }
1761 Ok(None)
1762 }
1763
1764 /// Walk root -> leaf taking READ guards on interior pages, dropping each
1765 /// as we step down (interiors are not modified by an insert), and end
1766 /// holding a single WRITE guard on the leaf. That one guard is carried
1767 /// through `insert_into_leaf` and, if needed, `split_leaf_and_insert`,
1768 /// so a logical insert acquires the leaf exactly once instead of the
1769 /// three separate acquisitions the old `descend` + re-`get`/`get_mut`
1770 /// shape required — each of which made the page evictable in between.
1771 fn descend_for_write(&mut self, key: &[u8]) -> Result<(PinnedWrite<'p>, Vec<u32>)> {
1772 let (w, path, _) = self.descend_for_write_fenced(key)?;
1773 Ok((w, path))
1774 }
1775
1776 /// The same descent, also reporting the leaf's FENCES: the separators
1777 /// immediately below and above the leaf it lands on.
1778 ///
1779 /// They are free here. `upper_bound` already chose a child index at every
1780 /// interior page; the separator at that index is the first key of the
1781 /// subtree to the right, and the one before it is the first key of this
1782 /// subtree. The innermost level where the chosen child was not the last
1783 /// gives the tightest upper fence; where it WAS the last, the fence is
1784 /// inherited from further up, and a path that was last-child the whole way
1785 /// down ends at the tree's rightmost leaf, which has no upper fence at all.
1786 ///
1787 /// This is exactly the interval `upper_bound` will route to this leaf, so a
1788 /// key inside it needs no descent to find its page -- which is what
1789 /// `TagHints` remembers.
1790 fn descend_for_write_fenced(&mut self, key: &[u8])
1791 -> Result<(PinnedWrite<'p>, Vec<u32>, LeafFences)> {
1792 // 2f: this is THE shadowed descent. Every page on the write path is
1793 // relocated to a fresh number if it belongs to the published epoch,
1794 // top-down, so by the time any mutation below runs -- the leaf edit,
1795 // and every `get_mut(parent)` a split performs on the `path` pages --
1796 // it lands on this epoch's copies only. The published root and its
1797 // pages stay byte-identical for snapshot readers.
1798 if self.pool.is_frozen(self.root) {
1799 self.root = shadow_page(self.pool, self.root)?;
1800 }
1801 let mut path = Vec::new();
1802 let mut cur = self.root;
1803 let mut fences = LeafFences::NONE;
1804 loop {
1805 let (is_leaf, frozen_child) = {
1806 let r = self.pool.get(cur)?;
1807 let p = open_cached(&r, cur)?;
1808 if p.tree_id() != self.tree_id {
1809 return Err(Error::Corrupt { page_no: cur, why: "page belongs to another tree" });
1810 }
1811 if p.kind() == PageKind::Leaf {
1812 (true, None)
1813 } else {
1814 let i = upper_bound(&p, key)?;
1815 // Narrow the fences by this level's separators before
1816 // stepping down. Child `i` holds the keys between slot
1817 // `i - 1` (inclusive) and slot `i` (exclusive).
1818 if i > 0 { fences.narrow_lower(validated_key(p.slot(i - 1))); }
1819 if i < p.nentries() { fences.narrow_upper(validated_key(p.slot(i))); }
1820 let child = child_at(self.pool, &p, i)?;
1821 if self.pool.is_frozen(child) {
1822 (false, Some((i, child)))
1823 } else {
1824 path.push(cur);
1825 cur = child;
1826 (false, None)
1827 }
1828 }
1829 }; // `r` dropped here, before either looping again or taking get_mut
1830 if let Some((i, child)) = frozen_child {
1831 let fresh = shadow_page(self.pool, child)?;
1832 patch_child(self.pool, cur, i, fresh)?;
1833 path.push(cur);
1834 cur = fresh;
1835 continue;
1836 }
1837 if is_leaf {
1838 let w = self.pool.get_mut(cur)?;
1839 return Ok((w, path, fences));
1840 }
1841 }
1842 }
1843
1844 /// Try to append `rec` to a leaf a `TagHints` slot named, skipping the
1845 /// descent. The caller has already established, IN MEMORY, that `key` sits
1846 /// inside the fences recorded when the slot was armed; this establishes the
1847 /// rest against the page itself.
1848 ///
1849 /// THE FENCE RULE, in full. `key` may go into this leaf without a descent
1850 /// when all of these hold:
1851 /// * ROUTING, established by the caller in memory from the cached
1852 /// fences: lower fence <= `key` < upper fence, the lower inclusive and
1853 /// the upper STRICTLY exclusive. Strictly, because `upper_bound`
1854 /// routes a key equal to a separator to the child on its RIGHT: a
1855 /// record placed here instead stays visible to a scan and disappears
1856 /// from every `get`. An absent upper fence means the tree's rightmost
1857 /// leaf, which has no separator above it.
1858 /// * IDENTITY: a live (unfrozen) leaf page of THIS tree whose
1859 /// `next_leaf` is still the one the hint recorded. The link is what
1860 /// makes a split of this very leaf -- the one event that subdivides
1861 /// the cached interval -- visible without re-descending, and it is
1862 /// also the evidence that the page number was not recycled underneath
1863 /// the hint.
1864 /// * ROOM for the record and its slot, and the leaf is not empty: an
1865 /// empty leaf is no evidence a key belongs to it, the trap
1866 /// `fast_path_leaf`'s check 5 documents.
1867 /// * `key` IS NOT ALREADY THERE. A replacement has to retire the old
1868 /// record's overflow pages and reclaim its payload bytes, which is
1869 /// `insert_into_leaf`'s work, not this one's.
1870 ///
1871 /// AND NOTHING ABOUT POSITION. The record goes in at `lower_bound`'s slot,
1872 /// wherever that is, because the fences alone already say the key belongs
1873 /// to this leaf and nowhere else. Two earlier versions of this check asked
1874 /// for more and both were wrong to:
1875 /// * "after the leaf's LAST record" never fires on a boundary leaf. One
1876 /// leaf per keyspace boundary holds the end of one tag and the start
1877 /// of the next, and for the tag below, that leaf is exactly where its
1878 /// run ends -- its appends land BEFORE the higher tag's records. The
1879 /// tag that most needs this hint (0x71 forward edges, with 0x72
1880 /// reverse edges above them) armed nothing at all: measured 0 hits in
1881 /// 200,000 edge puts, 4.40 page accesses each.
1882 /// * "after the last record OF ITS OWN TAG" fires on a boundary leaf but
1883 /// not on a near-ascending run. The reverse-edge rows go to
1884 /// destinations (i+1) and (i+7): the second is the tag's new maximum,
1885 /// the first is six keys below it, so two puts in three landed before
1886 /// a record of their own tag and refused a leaf they belonged in.
1887 /// Measured: 4.357 page accesses per reverse row, unchanged.
1888 ///
1889 /// Returns the slot to insert at.
1890 fn fast_path_tag_leaf(&self, hint: u32, next: u32, key: &[u8], rec: &[u8])
1891 -> Result<Option<(PinnedWrite<'p>, usize)>> {
1892 if self.pool.is_frozen(hint) { return Ok(None); }
1893 let w = self.pool.get_mut(hint)?;
1894 let at = {
1895 let p = PageRef::open_resident(w.bytes(), hint)?;
1896 validate_records(&p)?;
1897 if p.kind() != PageKind::Leaf
1898 || p.tree_id() != self.tree_id
1899 || p.next_leaf() != next
1900 || p.free_space() < rec.len() + 4
1901 || p.nentries() == 0
1902 {
1903 None
1904 } else {
1905 // `lower_bound` puts `at` at the first record >= `key`, so
1906 // inserting there keeps the leaf sorted, and everything before
1907 // it is already strictly below.
1908 let at = lower_bound(&p, key)?;
1909 let fresh = at == p.nentries() || validated_key(p.slot(at)) != key;
1910 fresh.then_some(at)
1911 }
1912 };
1913 Ok(at.map(|at| (w, at)))
1914 }
1915
1916 /// Try to insert directly into the cached last-written leaf, skipping
1917 /// the descent entirely. Every one of these five checks is load-bearing
1918 /// (Task 15 brief): a wrong hint may cost a fallback descent, but must
1919 /// never place a record wrongly or lose one (Law 3). On any failure the
1920 /// write guard, if taken, is dropped here and `None` is returned so the
1921 /// caller falls back to `descend_for_write`.
1922 ///
1923 /// SACRIFICE (Law 4): the hint can be stale (rightmost leaf changed,
1924 /// wrong tree, no room, key not actually the new maximum). Bought: an
1925 /// ascending-key workload — the common case for autoincrementing IDs and
1926 /// time-ordered writes — inserts with zero interior-page traffic instead
1927 /// of a full root-to-leaf walk per row.
1928 fn fast_path_leaf(&self, hint: u32, key: &[u8], rec: &[u8]) -> Result<Option<PinnedWrite<'p>>> {
1929 // check 0 (2f): a frozen hint belongs to the published epoch; fall
1930 // back to the shadowed descent, which relocates it properly.
1931 if self.pool.is_frozen(hint) { return Ok(None); }
1932 let w = self.pool.get_mut(hint)?;
1933 let fits = {
1934 let p = match PageRef::open_resident(w.bytes(), hint) {
1935 Ok(p) => p,
1936 Err(e) => return Err(e),
1937 };
1938 validate_records(&p)?;
1939 p.kind() == PageKind::Leaf // check 1
1940 && p.tree_id() == self.tree_id // check 2
1941 && p.next_leaf() == 0 // check 3: rightmost
1942 && p.free_space() >= rec.len() + 4 // check 4: room
1943 // check 5: strictly greater. An EMPTY leaf must NOT pass:
1944 // emptiness is not evidence the key belongs here. Delete every
1945 // key and the rightmost leaf is empty while separators still
1946 // route low keys left of it -- keys appended here become
1947 // reachable by scan but not by get (measured: 27/20,000 lost).
1948 && p.nentries() > 0
1949 && key > validated_key(p.slot(p.nentries() - 1))
1950 };
1951 Ok(if fits { Some(w) } else { None })
1952 }
1953
1954 /// Insert `rec` at the end of an already-validated rightmost leaf. Every
1955 /// `fast_path_leaf` check that ran before this was called guarantees
1956 /// `insert_slot` fits, so there is no split path here.
1957 fn insert_fast(&mut self, mut w: PinnedWrite<'p>, rec: Vec<u8>) -> Result<()> {
1958 #[cfg(feature = "write-trace")]
1959 let trace_started = crate::write_trace::active().then(std::time::Instant::now);
1960 let leaf = w.page_no();
1961 let mut p = PageMut::reopen(w.bytes_mut());
1962 let at = p.nentries_pub();
1963 p.insert_slot(at, &rec)?;
1964 #[cfg(feature = "write-trace")]
1965 if trace_started.is_some() { crate::write_trace::value_copy(); }
1966 p.finalise(0);
1967 drop(p);
1968 self.last_leaf.set(Some(leaf));
1969 #[cfg(feature = "write-trace")]
1970 if let Some(started) = trace_started {
1971 crate::write_trace::add(crate::write_trace::Field::LeafInsert, started.elapsed());
1972 }
1973 Ok(())
1974 }
1975
1976 pub fn insert(&mut self, key: &[u8], val: &[u8]) -> Result<()> {
1977 // The one choke point: put(), WAL replay and vacuum all insert here,
1978 // so spilling here means none of them can disagree about when a value
1979 // overflows. The KEY always stays in the leaf.
1980 #[cfg(feature = "write-trace")]
1981 let encode_started = crate::write_trace::active().then(std::time::Instant::now);
1982 let rec = if 4 + key.len() + val.len() > crate::page::MAX_RECORD_LEN {
1983 let (head, crc) = write_overflow(self.pool, val)?;
1984 enc_leaf_marker(key, &enc_marker(val.len() as u32, head, crc))
1985 } else {
1986 enc_leaf(key, val, self.pool.compact_cells())
1987 };
1988 #[cfg(feature = "write-trace")]
1989 if let Some(started) = encode_started {
1990 crate::write_trace::add(crate::write_trace::Field::RecordEncode, started.elapsed());
1991 if 4 + key.len() + val.len() <= crate::page::MAX_RECORD_LEN {
1992 crate::write_trace::value_copy();
1993 }
1994 }
1995 if rec.len() > crate::page::MAX_RECORD_LEN { return Err(Error::TooLarge); }
1996
1997 // Append fast path: try the last leaf we wrote before paying for a
1998 // root-to-leaf descent (PostgreSQL's `RelationGetTargetBlock`).
1999 #[cfg(feature = "write-trace")]
2000 let descent_started = crate::write_trace::active().then(std::time::Instant::now);
2001 // The per-keyspace hint goes first, and when it claims this key the
2002 // whole-tree hint is not probed at all. Probing both would be worse
2003 // than probing neither: `last_leaf` names the rightmost leaf of the
2004 // TREE, so every insert under a lower tag would fail it, disarm it,
2005 // and force the next insert under the top tag to re-descend -- two
2006 // interleaved ascending runs would knock each other's hint out
2007 // forever. Whichever hint owns the key owns the probe.
2008 if let Some(tags) = self.tags {
2009 let claimed = tags.find(self.tree_id, key);
2010 if claimed.is_none() { tags.misses.set(tags.misses.get() + 1); }
2011 if let Some((hint, next)) = claimed {
2012 tags.attempts.set(tags.attempts.get() + 1);
2013 if let Some((w, at)) = self.fast_path_tag_leaf(hint, next, key, &rec)? {
2014 tags.hits.set(tags.hits.get() + 1);
2015 #[cfg(feature = "write-trace")]
2016 if let Some(started) = descent_started {
2017 crate::write_trace::add(crate::write_trace::Field::Descent, started.elapsed());
2018 }
2019 return self.insert_tag_fast(w, at, rec);
2020 }
2021 // Only THIS slot was wrong (the leaf filled, or a key arrived
2022 // out of order inside its own run). The other fifteen describe
2023 // other runs and stay; the descent below re-arms this one.
2024 tags.forget_leaf(self.tree_id, hint);
2025 let (w, path, fence) = self.descend_for_write_fenced(key)?;
2026 #[cfg(feature = "write-trace")]
2027 if let Some(started) = descent_started {
2028 crate::write_trace::add(crate::write_trace::Field::Descent, started.elapsed());
2029 }
2030 return self.insert_into_leaf(w, path, key, rec, Some(fence));
2031 }
2032 }
2033 if let Some(hint) = self.last_leaf.get() {
2034 self.fast_path_attempts.set(self.fast_path_attempts.get() + 1);
2035 if let Some(w) = self.fast_path_leaf(hint, key, &rec)? {
2036 self.fast_path_hits.set(self.fast_path_hits.get() + 1);
2037 #[cfg(feature = "write-trace")]
2038 if let Some(started) = descent_started {
2039 crate::write_trace::add(crate::write_trace::Field::Descent, started.elapsed());
2040 }
2041 return self.insert_fast(w, rec);
2042 }
2043 // PostgreSQL's `_bt_search_insert`, on ANY fast-path rejection,
2044 // calls `RelationSetTargetBlock(rel, InvalidBlockNumber)` --
2045 // forgets the hint rather than retrying it next time. Copy that:
2046 // one failure disarms it, so a random-order workload pays for
2047 // exactly one wasted probe (this one) and then nothing more,
2048 // instead of paying a `get_mut` plus a full CRC verify on every
2049 // single insert forever. It is re-armed below, in
2050 // `insert_into_leaf`, only when an insert actually lands on the
2051 // rightmost leaf.
2052 //
2053 // SACRIFICE (Law 4): a workload that keeps switching between
2054 // append order and random order pays one wasted probe at every
2055 // switch -- the disarm is not free, it is just no longer
2056 // permanent.
2057 self.last_leaf.set(None);
2058 }
2059
2060 let (w, path, fence) = self.descend_for_write_fenced(key)?;
2061 #[cfg(feature = "write-trace")]
2062 if let Some(started) = descent_started {
2063 crate::write_trace::add(crate::write_trace::Field::Descent, started.elapsed());
2064 }
2065 self.insert_into_leaf(w, path, key, rec, Some(fence))
2066 }
2067
2068 /// Place a record in a leaf a tag hint named, at the slot the probe
2069 /// found. The slot already describes this leaf, so nothing is re-armed;
2070 /// unlike `insert_fast` this must NOT touch `last_leaf`, because the leaf
2071 /// is rightmost of its own tag, not of the tree.
2072 fn insert_tag_fast(&mut self, mut w: PinnedWrite<'p>, at: usize, rec: Vec<u8>) -> Result<()> {
2073 #[cfg(feature = "write-trace")]
2074 let trace_started = crate::write_trace::active().then(std::time::Instant::now);
2075 let mut p = PageMut::reopen(w.bytes_mut());
2076 p.insert_slot(at, &rec)?;
2077 #[cfg(feature = "write-trace")]
2078 if trace_started.is_some() { crate::write_trace::value_copy(); }
2079 p.finalise(0);
2080 #[cfg(feature = "write-trace")]
2081 if let Some(started) = trace_started {
2082 crate::write_trace::add(crate::write_trace::Field::LeafInsert, started.elapsed());
2083 }
2084 Ok(())
2085 }
2086
2087 fn insert_into_leaf(&mut self, mut w: PinnedWrite<'p>, path: Vec<u32>, key: &[u8], rec: Vec<u8>,
2088 fences: Option<LeafFences>) -> Result<()> {
2089 let leaf = w.page_no();
2090 #[cfg(feature = "write-trace")]
2091 let search_started = crate::write_trace::active().then(std::time::Instant::now);
2092 let (i, exists, is_rightmost, is_next, retired) = {
2093 // Not "just written": this guard came straight from
2094 // `descend_for_write`'s `get_mut`, which never verifies a CRC.
2095 // This is the one read that must.
2096 let p = PageRef::open_resident(w.bytes(), leaf)?;
2097 validate_records(&p)?;
2098 let i = lower_bound(&p, key)?;
2099 let exists = i < p.nentries()
2100 && validated_key(p.slot(i)) == key;
2101 // `descend_for_write` only ever hands back a write guard for a
2102 // page it already confirmed was a leaf (the interior branch
2103 // never breaks out of its loop), so `next_leaf == 0` here means
2104 // "rightmost leaf", not "leftmost child of an interior page" --
2105 // the ambiguity that field carries on a page whose kind hasn't
2106 // been checked yet. Read once, here, before anything below
2107 // mutates the page: neither `remove_slot`/`compact` nor
2108 // `insert_slot` ever touch this field, so it stays valid for the
2109 // whole function.
2110 let is_next = p.next_leaf();
2111 let is_rightmost = is_next == 0;
2112
2113 let retired = if exists { replaced_overflow_pages(self.pool, p.slot(i))? } else { Vec::new() };
2114 (i, exists, is_rightmost, is_next, retired)
2115 };
2116 #[cfg(feature = "write-trace")]
2117 if let Some(started) = search_started {
2118 crate::write_trace::add(crate::write_trace::Field::LeafSearch, started.elapsed());
2119 }
2120
2121 // Closed operation 1: if the key already exists, remove and compact
2122 // as one step, finalising before we ever ask whether the NEW record
2123 // fits. `compact` reclaims the old entry's payload bytes, not just
2124 // its slot; without this as a separate finalised step, an insert
2125 // that then failed to fit would leave the page dirty with a
2126 // checksum matching neither the old contents (already mutated) nor
2127 // the new (never written) — and the next `PageRef::open` on this
2128 // leaf, for ANY key, refuses the whole page.
2129 if exists {
2130 let mut p = PageMut::reopen(w.bytes_mut());
2131 p.remove_slot(i);
2132 p.compact();
2133 p.finalise(0);
2134 drop(p);
2135 for (no, birth) in retired { self.pool.free_shadow_page(no, birth)?; }
2136 }
2137
2138 // Closed operation 2: decide room from the page's REAL free space —
2139 // not a sum over live entry lengths, which diverges from the truth
2140 // the moment anything has ever been deleted from this page — then
2141 // insert as its own finalised step. `i` is still the correct
2142 // insertion point: if the key existed, entry `i` is exactly what we
2143 // just removed, so nothing before or after it moved. No fresh
2144 // `PageRef::open` here: we either just finalised this page
2145 // ourselves (the `exists` branch) or verified it above and have not
2146 // touched it since — both leave the CRC known-good without paying
2147 // for another ~254ns verification pass.
2148 // remove_slot frees the slot entry, not the payload bytes; only
2149 // compact() recovers them. An emptied leaf can report nentries()==0 AND
2150 // free_space()==0 -- empty and full at once -- sending a single record
2151 // to the split path, which cannot halve one record (TooLarge on an
2152 // empty page). Compact and re-check before concluding there is no room.
2153 let mut room = PageMut::reopen(w.bytes_mut()).free_space() >= rec.len() + 4;
2154 if !room {
2155 let mut p = PageMut::reopen(w.bytes_mut());
2156 p.compact();
2157 p.finalise(0);
2158 drop(p);
2159 room = PageMut::reopen(w.bytes_mut()).free_space() >= rec.len() + 4;
2160 }
2161 if room {
2162 #[cfg(feature = "write-trace")]
2163 let insert_started = crate::write_trace::active().then(std::time::Instant::now);
2164 let mut p = PageMut::reopen(w.bytes_mut());
2165 p.insert_slot(i, &rec)?;
2166 #[cfg(feature = "write-trace")]
2167 if insert_started.is_some() { crate::write_trace::value_copy(); }
2168 p.finalise(0);
2169 // Arm only when this write actually landed on the rightmost
2170 // leaf. A descent can insert into ANY leaf -- most of them not
2171 // rightmost -- and re-arming unconditionally (the pre-Task-18
2172 // bug) is what let a random-order workload thrash forever: every
2173 // wrong hint just gets replaced with another wrong hint instead
2174 // of staying disarmed.
2175 if is_rightmost {
2176 self.last_leaf.set(Some(leaf));
2177 }
2178 // Arm the per-keyspace hint whether or not this leaf is the
2179 // tree's rightmost -- that is the whole point -- and wherever in
2180 // the leaf the record landed. The hint says "this tag's writes
2181 // are currently in this leaf, between these two separators",
2182 // which is true of any insert that did not split; the next key of
2183 // the tag is probed against the FENCES, so a hint armed from a
2184 // mid-leaf insert is not a worse guess than one armed from an
2185 // append. It is only not armed when the insert split, because a
2186 // split moves the separator this leaf sits under.
2187 if let (Some(tags), Some(fences)) = (self.tags, fences) {
2188 tags.arm(self.tree_id, key, leaf, is_next, fences);
2189 }
2190 #[cfg(feature = "write-trace")]
2191 if let Some(started) = insert_started {
2192 crate::write_trace::add(crate::write_trace::Field::LeafInsert, started.elapsed());
2193 }
2194 return Ok(());
2195 }
2196 #[cfg(feature = "write-trace")]
2197 let split_started = crate::write_trace::active().then(std::time::Instant::now);
2198 let result = self.split_leaf_and_insert(w, path, i, rec, key, fences);
2199 #[cfg(feature = "write-trace")]
2200 if let Some(started) = split_started {
2201 crate::write_trace::add(crate::write_trace::Field::Split, started.elapsed());
2202 crate::write_trace::value_copy();
2203 }
2204 result
2205 }
2206
2207 /// Split dispatcher. Both bodies take the same decisions and write the
2208 /// same bytes; they differ only in how the records are MOVED (`_vec`
2209 /// copies every record into its own `Vec<u8>`, `_ref` points at them in
2210 /// place). Both are compiled under `cfg(test)` so the byte-equivalence
2211 /// oracle can run one against the other in a single build.
2212 fn split_leaf_and_insert(&mut self, w: PinnedWrite<'p>, path: Vec<u32>, at: usize, rec: Vec<u8>,
2213 key: &[u8], fences: Option<LeafFences>) -> Result<()> {
2214 // ONLY this leaf's fences. A split subdivides the interval of the leaf
2215 // it splits and inserts the new separator inside it; every other
2216 // leaf's interval is untouched, and a root split merely adds a level
2217 // above separators that do not move. Clearing the whole cache here
2218 // instead was measured at 1.410 page accesses per forward-edge insert
2219 // against 1.037 for this: a 0x40 leaf fills every fifteen documents,
2220 // and each of those splits was knocking out the edge run's hint.
2221 // (The neighbour redistribution inside DOES move other leaves'
2222 // separators, and clears their slots itself.)
2223 if let Some(t) = self.tags { t.forget_leaf(self.tree_id, w.page_no()); }
2224 #[cfg(not(feature = "slotref-split"))]
2225 { self.split_leaf_and_insert_vec(w, path, at, rec, key, fences) }
2226 #[cfg(feature = "slotref-split")]
2227 { self.split_leaf_and_insert_ref(w, path, at, rec, key, fences) }
2228 }
2229
2230 #[cfg(any(test, not(feature = "slotref-split")))]
2231 #[allow(clippy::too_many_arguments)]
2232 fn split_leaf_and_insert_vec(&mut self, mut w: PinnedWrite<'p>, mut path: Vec<u32>, at: usize,
2233 rec: Vec<u8>, key: &[u8], fences: Option<LeafFences>) -> Result<()> {
2234 #[cfg(feature = "write-trace")]
2235 if crate::write_trace::active() { crate::write_trace::leaf_split(); }
2236 let leaf = w.page_no();
2237 // Collect, insert into the collected list, then redistribute. Bounded by
2238 // one page, so this is RAM proportional to the change, not the store.
2239 // One `PageRef::open` reads both the entries and `next_leaf` -- the old
2240 // shape spent a second full acquisition-and-verify on the same page just
2241 // to learn one more field of the header it had already opened.
2242 let (mut recs, old_next): (Vec<Vec<u8>>, u32) = {
2243 let p = PageRef::open_resident(w.bytes(), leaf)?;
2244 validate_records(&p)?;
2245 ((0..p.nentries()).map(|j| p.slot(j).to_vec()).collect(), p.next_leaf())
2246 };
2247 recs.insert(at, rec);
2248 // Split AT the insertion point when the left page ends up well filled.
2249 //
2250 // A 50/50 split of an ascending run abandons every left half at 50%
2251 // forever -- measured 0.458 utilisation, the file ~2x its needed size.
2252 // Splitting at the insertion point (SQLite balance_quick, InnoDB
2253 // sequential-insert) closes each left page full. "Append = last in
2254 // leaf" is too narrow when several keyspaces share the tree: the top
2255 // leaf of one space also holds the next space's keys, so ascending
2256 // inserts land BEFORE them, never last.
2257 //
2258 // The >= 2/3 guard is what separates workloads without detecting them:
2259 // ascending leaves the left ~full, scattered ~half -- and unguarded,
2260 // scattered amplification measurably worsened (2955.8 -> 3511.0
2261 // bytes/row), caught by a pinned test, so scattered keeps the
2262 // balanced split.
2263 let usable = PAGE_SIZE - crate::page::HEADER_LEN;
2264 let bytes = |r: &[Vec<u8>]| r.iter().map(|x| x.len() + 4).sum::<usize>();
2265 let left_bytes = bytes(&recs[..at.min(recs.len())]);
2266 // The unambiguous append signal: the new record is LAST in the leaf and
2267 // the leaf is RIGHTMOST (old_next == 0). Local byte-guards could not
2268 // separate the workloads -- left>=2/3 admitted scattered splits (file
2269 // 156 -> 182 MiB), and a small-right-count guard still left it at 174
2270 // (a random key is a new leaf-local max in ~1/nentries of splits, each
2271 // stranding a nearly-empty right page). Rightmost-append never fires
2272 // mid-tree under random keys, and is exactly SQLite's balance_quick
2273 // condition.
2274 //
2275 // `keyspace-append` widens "rightmost" from the TREE to the KEY TAG.
2276 // One collection is three ascending runs in one tree (D3/D4: vector
2277 // 0x60, row 0x40, external-key mapping 0x20), written one document at a
2278 // time, so only the top tag is ever the tree's rightmost leaf and the
2279 // other two never qualified -- see `keyspace_rightmost` below.
2280 let at_tail = at > 0
2281 && at == recs.len() - 1
2282 && left_bytes <= usable
2283 && bytes(&recs[at..]) <= usable;
2284 let at_point = at_tail
2285 && (old_next == 0 || self.keyspace_rightmost(&path, leaf, old_next, &recs[at])?);
2286 // SQLite's balance_nonroot uses neighboring child pages before growing
2287 // the tree. Reuse capacity in at most three existing leaves before
2288 // splitting. An append we have already decided to take skips it: moving
2289 // a closed run's records between siblings is the work this whole path
2290 // exists to avoid, and it cannot add capacity the append does not need.
2291 #[cfg(feature = "sqlite-balance")]
2292 if !at_point && (old_next != 0 || at + 1 != recs.len()) {
2293 if self.redistribute_neighbors_vec(&mut w, &path, &recs, old_next)? {
2294 return Ok(());
2295 }
2296 }
2297 let sp = if at_point {
2298 at
2299 } else if let Some(sp) = split_point(&recs, usable) {
2300 sp
2301 } else {
2302 // A large indivisible record between two existing runs can need
2303 // THREE leaves even though the total is below two-page capacity.
2304 // Greedy byte packing of one old page plus one new record needs
2305 // at most three groups; every individual cell was already bounded.
2306 let mut cuts=vec![0];let mut used=0;
2307 for (i,r) in recs.iter().enumerate() {
2308 if used+r.len()+4>usable {cuts.push(i);used=0;}
2309 used+=r.len()+4;
2310 }
2311 cuts.push(recs.len());
2312 if cuts.len()!=4 || cuts.windows(2).any(|w|w[0]==w[1]) {return Err(Error::TooLarge);}
2313 let mut middle=self.pool.allocate()?;let mid_no=middle.page_no();
2314 let mut right=self.pool.allocate()?;let right_no=right.page_no();
2315 let left_image=build_page(PageKind::Leaf,self.tree_id,leaf,&recs[..cuts[1]],mid_no)?;
2316 let mid_image=build_page(PageKind::Leaf,self.tree_id,mid_no,&recs[cuts[1]..cuts[2]],right_no)?;
2317 let right_image=build_page(PageKind::Leaf,self.tree_id,right_no,&recs[cuts[2]..],old_next)?;
2318 let mid_sep=validated_key(&recs[cuts[1]]).to_vec();let right_sep=validated_key(&recs[cuts[2]]).to_vec();
2319 middle.bytes_mut().copy_from_slice(&mid_image);right.bytes_mut().copy_from_slice(&right_image);w.bytes_mut().copy_from_slice(&left_image);
2320 drop((middle,right,w));self.last_leaf.set(None);
2321 self.insert_separator(&mut path,&mid_sep,mid_no)?;
2322 // The first fence can split its ancestors, so reacquire the second
2323 // fence's path from the new root instead of reusing stale parents.
2324 let (guard,mut new_path)=self.descend_for_write(&right_sep)?;drop(guard);
2325 self.insert_separator(&mut new_path,&right_sep,right_no)?;
2326 if old_next==0 {self.last_leaf.set(Some(right_no));}
2327 if at<cuts[1] {self.arm_after_split(key,fences,leaf,mid_no,|f|f.below(&mid_sep));}
2328 else if at<cuts[2] {self.arm_after_split(key,fences,mid_no,right_no,
2329 |f|f.above(&mid_sep).below(&right_sep));}
2330 else {self.arm_after_split(key,fences,right_no,old_next,|f|f.above(&right_sep));}
2331 return Ok(());
2332 };
2333 let right_recs = recs.split_off(sp);
2334 let sep = validated_key(&right_recs[0]).to_vec();
2335
2336 let right_no = {
2337 let mut rw = self.pool.allocate()?;
2338 let no = rw.page_no();
2339 let page = build_page(PageKind::Leaf, self.tree_id, no, &right_recs, old_next)?;
2340 rw.bytes_mut().copy_from_slice(&page);
2341 no
2342 };
2343
2344 {
2345 // Built before the frame is overwritten, so a failure cannot
2346 // leave the live leaf wiped.
2347 let page = build_page(PageKind::Leaf, self.tree_id, leaf, &recs, right_no)?;
2348 w.bytes_mut().copy_from_slice(&page);
2349 }
2350 drop(w); // release the leaf pin before recursing into the parent chain
2351
2352 // The append hint: meaningful only when this split happened at the
2353 // tail of the leaf chain (`old_next == 0`), in which case `right_no`
2354 // is now the rightmost leaf and the next ascending insert should try
2355 // it first. A split elsewhere in the tree leaves whatever hint
2356 // already existed exactly as valid, or invalid, as it was — so it is
2357 // left untouched rather than being overwritten with a leaf that
2358 // isn't actually rightmost.
2359 if old_next == 0 {
2360 self.last_leaf.set(Some(right_no));
2361 }
2362
2363 self.insert_separator(&mut path, &sep, right_no)?;
2364 if at < sp {
2365 self.arm_after_split(key, fences, leaf, right_no, |f| f.below(&sep));
2366 } else {
2367 self.arm_after_split(key, fences, right_no, old_next, |f| f.above(&sep));
2368 }
2369 Ok(())
2370 }
2371
2372 /// L2.3-2: the same split, with every record left where it already is.
2373 ///
2374 /// Identical to `split_leaf_and_insert_vec` in every decision it takes and
2375 /// every byte it writes -- see the module comment above `SlotRef` and the
2376 /// `split_byte_equivalence` oracle. The difference is that the leaf's
2377 /// records are named by `SlotRef`s into one 4 KiB copy of the page image
2378 /// instead of being copied into one `Vec<u8>` each.
2379 #[cfg(any(test, feature = "slotref-split"))]
2380 #[allow(clippy::too_many_arguments)]
2381 fn split_leaf_and_insert_ref(&mut self, w: PinnedWrite<'p>, path: Vec<u32>, at: usize,
2382 rec: Vec<u8>, key: &[u8], fences: Option<LeafFences>) -> Result<()> {
2383 // Re-entrancy: nothing `split_core` can reach -- `insert_separator`,
2384 // `descend_for_write`, `shadow_page`, `redistribute_neighbors_ref` --
2385 // splits a LEAF, so these three borrows cannot nest. That is what lets
2386 // the separator key stay a borrow into scratch instead of a `Vec`.
2387 SPLIT_LEAF.with(|li| SPLIT_SLOTS.with(|sl| SPLIT_SCRATCH.with(|ss| {
2388 let mut leaf_img = li.borrow_mut();
2389 let mut slots = sl.borrow_mut();
2390 let mut sc = ss.borrow_mut();
2391 self.split_core(w, path, at, &rec, key, fences, &mut leaf_img, &mut slots, &mut sc)
2392 })))
2393 }
2394
2395 #[cfg(any(test, feature = "slotref-split"))]
2396 #[allow(clippy::too_many_arguments)]
2397 fn split_core(
2398 &mut self,
2399 mut w: PinnedWrite<'p>,
2400 mut path: Vec<u32>,
2401 at: usize,
2402 rec: &[u8],
2403 key: &[u8],
2404 fences: Option<LeafFences>,
2405 leaf_img: &mut [u8],
2406 slots: &mut Vec<SlotRef>,
2407 sc: &mut SplitScratch,
2408 ) -> Result<()> {
2409 const P: usize = PAGE_SIZE;
2410 #[cfg(feature = "write-trace")]
2411 if crate::write_trace::active() { crate::write_trace::leaf_split(); }
2412 let leaf = w.page_no();
2413
2414 // One `PageRef::open` reads the records' addresses and `next_leaf`.
2415 // The image is then copied into scratch: the write pin stays held, but
2416 // its bytes cannot be borrowed across the `&mut w` redistribution
2417 // needs, and one 4 KiB memcpy is cheaper than ~86 allocations.
2418 let old_next = {
2419 let p = PageRef::open_resident(w.bytes(), leaf)?;
2420 validate_records(&p)?;
2421 slots.clear();
2422 for i in 0..p.nentries() {
2423 let (off, len) = p.slot_bounds(i);
2424 slots.push(SlotRef { page_idx: frame::LEAF, off, len });
2425 }
2426 p.next_leaf()
2427 };
2428 leaf_img.copy_from_slice(w.bytes());
2429 slots.insert(at, SlotRef { page_idx: frame::REC, off: 0, len: rec.len() as u16 });
2430 let frames: [&[u8]; 2] = [leaf_img, rec];
2431
2432 let usable = P - crate::page::HEADER_LEN;
2433 let bytes = |s: &[SlotRef]| s.iter().map(|x| x.len as usize + 4).sum::<usize>();
2434 let left_bytes = bytes(&slots[..at.min(slots.len())]);
2435 let at_tail = at > 0
2436 && at == slots.len() - 1
2437 && left_bytes <= usable
2438 && bytes(&slots[at..]) <= usable;
2439 let at_point = at_tail
2440 && (old_next == 0 || self.keyspace_rightmost(&path, leaf, old_next, rec)?);
2441
2442 #[cfg(feature = "sqlite-balance")]
2443 if !at_point && (old_next != 0 || at + 1 != slots.len()) {
2444 if self.redistribute_neighbors_ref(&mut w, &path, leaf_img, rec, slots, old_next, sc)? {
2445 return Ok(());
2446 }
2447 }
2448
2449 let sp = if at_point {
2450 at
2451 } else if let Some(sp) = split_point_ref(slots, usable) {
2452 sp
2453 } else {
2454 // The three-way cut: one indivisible record too large to pair with
2455 // its neighbours. Greedy byte packing, exactly as before; anything
2456 // other than two breaks is a record no arrangement can place.
2457 let mut cuts = [0usize; 4];
2458 let mut breaks = 0usize;
2459 let mut used = 0usize;
2460 for (i, s) in slots.iter().enumerate() {
2461 if used + s.len as usize + 4 > usable {
2462 breaks += 1;
2463 if breaks <= 2 { cuts[breaks] = i; }
2464 used = 0;
2465 }
2466 used += s.len as usize + 4;
2467 }
2468 cuts[3] = slots.len();
2469 if breaks != 2 || cuts.windows(2).any(|c| c[0] == c[1]) { return Err(Error::TooLarge); }
2470 let mut middle = self.pool.allocate()?; let mid_no = middle.page_no();
2471 let mut right = self.pool.allocate()?; let right_no = right.page_no();
2472 build_page_into(&mut sc.out[0..P], PageKind::Leaf, self.tree_id, leaf,
2473 slots[..cuts[1]].iter().map(|x| rec_of(&frames, *x)), mid_no)?;
2474 build_page_into(&mut sc.out[P..2 * P], PageKind::Leaf, self.tree_id, mid_no,
2475 slots[cuts[1]..cuts[2]].iter().map(|x| rec_of(&frames, *x)), right_no)?;
2476 build_page_into(&mut sc.out[2 * P..3 * P], PageKind::Leaf, self.tree_id, right_no,
2477 slots[cuts[2]..].iter().map(|x| rec_of(&frames, *x)), old_next)?;
2478 let mid_sep = validated_key(rec_of(&frames, slots[cuts[1]]));
2479 let right_sep = validated_key(rec_of(&frames, slots[cuts[2]]));
2480 middle.bytes_mut().copy_from_slice(&sc.out[P..2 * P]);
2481 right.bytes_mut().copy_from_slice(&sc.out[2 * P..3 * P]);
2482 w.bytes_mut().copy_from_slice(&sc.out[0..P]);
2483 drop((middle, right, w));
2484 self.last_leaf.set(None);
2485 self.insert_separator(&mut path, mid_sep, mid_no)?;
2486 let (guard, mut new_path) = self.descend_for_write(right_sep)?;
2487 drop(guard);
2488 self.insert_separator(&mut new_path, right_sep, right_no)?;
2489 if old_next == 0 { self.last_leaf.set(Some(right_no)); }
2490 if at < cuts[1] { self.arm_after_split(key, fences, leaf, mid_no, |f| f.below(mid_sep)); }
2491 else if at < cuts[2] { self.arm_after_split(key, fences, mid_no, right_no,
2492 |f| f.above(mid_sep).below(right_sep)); }
2493 else { self.arm_after_split(key, fences, right_no, old_next, |f| f.above(right_sep)); }
2494 return Ok(());
2495 };
2496
2497 let sep = validated_key(rec_of(&frames, slots[sp]));
2498 let right_no = {
2499 let mut rw = self.pool.allocate()?;
2500 let no = rw.page_no();
2501 build_page_into(&mut sc.out[P..2 * P], PageKind::Leaf, self.tree_id, no,
2502 slots[sp..].iter().map(|x| rec_of(&frames, *x)), old_next)?;
2503 rw.bytes_mut().copy_from_slice(&sc.out[P..2 * P]);
2504 no
2505 };
2506 // Built before the frame is overwritten, so a failure cannot leave the
2507 // live leaf wiped (btree.rs's atomicity rule for `build_page`).
2508 build_page_into(&mut sc.out[0..P], PageKind::Leaf, self.tree_id, leaf,
2509 slots[..sp].iter().map(|x| rec_of(&frames, *x)), right_no)?;
2510 w.bytes_mut().copy_from_slice(&sc.out[0..P]);
2511 drop(w);
2512 if old_next == 0 { self.last_leaf.set(Some(right_no)); }
2513 self.insert_separator(&mut path, sep, right_no)?;
2514 if at < sp {
2515 self.arm_after_split(key, fences, leaf, right_no, |f| f.below(sep));
2516 } else {
2517 self.arm_after_split(key, fences, right_no, old_next, |f| f.above(sep));
2518 }
2519 Ok(())
2520 }
2521
2522 /// L2.3-2: `redistribute_neighbors` over borrowed records.
2523 ///
2524 /// Same windows in the same order, same `neighbor_cell_cuts`, same fit
2525 /// probe, same shadowing, same guard-before-install order. What changed:
2526 /// the window's records and the parent's separators are `SlotRef`s rather
2527 /// than `Vec<u8>`s, the four page images are built into reusable scratch,
2528 /// and the two or three separators that actually change are written into a
2529 /// small arena instead of one `Vec` each.
2530 ///
2531 /// PINNING. A `SlotRef` is only valid while the frame behind it is pinned,
2532 /// and `get_mut` below demands those same frames be unpinned. So each
2533 /// sibling is read-pinned ONE AT A TIME, validated, copied into scratch,
2534 /// and released; the splitting leaf's write pin is held throughout and its
2535 /// image was copied by the caller for the same reason.
2536 #[cfg(all(feature = "sqlite-balance", any(test, feature = "slotref-split")))]
2537 #[allow(clippy::too_many_arguments)]
2538 fn redistribute_neighbors_ref(
2539 &mut self,
2540 w: &mut PinnedWrite<'p>,
2541 path: &[u32],
2542 leaf_img: &[u8],
2543 rec: &[u8],
2544 current: &[SlotRef],
2545 current_next: u32,
2546 sc: &mut SplitScratch,
2547 ) -> Result<bool> {
2548 const P: usize = PAGE_SIZE;
2549 let Some(&parent_no) = path.last() else { return Ok(false) };
2550 let leaf = w.page_no();
2551 let SplitScratch {
2552 sib, parent, out, seps, win, pr0, pr, sizes, prefix, ends, needed, cuts, ids, newids,
2553 } = sc;
2554
2555 let pos = {
2556 let r = self.pool.get(parent_no)?;
2557 let p = open_cached(&r, parent_no)?;
2558 if p.kind() != PageKind::Interior || p.tree_id() != self.tree_id {
2559 return Err(Error::Corrupt { page_no: parent_no, why: "redistribution parent identity" });
2560 }
2561 ids.clear();
2562 pr0.clear();
2563 ids.push(p.child0());
2564 for i in 0..p.nentries() {
2565 let (off, len) = p.slot_bounds(i);
2566 pr0.push(SlotRef { page_idx: frame::PARENT, off, len });
2567 ids.push(validated_child(p.slot(i)));
2568 }
2569 parent.copy_from_slice(&r[..]);
2570 ids.iter().position(|id| *id == leaf)
2571 .ok_or(Error::Corrupt { page_no: parent_no, why: "redistribution child missing" })?
2572 };
2573 let nchild = ids.len();
2574
2575 let mut windows = [(0usize, 0usize); 3];
2576 let mut nw = 0;
2577 if nchild >= 3 { windows[nw] = (pos.saturating_sub(1).min(nchild - 3), 3); nw += 1; }
2578 if pos + 1 < nchild { windows[nw] = (pos, 2); nw += 1; }
2579 if pos > 0 { windows[nw] = (pos - 1, 2); nw += 1; }
2580 let usable = P - crate::page::HEADER_LEN;
2581
2582 'windows: for &(start, count) in &windows[..nw] {
2583 // Gather the window. One sibling pinned at a time; its image is
2584 // copied into scratch before the pin is dropped.
2585 win.clear();
2586 let mut next = 0u32;
2587 let mut nsib = 0usize;
2588 for k in start..start + count {
2589 let no = ids[k];
2590 if no == leaf {
2591 win.extend_from_slice(current);
2592 next = current_next;
2593 continue;
2594 }
2595 let r = self.pool.get(no)?;
2596 let p = open_cached(&r, no)?;
2597 if p.tree_id() != self.tree_id {
2598 return Err(Error::Corrupt { page_no: no, why: "redistribution sibling identity" });
2599 }
2600 if p.kind() != PageKind::Leaf {
2601 // A grafted run enters the tree as a SUBTREE under a single
2602 // separator, so a leaf's sibling under the same parent can be
2603 // an interior page: a tree with a graft in it is no longer
2604 // uniformly deep. Redistribution moves leaf CELLS between
2605 // neighbours and has nothing to say about a subtree, so skip
2606 // this window and let the ordinary split run -- it never
2607 // needed the neighbours. Refusing here turned an ordinary
2608 // insert next to a bulk-built index into `Corrupt`, with
2609 // nothing actually corrupt (kernel/tests/range_graft.rs).
2610 continue 'windows;
2611 }
2612 let idx = if nsib == 0 { frame::SIB0 } else { frame::SIB1 };
2613 sib[nsib * P..nsib * P + P].copy_from_slice(&r[..]);
2614 for i in 0..p.nentries() {
2615 let (off, len) = p.slot_bounds(i);
2616 win.push(SlotRef { page_idx: idx, off, len });
2617 }
2618 next = p.next_leaf();
2619 nsib += 1;
2620 }
2621
2622 sizes.clear();
2623 sizes.extend(win.iter().map(|s| s.len as usize + 4));
2624 if !neighbor_cell_cuts_into(sizes, usable, count, prefix, ends, needed, cuts) { continue; }
2625 let groups = cuts.len() - 1;
2626 if groups > count { continue; }
2627 // `neighbor_cell_cuts` returns `max(existing, needed)` groups and
2628 // the line above rejects more than `existing`, so the window is
2629 // always repacked into exactly as many pages as it already had.
2630 debug_assert_eq!(groups, count);
2631
2632 newids.clear();
2633 newids.extend_from_slice(ids);
2634 pr.clear();
2635 pr.extend_from_slice(pr0);
2636
2637 // The separators this window rewrites, into the arena. Children
2638 // are the pre-shadow page numbers for now: the fit probe below
2639 // depends on the RECORD LENGTHS, which shadowing cannot change,
2640 // and shadowing must not happen before a window can still be
2641 // rejected.
2642 let mut arena = 0usize;
2643 {
2644 let f: [&[u8]; 5] = [leaf_img, rec, &sib[..P], &sib[P..2 * P], parent];
2645 for j in 1..groups {
2646 let key = validated_key(rec_of(&f, win[cuts[j]]));
2647 pr[start + j - 1] = push_sep(seps, &mut arena, key, newids[start + j]);
2648 }
2649 if start > 0 {
2650 // The separator to the window's LEFT keeps its key and is
2651 // re-pointed at whatever page now starts the window.
2652 let key = validated_key(rec_of(&f, pr0[start - 1]));
2653 pr[start - 1] = push_sep(seps, &mut arena, key, newids[start]);
2654 }
2655 }
2656
2657 {
2658 let f: [&[u8]; 6] = [leaf_img, rec, &sib[..P], &sib[P..2 * P], parent, seps];
2659 match build_page_into(&mut out[3 * P..4 * P], PageKind::Interior, self.tree_id,
2660 parent_no, pr.iter().map(|x| rec_of(&f, *x)), ids[0]) {
2661 Ok(()) => {}
2662 Err(Error::TooLarge) => continue,
2663 Err(e) => return Err(e),
2664 }
2665 }
2666
2667 // Past every `continue`: from here the window is committed, so
2668 // page-allocating side effects are allowed.
2669 for k in start..start + count {
2670 if newids[k] != leaf && self.pool.is_frozen(newids[k]) {
2671 newids[k] = shadow_page(self.pool, newids[k])?;
2672 }
2673 }
2674 for j in 1..groups { patch_sep_child(seps, pr[start + j - 1], newids[start + j]); }
2675 if start > 0 { patch_sep_child(seps, pr[start - 1], newids[start]); }
2676
2677 {
2678 let f: [&[u8]; 6] = [leaf_img, rec, &sib[..P], &sib[P..2 * P], parent, seps];
2679 for j in 0..groups {
2680 let no = newids[start + j];
2681 let nx = if j + 1 < groups { newids[start + j + 1] } else { next };
2682 build_page_into(&mut out[j * P..(j + 1) * P], PageKind::Leaf, self.tree_id, no,
2683 win[cuts[j]..cuts[j + 1]].iter().map(|x| rec_of(&f, *x)), nx)?;
2684 }
2685 build_page_into(&mut out[3 * P..4 * P], PageKind::Interior, self.tree_id, parent_no,
2686 pr.iter().map(|x| rec_of(&f, *x)), newids[0])?;
2687 }
2688
2689 // Acquire every fallible guard before installing any new image.
2690 let mut guards: [Option<(usize, PinnedWrite<'p>)>; 3] = [None, None, None];
2691 let mut g = 0;
2692 for j in 0..groups {
2693 if newids[start + j] != leaf {
2694 guards[g] = Some((j, self.pool.get_mut(newids[start + j])?));
2695 g += 1;
2696 }
2697 }
2698 let mut pw = self.pool.get_mut(parent_no)?;
2699 for slot in guards.iter_mut().flatten() {
2700 let j = slot.0;
2701 slot.1.bytes_mut().copy_from_slice(&out[j * P..(j + 1) * P]);
2702 }
2703 let me = pos - start;
2704 w.bytes_mut().copy_from_slice(&out[me * P..(me + 1) * P]);
2705 pw.bytes_mut().copy_from_slice(&out[3 * P..4 * P]);
2706 self.last_leaf.set(None);
2707 // Redistribution rewrote the separators BETWEEN these siblings, so
2708 // a hint on any of them describes an interval that has moved --
2709 // and unlike a split it can leave the sibling chain intact, so the
2710 // page-side check would not notice. Only the window is affected;
2711 // the rest of the cache is still exact.
2712 if let Some(t) = self.tags {
2713 for j in 0..groups { t.forget_leaf(self.tree_id, newids[start + j]); }
2714 }
2715 return Ok(true);
2716 }
2717 Ok(false)
2718 }
2719
2720 /// Is this leaf the rightmost one of the NEW RECORD'S OWN key tag?
2721 ///
2722 /// D9's append split fires only at the tree's rightmost leaf. That is one
2723 /// leaf in the whole file, and D4 makes every feature a key tag inside the
2724 /// same tree, so a store with several ascending runs gets the append split
2725 /// for exactly one of them. `src/collections/mod.rs` writes one document as
2726 /// three keys -- 0x60 vector, 0x40 row, 0x20 mapping -- each ascending in
2727 /// itself; the 0x40 and 0x20 runs always have a higher-tag leaf to their
2728 /// right, so both paid `redistribute_neighbors` (a clone of the parent's
2729 /// records plus up to three siblings' records, then up to four rebuilt page
2730 /// images) on every single leaf fill. A CPU sample of a 100K typed load put
2731 /// 23.3% of load time inside `split_leaf_and_insert`, about 45% of that in
2732 /// malloc/clone/free and 26% in `build_page` + `neighbor_cell_cuts`.
2733 ///
2734 /// The widened signal keeps D9's discipline exactly. The caller has already
2735 /// established that the new record is STRICTLY the largest in this leaf; so
2736 /// if the first key to this leaf's right carries a different tag, there is
2737 /// no key of this tag anywhere to the right, and the insert is an append to
2738 /// its own run in the same unambiguous sense D9 requires. A scattered
2739 /// workload still cannot reach here: a random key is the leaf-local maximum
2740 /// only rarely, and when it is, the tag test is the same one D9 already
2741 /// trusted.
2742 ///
2743 /// THE BOUNDARY LEAF. One leaf per keyspace holds the last key of one tag
2744 /// and the first key of the next. An ascending insert into that leaf lands
2745 /// BEFORE the higher tag's records, so `at == recs.len() - 1` is false and
2746 /// this is never consulted: the boundary leaf keeps the balanced split and
2747 /// the neighbour redistribution, unchanged. The shortcut costs one such
2748 /// leaf per keyspace boundary, which is where it belongs.
2749 ///
2750 /// The separator sitting in the parent ALREADY ON THE DESCENT PATH is this
2751 /// leaf's right neighbour's first key, so the ordinary case reads no page
2752 /// the insert had not already read. Only a leaf that is its parent's last
2753 /// child needs the sibling itself, and that leaf's right neighbour lives
2754 /// under another parent.
2755 ///
2756 /// SACRIFICE (Law 4): one buffer-pool `get` of the parent per split of a
2757 /// full leaf whose new record is its maximum, and for the last child of a
2758 /// parent one `get` of the right sibling, which may be a page read. Splits
2759 /// are one insert in tens; redistribution already opened this same parent.
2760 #[cfg(feature = "keyspace-append")]
2761 fn keyspace_rightmost(&self, path: &[u32], leaf: u32, old_next: u32, rec: &[u8]) -> Result<bool> {
2762 let Some(&tag) = validated_key(rec).first() else { return Ok(false) };
2763 // The parent separator is free: this descent just walked through it.
2764 if let Some(&parent) = path.last() {
2765 let r = self.pool.get(parent)?;
2766 let p = open_cached(&r, parent)?;
2767 if p.kind() == PageKind::Interior && p.tree_id() == self.tree_id {
2768 let n = p.nentries();
2769 let pos = if p.child0() == leaf {
2770 Some(0)
2771 } else {
2772 (0..n).find(|&i| validated_child(p.slot(i)) == leaf).map(|i| i + 1)
2773 };
2774 // `pos == n` is the parent's last child: its right neighbour is
2775 // under a different parent, so fall through to the sibling.
2776 if let Some(pos) = pos {
2777 if pos < n {
2778 return Ok(validated_key(p.slot(pos)).first() != Some(&tag));
2779 }
2780 }
2781 }
2782 }
2783 let r = self.pool.get(old_next)?;
2784 let p = open_cached(&r, old_next)?;
2785 if p.kind() != PageKind::Leaf || p.tree_id() != self.tree_id || p.nentries() == 0 {
2786 return Ok(false);
2787 }
2788 Ok(validated_key(p.slot(0)).first() != Some(&tag))
2789 }
2790
2791 #[cfg(not(feature = "keyspace-append"))]
2792 fn keyspace_rightmost(&self, _path: &[u32], _leaf: u32, _next: u32, _rec: &[u8]) -> Result<bool> {
2793 Ok(false)
2794 }
2795
2796 /// Reuse neighboring capacity without adding a leaf to the window. If all
2797 /// neighbors are full, use the ordinary one-to-two split instead. This
2798 /// avoids rewriting unrelated full siblings merely to allocate a new leaf.
2799 /// New images are built before edits; frozen siblings are shadowed before
2800 /// changing their records. Only this parent and its children participate.
2801 #[cfg(feature = "sqlite-balance")]
2802 #[cfg(any(test, not(feature = "slotref-split")))]
2803 fn redistribute_neighbors_vec(&mut self, w: &mut PinnedWrite<'p>, path: &[u32], current: &[Vec<u8>], current_next: u32) -> Result<bool> {
2804 let Some(&parent)=path.last() else{return Ok(false);};
2805 let leaf=w.page_no();
2806 let (parent_recs, child_ids, pos)={
2807 let r=self.pool.get(parent)?;let p=open_cached(&r,parent)?;
2808 if p.kind()!=PageKind::Interior || p.tree_id()!=self.tree_id{return Err(Error::Corrupt{page_no:parent,why:"redistribution parent identity"});}
2809 let recs:Vec<Vec<u8>>=(0..p.nentries()).map(|i|p.slot(i).to_vec()).collect();
2810 let mut ids=vec![p.child0()];ids.extend(recs.iter().map(|r|validated_child(r)));
2811 let pos=ids.iter().position(|id|*id==leaf).ok_or(Error::Corrupt{page_no:parent,why:"redistribution child missing"})?;
2812 (recs,ids,pos)
2813 };
2814 let mut windows=Vec::new();
2815 if child_ids.len()>=3{windows.push((pos.saturating_sub(1).min(child_ids.len()-3),3));}
2816 if pos+1<child_ids.len(){windows.push((pos,2));}
2817 if pos>0{windows.push((pos-1,2));}
2818 let usable=PAGE_SIZE-crate::page::HEADER_LEN;
2819 'windows: for (start,count) in windows{
2820 let mut all=Vec::new();let mut next=0;
2821 for &no in &child_ids[start..start+count]{
2822 if no==leaf{all.extend(current.iter().cloned());next=current_next;}
2823 else{let r=self.pool.get(no)?;let p=open_cached(&r,no)?;
2824 if p.tree_id()!=self.tree_id{return Err(Error::Corrupt{page_no:no,why:"redistribution sibling identity"});}
2825 if p.kind()!=PageKind::Leaf{
2826 // A grafted run enters the tree as a SUBTREE under a
2827 // single separator, so a leaf's sibling under the same
2828 // parent can be an interior page: a tree with a graft in
2829 // it is no longer uniformly deep. Redistribution moves
2830 // leaf CELLS between neighbours and has nothing to say
2831 // about a subtree, so skip this window and let the
2832 // ordinary split run -- it never needed the neighbours.
2833 // Refusing here turned an ordinary insert next to a
2834 // bulk-built index into `Corrupt`, with nothing actually
2835 // corrupt (kernel/tests/range_graft.rs).
2836 continue 'windows;
2837 }
2838 all.extend((0..p.nentries()).map(|i|p.slot(i).to_vec()));next=p.next_leaf();}
2839 }
2840 let sizes:Vec<usize>=all.iter().map(|r|r.len()+4).collect();
2841 let Some(cuts)=neighbor_cell_cuts(&sizes,usable,count) else { continue; };
2842 let groups=cuts.len()-1;
2843 if groups>count { continue; }
2844 let mut ids=child_ids.clone();
2845 let mut pr=parent_recs.clone();
2846 if groups>count{ids.insert(start+count,0);pr.insert(start+count-1,enc_interior(validated_key(&all[cuts[count]]),0));}
2847 for j in 1..groups{pr[start+j-1]=enc_interior(validated_key(&all[cuts[j]]),ids[start+j]);}
2848 match build_page(PageKind::Interior,self.tree_id,parent,&pr,child_ids[0]){Ok(_)=>{},Err(Error::TooLarge)=>continue,Err(e)=>return Err(e)}
2849 if groups>count{let mut fresh=self.pool.allocate()?;let no=fresh.page_no();let mut p=PageMut::init(fresh.bytes_mut(),PageKind::Leaf,self.tree_id,no);p.finalise(0);ids[start+count]=no;}
2850 for id in &mut ids[start..start+count]{if *id!=leaf && self.pool.is_frozen(*id){*id=shadow_page(self.pool,*id)?;}}
2851 let mut images=Vec::new();
2852 for j in 0..groups{
2853 let no=ids[start+j];let next=if j+1<groups{ids[start+j+1]}else{next};
2854 images.push(build_page(PageKind::Leaf,self.tree_id,no,&all[cuts[j]..cuts[j+1]],next)?);
2855 if start+j>0{
2856 let k=if j>0{validated_key(&all[cuts[j]]).to_vec()}else{validated_key(&pr[start+j-1]).to_vec()};
2857 pr[start+j-1]=enc_interior(&k,no);
2858 }
2859 }
2860 let parent_image=build_page(PageKind::Interior,self.tree_id,parent,&pr,ids[0])?;
2861 // Acquire every fallible guard before installing any new image.
2862 let mut guards=Vec::new();for j in 0..groups{if ids[start+j]!=leaf{guards.push((j,self.pool.get_mut(ids[start+j])?));}}
2863 let mut pw=self.pool.get_mut(parent)?;
2864 for (j,guard) in &mut guards{guard.bytes_mut().copy_from_slice(&images[*j]);}
2865 w.bytes_mut().copy_from_slice(&images[pos-start]);pw.bytes_mut().copy_from_slice(&parent_image);
2866 self.last_leaf.set(None);
2867 // See `redistribute_neighbors_ref`: the window's separators moved.
2868 if let Some(t)=self.tags {for j in 0..groups {t.forget_leaf(self.tree_id,ids[start+j]);}}
2869 return Ok(true);
2870 }
2871 Ok(false)
2872 }
2873
2874 fn insert_separator(&mut self, path: &mut Vec<u32>, sep: &[u8], right: u32) -> Result<()> {
2875 let Some(parent) = path.pop() else {
2876 // The root split: build a new root above the old one.
2877 let left = self.root;
2878 let mut w = self.pool.allocate()?;
2879 let no = w.page_no();
2880 let mut p = PageMut::init(w.bytes_mut(), PageKind::Interior, self.tree_id, no);
2881 p.set_child0(left); // the old root
2882 p.insert_slot(0, &enc_interior(sep, right))?;
2883 p.finalise(0);
2884 drop(w);
2885 self.root = no;
2886 return Ok(());
2887 };
2888
2889 let rec = enc_interior(sep, right);
2890 let (i, room) = {
2891 let r = self.pool.get(parent)?;
2892 let p = PageRef::open_resident(&r, parent)?;
2893 validate_records(&p)?;
2894 let i = upper_bound(&p, sep)?;
2895 // Interior pages never have entries removed in this task, so
2896 // this figure cannot yet have diverged from a summed
2897 // reconstruction — but the real free_space() is what's actually
2898 // true, and using it here keeps both room checks in the file
2899 // computing the same thing the same way.
2900 (i, p.free_space() >= rec.len() + 4)
2901 };
2902
2903 if room {
2904 let mut w = self.pool.get_mut(parent)?;
2905 let mut p = PageMut::reopen(w.bytes_mut());
2906 // (sep, right) slots in ahead of the first entry whose key
2907 // exceeds sep. Whatever pointer already reached the left half still
2908 // reaches it, because the left half kept its page number.
2909 p.insert_slot(i, &rec)?;
2910 p.finalise(0);
2911 return Ok(());
2912 }
2913
2914 // Split the interior page the same way.
2915 #[cfg(feature = "write-trace")]
2916 if crate::write_trace::active() { crate::write_trace::interior_split(); }
2917 let mut recs: Vec<Vec<u8>> = {
2918 let r = self.pool.get(parent)?;
2919 let p = PageRef::open_resident(&r, parent)?;
2920 validate_records(&p)?;
2921 (0..p.nentries()).map(|j| p.slot(j).to_vec()).collect()
2922 };
2923 recs.insert(i, rec);
2924 let old_child0 = {
2925 let r = self.pool.get(parent)?;
2926 let p = PageRef::open_resident(&r, parent)?;
2927 validate_records(&p)?;
2928 p.child0()
2929 };
2930 let sp = split_point(&recs, PAGE_SIZE - crate::page::HEADER_LEN)
2931 .ok_or(Error::TooLarge)?;
2932 let right_recs = recs.split_off(sp);
2933 // The right page's FIRST entry is promoted out of the slot array: its
2934 // key becomes the separator pushed up, and its child becomes the right
2935 // page's child0. This is the standard interior split.
2936 let up = validated_key(&right_recs[0]).to_vec();
2937 let right_child0 = validated_child(&right_recs[0]);
2938
2939 let right_no = {
2940 let mut w = self.pool.allocate()?;
2941 let no = w.page_no();
2942 let page = build_page(
2943 PageKind::Interior, self.tree_id, no, &right_recs[1..], right_child0)?;
2944 w.bytes_mut().copy_from_slice(&page);
2945 no
2946 };
2947 {
2948 let page = build_page(
2949 PageKind::Interior, self.tree_id, parent, &recs, old_child0)?;
2950 let mut w = self.pool.get_mut(parent)?;
2951 w.bytes_mut().copy_from_slice(&page);
2952 }
2953 self.insert_separator(path, &up, right_no)
2954 }
2955
2956 /// Delete one record and maintain sparsely occupied siblings locally.
2957 /// Scratch is bounded by two children plus their parent; published pages
2958 /// remain protected by the ordinary copy-on-write retirement protocol.
2959 pub fn delete(&mut self, key: &[u8]) -> Result<bool> {
2960 // A delete can empty a leaf, merge two, collapse the root, or free a
2961 // page that a later allocation hands to another leaf of this same
2962 // tree. The hinted fences survive none of that, and the page-identity
2963 // checks cannot tell a recycled leaf from the one that was armed.
2964 self.forget_tag_hints();
2965 // Read-only presence probe first, so a miss never shadows anything.
2966 let (leaf, _) = self.descend(key)?;
2967 let retired = {
2968 let r = self.pool.get(leaf)?;
2969 let p = open_cached(&r, leaf)?;
2970 let i = lower_bound(&p, key)?;
2971 if i >= p.nentries()
2972 || validated_key(p.slot(i)) != key
2973 {
2974 return Ok(false);
2975 }
2976 replaced_overflow_pages(self.pool, p.slot(i))?
2977 };
2978 // Hit: take the shadowed write descent (2f) and remove there.
2979 let (mut w, path) = self.descend_for_write(key)?;
2980 let i = {
2981 let pr = PageRef::open_resident_validated(w.bytes(), w.page_no())?;
2982 lower_bound(&pr, key)?
2983 };
2984 let mut p = PageMut::reopen(w.bytes_mut());
2985 p.remove_slot(i);
2986 p.finalise(0);
2987 drop(p);
2988 let leaf = w.page_no();
2989 drop(w);
2990 for (no, birth) in retired { self.pool.free_shadow_page(no, birth)?; }
2991 self.rebalance_after_delete(leaf, path)?;
2992 Ok(true)
2993 }
2994
2995 /// At most two sibling images and their parent per level. Merge when they
2996 /// fit; otherwise redistribute only below one-third occupancy. The gap to
2997 /// half occupancy provides hysteresis for alternating delete/reinsert.
2998 fn rebalance_after_delete(&mut self, mut node: u32, mut path: Vec<u32>) -> Result<()> {
2999 let usable = PAGE_SIZE - crate::page::HEADER_LEN;
3000 loop {
3001 let (kind, used, entries, only_child, node_birth) = {
3002 let r = self.pool.get(node)?; let p = open_cached(&r, node)?;
3003 if p.tree_id() != self.tree_id || !matches!(p.kind(), PageKind::Leaf | PageKind::Interior) {
3004 return Err(Error::Corrupt { page_no: node, why: "delete maintenance node identity" });
3005 }
3006 (p.kind(), (0..p.nentries()).map(|i|p.slot(i).len()+4).sum::<usize>(), p.nentries(), p.child0(), p.lsn())
3007 };
3008 if node == self.root {
3009 if kind == PageKind::Interior && entries == 0 {
3010 // Verify the surviving child before unlinking the old root.
3011 let r = self.pool.get(only_child)?; let child = open_cached(&r, only_child)?;
3012 if child.tree_id()!=self.tree_id || !matches!(child.kind(),PageKind::Leaf|PageKind::Interior) {
3013 return Err(Error::Corrupt {page_no:only_child,why:"collapsed root child identity"});
3014 }
3015 drop(r);
3016 let birth=if self.pool.is_frozen(node) {node_birth}else{self.pool.write_generation()};
3017 self.pool.free_shadow_page(node, birth)?;
3018 self.root = only_child; self.last_leaf.set(None); node = only_child;
3019 continue;
3020 }
3021 return Ok(());
3022 }
3023 if used >= usable / 3 { return Ok(()); }
3024 let parent = path.pop().ok_or(Error::Corrupt {page_no:node,why:"delete maintenance missing parent"})?;
3025 let (parent_recs, ids, pos) = {
3026 let r=self.pool.get(parent)?;let p=open_cached(&r,parent)?;
3027 if p.kind()!=PageKind::Interior || p.tree_id()!=self.tree_id {
3028 return Err(Error::Corrupt {page_no:parent,why:"delete maintenance parent identity"});
3029 }
3030 let recs:Vec<Vec<u8>>=(0..p.nentries()).map(|i|p.slot(i).to_vec()).collect();
3031 let ids:Vec<u32>=(0..=p.nentries()).map(|i|child_at(self.pool,&p,i)).collect::<Result<_>>()?;
3032 let pos=ids.iter().position(|id|*id==node).ok_or(Error::Corrupt {page_no:parent,why:"delete maintenance child missing"})?;
3033 (recs,ids,pos)
3034 };
3035 if ids.len()==1 { node=parent; continue; }
3036 // Prefer a merge in either direction over rewriting two pages.
3037 let mut starts=Vec::with_capacity(2);
3038 if pos>0 {starts.push(pos-1);} if pos+1<ids.len() {starts.push(pos);}
3039 let mut changed=false;
3040 'attempt: for merge_only in [true,false] {
3041 for &start in &starts {
3042 let left=ids[start];let right=ids[start+1];
3043 // A grafted run enters the tree as a SUBTREE under one
3044 // separator, so two children of the same parent need not
3045 // be the same kind any more: an underfull leaf can sit
3046 // beside a bulk-built subtree root. Merging or
3047 // redistributing across that boundary is not defined --
3048 // their records are not the same shape and they are not
3049 // the same height -- so this PAIR is declined and the next
3050 // candidate tried. Refusing outright failed an ordinary
3051 // delete next to a bulk-built index with `Corrupt` and
3052 // nothing corrupt; a declined pair only leaves a node
3053 // underfull, which `!changed` already tolerates.
3054 let (mut all, left_link) = {
3055 let r=self.pool.get(left)?;let p=open_cached(&r,left)?;
3056 if p.tree_id()!=self.tree_id {return Err(Error::Corrupt {page_no:left,why:"delete left sibling identity"});}
3057 if p.kind()!=kind {continue;}
3058 ((0..p.nentries()).map(|i|p.slot(i).to_vec()).collect::<Vec<_>>(),p.next_leaf())
3059 };
3060 let (right_link, right_birth) = {
3061 let r=self.pool.get(right)?;let p=open_cached(&r,right)?;
3062 if p.tree_id()!=self.tree_id {return Err(Error::Corrupt {page_no:right,why:"delete right sibling identity"});}
3063 if p.kind()!=kind {continue;}
3064 if kind==PageKind::Interior {all.push(enc_interior(validated_key(&parent_recs[start]),p.child0()));}
3065 all.extend((0..p.nentries()).map(|i|p.slot(i).to_vec()));(p.next_leaf(),p.lsn())
3066 };
3067 let total:usize=all.iter().map(|r|r.len()+4).sum();let merge=total<=usable;
3068 if merge_only!=merge {continue;}
3069 let mut pr=parent_recs.clone();let mut children=ids.clone();
3070 let (left_recs,right_recs,right_child0)=if merge {
3071 pr.remove(start);children.remove(start+1);(all.clone(),Vec::new(),0)
3072 } else if kind==PageKind::Leaf {
3073 let Some(mid)=split_point(&all,usable) else {continue;};
3074 pr[start]=enc_interior(validated_key(&all[mid]),right);
3075 (all[..mid].to_vec(),all[mid..].to_vec(),right_link)
3076 } else {
3077 let mut prefix=0usize;let mut best=None;
3078 for (i,rec) in all.iter().enumerate() {
3079 let suffix=total-prefix-rec.len()-4;
3080 if prefix<=usable && suffix<=usable {
3081 let delta=prefix.abs_diff(suffix);
3082 if best.is_none_or(|(_,old)|delta<old) {best=Some((i,delta));}
3083 }
3084 prefix+=rec.len()+4;
3085 }
3086 let Some((mid,_))=best else {continue;};
3087 pr[start]=enc_interior(validated_key(&all[mid]),right);
3088 (all[..mid].to_vec(),all[mid+1..].to_vec(),validated_child(&all[mid]))
3089 };
3090 // A longer replacement fence can overflow a variable-key
3091 // parent. Decline that redistribution without touching it.
3092 match build_page(PageKind::Interior,self.tree_id,parent,&pr,children[0]) {
3093 Ok(_)=>{},Err(Error::TooLarge)=>continue,Err(e)=>return Err(e),
3094 }
3095 let left_next=if kind==PageKind::Interior {left_link}else if merge {right_link}else{right};
3096 let mut left_image=build_page(kind,self.tree_id,left,&left_recs,left_next)?;
3097 let mut right_image=if merge {None}else{Some(build_page(kind,self.tree_id,right,&right_recs,right_child0)?)};
3098 // Shadow only pages that will be rewritten; a merged-away
3099 // frozen sibling is retired directly, never overwritten.
3100 let fresh_left=if self.pool.is_frozen(left) {shadow_page(self.pool,left)?}else{left};
3101 let fresh_right=if !merge && self.pool.is_frozen(right) {shadow_page(self.pool,right)?}else{right};
3102 children[start]=fresh_left;
3103 if !merge {children[start+1]=fresh_right;}
3104 for (i,&id) in children.iter().enumerate().skip(1) {
3105 let key=validated_key(&pr[i-1]).to_vec();pr[i-1]=enc_interior(&key,id);
3106 }
3107 left_image[12..16].copy_from_slice(&fresh_left.to_le_bytes());
3108 if kind==PageKind::Leaf && !merge {left_image[20..24].copy_from_slice(&fresh_right.to_le_bytes());}
3109 if let Some(image)=right_image.as_mut(){image[12..16].copy_from_slice(&fresh_right.to_le_bytes());}
3110 let parent_image=build_page(PageKind::Interior,self.tree_id,parent,&pr,children[0])?;
3111 // Acquire every fallible guard and reserve retirement before
3112 // installing any replacement image. Store fails closed on
3113 // any error; no fallible operation follows the image copies.
3114 let mut lw=self.pool.get_mut(fresh_left)?;
3115 let mut rw=if merge {None}else{Some(self.pool.get_mut(fresh_right)?)};
3116 let mut pw=self.pool.get_mut(parent)?;
3117 if merge {
3118 let birth=if self.pool.is_frozen(right) {right_birth}else{self.pool.write_generation()};
3119 self.pool.free_shadow_page(right,birth)?;
3120 }
3121 lw.bytes_mut().copy_from_slice(&left_image);
3122 if let (Some(w),Some(image))=(&mut rw,&right_image){w.bytes_mut().copy_from_slice(image);}
3123 pw.bytes_mut().copy_from_slice(&parent_image);
3124 self.last_leaf.set(None);changed=merge;
3125 break 'attempt;
3126 }
3127 }
3128 if !changed {return Ok(());} node=parent;
3129 }
3130 }
3131
3132 /// Scan from `from` forward, in key order.
3133 ///
3134 /// SINGLE WRITER. The iterator borrows the pool, not `self`, so the borrow
3135 /// checker will happily let you `insert` into this tree while a scan is live.
3136 /// Do not: the iterator re-reads its leaf on every `next`, so an insert that
3137 /// shifts entries in the leaf it is standing on makes it skip or repeat a
3138 /// row, with no compiler or runtime signal. The engine is single-writer by
3139 /// design, and this is where that assumption is cashed.
3140 /// Remove every key with `prefix`. Walks matching leaves via the
3141 /// parent path (a cleared leaf still receives the same descent -- the
3142 /// separators do not change -- so re-descending with the prefix would
3143 /// loop on the first emptied page forever; the advance must go THROUGH
3144 /// the parents, the RangeIter lesson). Each matching leaf is either
3145 /// CLEARED in one page write (every entry matches) or slot-trimmed.
3146 /// Cost: O(matching leaves) page writes + one read-descent each, not
3147 /// O(matching rows) tree operations. Emptied leaves stay allocated
3148 /// (the documented delete posture).
3149 pub fn delete_prefix(&mut self, prefix: &[u8]) -> Result<u64> {
3150 self.forget_tag_hints();
3151 let mut removed = 0u64;
3152 let mut cursor: Vec<u8> = prefix.to_vec();
3153 // 2n: the last leaf KEPT in the chain. Fully-cleared leaves after it
3154 // are unlinked from their parent and freed; the anchor's sibling
3155 // pointer is patched forward over each. The FIRST visited leaf is
3156 // never freed (its left neighbour is unknown), and a parent's last
3157 // child is never detached (no cascade; the leaf stays cleared).
3158 let mut anchor: Option<u32> = None;
3159 'leaves: loop {
3160 // read-descend to the candidate leaf, remembering the path
3161 let (leaf, mut path) = self.descend_with_path(&cursor)?;
3162 let (matches, all, past, retired) = {
3163 let r = self.pool.get(leaf)?;
3164 let p = open_cached(&r, leaf)?;
3165 let n = p.nentries();
3166 let mut idx = Vec::new();
3167 let mut past = false;
3168 let mut retired = Vec::new();
3169 for i in 0..n {
3170 let k = validated_key(p.slot(i));
3171 if k.starts_with(prefix) {
3172 idx.push(i);
3173 retired.extend(replaced_overflow_pages(self.pool, p.slot(i))?);
3174 }
3175 else if k > prefix { past = true; }
3176 }
3177 (idx.clone(), n > 0 && idx.len() == n, past, retired)
3178 };
3179 if !matches.is_empty() {
3180 removed += matches.len() as u64;
3181 let (mut w, wpath) = self.descend_for_write(&cursor)?;
3182 // The write descent may SHADOW the leaf (fresh or recycled
3183 // number) -- the numbers legitimately differ. What the slot
3184 // indices computed from the read descent require is CONTENT
3185 // congruence: the shadow is a byte-identical copy. Only
3186 // check when a shadow actually happened: reading the same
3187 // page while `w` write-pins it is itself a pin conflict.
3188 #[cfg(debug_assertions)]
3189 if w.page_no() != leaf {
3190 let wn = { let p = PageMut::reopen(w.bytes_mut()); p.nentries_pub() };
3191 let rn = { let r = self.pool.get(leaf)?; open_cached(&r, leaf)?.nentries() };
3192 debug_assert_eq!(wn, rn,
3193 "write-descent leaf diverged from the read-descent leaf");
3194 }
3195 if all {
3196 // Whole-leaf clear MUST keep the sibling pointer: init
3197 // resets the full header, and a zeroed next_leaf makes a
3198 // MIDDLE leaf look rightmost -- the append fast path then
3199 // writes keys past the leaf's true range and orphans
3200 // committed subtrees (silent loss: a fold+insert+fold
3201 // cycle dropped every folded segment; caught by 2n's
3202 // reuse probe, present since 2h).
3203 let no = w.page_no();
3204 let tid = self.tree_id;
3205 let nl = u32::from_le_bytes(w.bytes_mut()[20..24].try_into().unwrap());
3206 let mut p = PageMut::init(w.bytes_mut(), PageKind::Leaf, tid, no);
3207 p.set_next_leaf(nl);
3208 p.finalise(0);
3209 drop(w);
3210 // 2n: detach and free it when safe -- an emptied leaf
3211 // whose key range never refills (monotonic ids) is
3212 // otherwise allocated forever. Stale read paths still
3213 // find it cleared-with-chain, which walks treat as any
3214 // other empty leaf.
3215 let freed = match (anchor, wpath.last()) {
3216 (Some(a), Some(&parent)) if a != no => {
3217 match unlink_child(self.pool, self.tree_id, parent, no)? {
3218 Some(pos) => {
3219 self.pool.free_page(no)?;
3220 let mut aw = self.pool.get_mut(a)?;
3221 let mut ap = PageMut::reopen(aw.bytes_mut());
3222 ap.set_next_leaf(nl);
3223 ap.finalise(0);
3224 // later iterations' read path may hold
3225 // saved child indices into this SAME
3226 // writable parent -- shift them left
3227 // past the removed position, or the
3228 // advance skips a child (rows survive
3229 // the delete; measured, not theorised).
3230 for e in path.iter_mut() {
3231 if e.0 == parent && e.1 >= pos && e.1 > 0 { e.1 -= 1; }
3232 }
3233 true
3234 }
3235 None => false,
3236 }
3237 }
3238 _ => false,
3239 };
3240 if !freed { anchor = Some(no); }
3241 } else {
3242 let mut p = PageMut::reopen(w.bytes_mut());
3243 for &i in matches.iter().rev() { p.remove_slot(i); }
3244 p.finalise(0);
3245 anchor = Some(w.page_no());
3246 }
3247 for (no, birth) in retired { self.pool.free_shadow_page(no, birth)?; }
3248 self.last_leaf.set(None); // the hint may name a cleared leaf
3249 }
3250 if past { return Ok(removed); }
3251 // advance through the parents to the next leaf's first key
3252 loop {
3253 let (mut cur, mut idx) = loop {
3254 let Some((page, i)) = path.pop() else { return Ok(removed) };
3255 let n = {
3256 let r = self.pool.get(page)?;
3257 open_cached(&r, page)?.nentries()
3258 };
3259 if i < n { break (page, i + 1); }
3260 };
3261 // leftmost spine from the right sibling down to a leaf
3262 let first_key: Option<Vec<u8>> = loop {
3263 let r = self.pool.get(cur)?;
3264 let p = open_cached(&r, cur)?;
3265 if p.kind() == PageKind::Leaf {
3266 break if p.nentries() == 0 { None }
3267 else { Some(validated_key(p.slot(0)).to_vec()) };
3268 }
3269 let child = child_at(self.pool, &p, idx)?;
3270 path.push((cur, idx));
3271 cur = child;
3272 idx = 0;
3273 };
3274 match first_key {
3275 Some(k) if k.starts_with(prefix) => { cursor = k; continue 'leaves; }
3276 Some(_) => return Ok(removed),
3277 None => continue, // empty leaf: keep advancing
3278 }
3279 }
3280 }
3281 }
3282
3283 pub fn range(&self, from: &[u8]) -> Result<RangeIter<'p>> {
3284 let (leaf, idx, path) = self.descend_positioned(from)?;
3285 Ok(RangeIter {
3286 pool: self.pool, tree_id: self.tree_id, page: leaf, idx,
3287 done: false, leaves: 1, max_leaves: self.pool.page_count(),
3288 buf: std::collections::VecDeque::new(),
3289 served: 0,
3290 path,
3291 pin: None,
3292 leaf_entries: 0,
3293 leaf_markers: true,
3294 seeks: 0,
3295 })
3296 }
3297
3298 /// Scan keys strictly below `to` in descending order.
3299 pub fn range_reverse(&self, to: &[u8]) -> Result<ReverseRangeIter<'p>> {
3300 let (leaf, path) = self.descend_with_path(to)?;
3301 let idx = { let r = self.pool.get(leaf)?; lower_bound(&open_cached(&r, leaf)?, to)? };
3302 Ok(ReverseRangeIter {
3303 pool: self.pool, tree_id: self.tree_id, page: leaf, idx,
3304 done: false, leaves: 1, max_leaves: self.pool.page_count(), path,
3305 pin: None, pending: None,
3306 })
3307 }
3308}
3309
3310impl ReverseRangeIter<'_> {
3311 /// Step to the leaf immediately left of the current one through the saved
3312 /// parent path, then land just past its final slot.
3313 fn retreat(&mut self) -> Result<bool> {
3314 let (mut cur, mut idx_in_parent) = loop {
3315 let Some((page, i)) = self.path.pop() else { return Ok(false) };
3316 if i > 0 { break (page, i - 1); }
3317 };
3318 loop {
3319 let r = self.pool.get(cur)?;
3320 let p = open_cached(&r, cur)?;
3321 if p.tree_id() != self.tree_id {
3322 return Err(Error::Corrupt { page_no: cur, why: "page belongs to another tree" });
3323 }
3324 if p.kind() == PageKind::Leaf {
3325 self.page = cur;
3326 self.idx = p.nentries();
3327 break;
3328 }
3329 let n = p.nentries();
3330 let child_i = if idx_in_parent > n { n } else { idx_in_parent };
3331 let child = child_at(self.pool, &p, child_i)?;
3332 self.path.push((cur, child_i));
3333 cur = child;
3334 idx_in_parent = usize::MAX; // every later descent takes the rightmost child
3335 }
3336 self.leaves += 1;
3337 if self.leaves > self.max_leaves {
3338 return Err(Error::Corrupt {
3339 page_no: self.page, why: "reverse scan visits more leaves than the file holds" });
3340 }
3341 Ok(true)
3342 }
3343
3344 /// The descending record the cursor is parked on, borrowed from the pinned
3345 /// leaf, with no allocation. Paired with [`ReverseRangeIter::step`] this is
3346 /// the PULL cursor the forward iterator already has: peek, use, step, peek.
3347 ///
3348 /// A query executor cannot live inside `for_each_ref`'s callback -- it has
3349 /// to interleave the walk with a heap, a work meter and a cancellation
3350 /// check -- and until this existed, every descending order had to
3351 /// materialise its whole range before it could rank it. Same leaf pin,
3352 /// same tree-id and cycle checks, same overflow resolution as the
3353 /// callback form.
3354 pub fn peek_ref(&mut self) -> Result<Option<(&[u8], &[u8])>> {
3355 self.position()?;
3356 self.current_ref()
3357 }
3358
3359 /// Step past the record the last peek returned. Crossing into the leaf to
3360 /// the LEFT is left to the next peek, which retreats through the saved
3361 /// parent path when the slot index reaches the start of the leaf.
3362 pub fn step(&mut self) {
3363 if self.pending.take().is_some() {
3364 return;
3365 }
3366 self.idx = self.idx.saturating_sub(1);
3367 }
3368
3369 fn position(&mut self) -> Result<()> {
3370 loop {
3371 if self.pending.is_some() || self.done {
3372 return Ok(());
3373 }
3374 if self.pin.is_none() {
3375 self.pin = Some(self.pool.get(self.page)?);
3376 }
3377 enum Step {
3378 Stay,
3379 Overflow { key: Vec<u8>, marker: Vec<u8> },
3380 PreviousLeaf,
3381 Corrupt,
3382 }
3383 let step = {
3384 let pin = self.pin.as_ref().unwrap();
3385 let p = open_cached(pin, self.page)?;
3386 if p.tree_id() != self.tree_id {
3387 Step::Corrupt
3388 } else if self.idx > 0 {
3389 let (key, value, is_marker) = validated_leaf(p.slot(self.idx - 1));
3390 if is_marker {
3391 Step::Overflow { key: key.to_vec(), marker: value.to_vec() }
3392 } else {
3393 Step::Stay
3394 }
3395 } else {
3396 Step::PreviousLeaf
3397 }
3398 };
3399 match step {
3400 Step::Corrupt => {
3401 self.pin = None;
3402 self.done = true;
3403 return Err(Error::Corrupt {
3404 page_no: self.page, why: "reverse-scan page belongs to another tree" });
3405 }
3406 Step::Stay => return Ok(()),
3407 Step::Overflow { key, marker } => {
3408 // Step past it here: `step()` then only drops `pending`.
3409 self.idx -= 1;
3410 self.pin = None;
3411 let value = read_overflow(self.pool, &marker)?;
3412 self.pending = Some((key, value));
3413 return Ok(());
3414 }
3415 Step::PreviousLeaf => {
3416 self.pin = None;
3417 if !self.retreat()? {
3418 self.done = true;
3419 }
3420 }
3421 }
3422 }
3423 }
3424
3425 fn current_ref(&self) -> Result<Option<(&[u8], &[u8])>> {
3426 if let Some((key, value)) = self.pending.as_ref() {
3427 return Ok(Some((key.as_slice(), value.as_slice())));
3428 }
3429 if self.done {
3430 return Ok(None);
3431 }
3432 let Some(pin) = self.pin.as_ref() else {
3433 return Ok(None);
3434 };
3435 let p = open_cached(pin, self.page)?;
3436 if self.idx == 0 {
3437 return Ok(None);
3438 }
3439 let (key, value, is_marker) = validated_leaf(p.slot(self.idx - 1));
3440 debug_assert!(!is_marker, "peek parks overflow markers in `pending`");
3441 Ok(Some((key, value)))
3442 }
3443
3444 /// Visit descending records as borrows into one pinned leaf. The cursor is
3445 /// bounded by the buffer pool; a caller stopping after `k` entries pays for
3446 /// only the pages containing those entries.
3447 pub fn for_each_ref(mut self, mut f: impl FnMut(&[u8], &[u8]) -> bool) -> Result<()> {
3448 self.pin = None;
3449 if let Some((key, value)) = self.pending.take() {
3450 if !f(&key, &value) { return Ok(()) }
3451 }
3452 loop {
3453 if self.done { return Ok(()) }
3454 let r = self.pool.get(self.page)?;
3455 let p = open_cached(&r, self.page)?;
3456 if p.tree_id() != self.tree_id {
3457 return Err(Error::Corrupt {
3458 page_no: self.page, why: "reverse-scan page belongs to another tree" });
3459 }
3460 let mut overflow: Option<(Vec<u8>, Vec<u8>)> = None;
3461 while self.idx > 0 {
3462 self.idx -= 1;
3463 let rec = p.slot(self.idx);
3464 let (key, value, is_marker) = validated_leaf(rec);
3465 if is_marker {
3466 overflow = Some((key.to_vec(), value.to_vec()));
3467 break;
3468 }
3469 if !f(key, value) { return Ok(()) }
3470 }
3471 drop(r);
3472 if let Some((key, marker)) = overflow {
3473 let val = read_overflow(self.pool, &marker)?;
3474 if !f(&key, &val) { return Ok(()) }
3475 // Re-pin the same leaf at the already-decremented slot.
3476 continue;
3477 }
3478 if self.idx > 0 { continue }
3479 if !self.retreat()? { self.done = true; }
3480 }
3481 }
3482}
3483
3484impl RangeIter<'_> {
3485 /// How many LEAVES this cursor has stepped through since it was opened.
3486 ///
3487 /// `advance` is the cursor's real unit of cost: it climbs the parent path
3488 /// until an ancestor has a child to the right and then re-descends the
3489 /// leftmost spine, so one step is one or more pager accesses and a long
3490 /// reach is many. A caller choosing between stepping this cursor forward
3491 /// and descending the tree afresh is choosing between LEAF STEPS and a
3492 /// tree height, and this is the only half of that it cannot work out for
3493 /// itself -- how far apart two keys are in leaves depends on how wide the
3494 /// records between them are, which is the caller's data and not its plan.
3495 pub fn leaves_stepped(&self) -> u32 {
3496 self.leaves
3497 }
3498
3499 /// 2f: step to the next leaf through the parent path. Climb until an
3500 /// ancestor has a child to the right, step into it, then take the
3501 /// leftmost spine down to its first leaf. Returns false when every
3502 /// ancestor was exhausted -- the finished leaf was rightmost. Typical
3503 /// cost: one parent open and one leaf open, same as the old chain.
3504 fn advance(&mut self) -> Result<bool> {
3505 // Climb: drop exhausted levels.
3506 let (mut cur, mut idx_in_parent) = loop {
3507 let Some((page, i)) = self.path.pop() else { return Ok(false) };
3508 let n = {
3509 let r = self.pool.get(page)?;
3510 open_cached(&r, page)?.nentries()
3511 };
3512 // Children are child0 + one per slot: indices 0..=nentries.
3513 if i < n { break (page, i + 1); }
3514 };
3515 // Step right, then take the leftmost spine down to a leaf. Keep the
3516 // leaf pin so the next for_each_ref / peek does not get the same page
3517 // a second time -- decode each page once.
3518 loop {
3519 let r = self.pool.get(cur)?;
3520 let p = open_cached(&r, cur)?;
3521 if p.tree_id() != self.tree_id {
3522 return Err(Error::Corrupt { page_no: cur, why: "page belongs to another tree" });
3523 }
3524 if p.kind() == PageKind::Leaf {
3525 self.page = cur;
3526 self.idx = 0;
3527 self.leaf_entries = p.nentries();
3528 self.leaf_markers = leaf_has_marker(&p);
3529 drop(p);
3530 self.pin = Some(r);
3531 break;
3532 }
3533 let child = child_at(self.pool, &p, idx_in_parent)?;
3534 self.path.push((cur, idx_in_parent));
3535 cur = child;
3536 idx_in_parent = 0; // leftmost from here down
3537 }
3538 self.leaves += 1;
3539 if self.leaves > self.max_leaves {
3540 return Err(Error::Corrupt { page_no: self.page, why: "scan visits more leaves than the file holds" });
3541 }
3542 Ok(true)
3543 }
3544}
3545
3546impl RangeIter<'_> {
3547 /// Stop permanently, handing back the error that stopped us.
3548 ///
3549 /// Fusing on error is not tidiness. Returning `Some(Err(..))` without it
3550 /// leaves `page` and `idx` unchanged, so the next call retries the identical
3551 /// read and fails identically — forever. A consumer that logs and continues,
3552 /// or uses `filter_map(Result::ok)`, spins for good the first time a scan
3553 /// crosses an unreadable page. Fail loudly once; never fail forever.
3554 fn fail(&mut self, e: Error) -> Option<Result<(Vec<u8>, Vec<u8>)>> {
3555 self.done = true;
3556 Some(Err(e))
3557 }
3558}
3559
3560impl RangeIter<'_> {
3561 /// Visit every remaining record WITHOUT allocating per entry: the callback
3562 /// gets the record's key and value as borrows into the pinned leaf, and
3563 /// iteration stops when it returns false.
3564 ///
3565 /// This exists because the allocating path costs ~34ns/key against
3566 /// SQLite's ~18 on covering scans -- two Vecs per row, paid even by
3567 /// count-only queries. One pin and one validation per leaf, zero allocs,
3568 /// same sibling-chain, cycle and tree-id checks as `next()`.
3569 pub fn for_each_ref(mut self, mut f: impl FnMut(&[u8], &[u8]) -> bool) -> Result<()> {
3570 // Drain anything already buffered by earlier `next()` calls first.
3571 while let Some((k, v, is_marker)) = self.buf.pop_front() {
3572 if is_marker {
3573 let val = read_overflow(self.pool, &v)?;
3574 if !f(&k, &val) { return Ok(()); }
3575 continue;
3576 }
3577 if !f(&k, &v) { return Ok(()); }
3578 }
3579 loop {
3580 if self.done { return Ok(()); }
3581 let r = match self.pin.take() {
3582 Some(pin) => pin,
3583 None => self.pool.get(self.page)?,
3584 };
3585 let p = open_cached(&r, self.page)?;
3586 if p.tree_id() != self.tree_id {
3587 return Err(Error::Corrupt {
3588 page_no: self.page, why: "sibling page belongs to another tree" });
3589 }
3590 let mut deferred: Vec<(Vec<u8>, Vec<u8>, bool)> = Vec::new();
3591 while self.idx < p.nentries() {
3592 let rec = p.slot(self.idx);
3593 self.idx += 1;
3594 let (key, value, flag) = validated_leaf(rec);
3595 if flag || !deferred.is_empty() {
3596 // A marker cannot resolve while this leaf is pinned, and
3597 // everything after it defers too so key order is kept.
3598 deferred.push((key.to_vec(), value.to_vec(), flag));
3599 continue;
3600 }
3601 if !f(key, value) { return Ok(()); }
3602 }
3603 drop(r);
3604 for (k, v, is_marker) in deferred {
3605 if is_marker {
3606 let val = read_overflow(self.pool, &v)?;
3607 if !f(&k, &val) { return Ok(()); }
3608 } else if !f(&k, &v) { return Ok(()); }
3609 }
3610 if !self.advance()? { return Ok(()); }
3611 }
3612 }
3613
3614 /// Advance to the first remaining key >= `target` and return borrowed
3615 /// slices into the pinned leaf (or into a resolved overflow buffer).
3616 /// The iterator stays on that record so a later, still-greater target
3617 /// can resume without skipping it. `Ok(None)` means the range is empty
3618 /// or every remaining key is below `target`.
3619 ///
3620 /// Does not drain in-leaf records into `Vec`s. Overflow values still
3621 /// allocate, because they do not live in the pinned leaf. Reuses
3622 /// `advance()` and the validated-bit `open_cached` path that
3623 /// `for_each_ref` uses.
3624 pub fn peek_at_or_after(&mut self, target: &[u8]) -> Result<Option<(&[u8], &[u8])>> {
3625 self.position_at_or_after(target)?;
3626 self.current_ref()
3627 }
3628
3629 /// The record the cursor is parked on, borrowed from the pinned leaf, with
3630 /// no seek and no allocation. Paired with [`RangeIter::step`] this makes a
3631 /// PULL cursor -- peek, use, step, peek -- that costs what `for_each_ref`
3632 /// costs while still letting the caller stop and resume. A query executor
3633 /// needs exactly that: it has to interleave the walk with a heap, a work
3634 /// meter and a cancellation check, none of which fit inside a callback.
3635 ///
3636 /// The empty target is below every key, so this is `peek_at_or_after`
3637 /// standing still: same leaf pin, same overflow-marker resolution.
3638 pub fn peek_ref(&mut self) -> Result<Option<(&[u8], &[u8])>> {
3639 // A cursor that is already standing on a record of its pinned leaf
3640 // has nothing to seek for: the empty target is below every key, so
3641 // `position_at_or_after` would open the page a second time and decode
3642 // the record two more times to arrive back where it already is. That
3643 // was 2 page opens and 3 record decodes per ROW where the callback
3644 // walk pays 1 open per LEAF and 1 decode per row.
3645 if !self.parked() {
3646 self.position_at_or_after(&[])?;
3647 }
3648 self.current_ref()
3649 }
3650
3651 /// True when `current_ref` alone is the whole answer: nothing parked in
3652 /// `buf`, the walk is live, a leaf is pinned, `idx` names a record in it,
3653 /// and that leaf holds no overflow marker (a marker's value lives off the
3654 /// page and only the seek path can resolve it). Every term is a field
3655 /// read, and the two leaf facts were recorded when the pin was taken.
3656 fn parked(&self) -> bool {
3657 self.buf.is_empty()
3658 && !self.done
3659 && self.pin.is_some()
3660 && !self.leaf_markers
3661 && self.idx < self.leaf_entries
3662 }
3663
3664 /// How many times this cursor has had to seek -- ask the tree where it
3665 /// is -- rather than read the record it was standing on. A streaming walk
3666 /// should seek about once per leaf; seeking once per row is the defect
3667 /// this counts.
3668 pub fn seeks(&self) -> u64 {
3669 self.seeks
3670 }
3671
3672 /// Step past the record the last peek returned, without materialising it.
3673 /// Crossing a leaf boundary is left to the next peek, which already climbs
3674 /// the parent path when the slot index runs past the end of the leaf.
3675 pub fn step(&mut self) {
3676 if self.buf.pop_front().is_some() {
3677 return;
3678 }
3679 self.idx += 1;
3680 }
3681
3682 fn position_at_or_after(&mut self, target: &[u8]) -> Result<()> {
3683 self.seeks += 1;
3684 loop {
3685 let skip_buf = matches!(self.buf.front(), Some((k, _, _)) if k.as_slice() < target);
3686 if skip_buf {
3687 self.buf.pop_front();
3688 continue;
3689 }
3690 break;
3691 }
3692 if matches!(self.buf.front(), Some((k, _, _)) if k.as_slice() >= target) {
3693 if let Some((_, v, true)) = self.buf.front() {
3694 let marker = v.clone();
3695 let (k, _, _) = self.buf.pop_front().unwrap();
3696 let val = read_overflow(self.pool, &marker)?;
3697 self.buf.push_front((k, val, false));
3698 }
3699 return Ok(());
3700 }
3701
3702 loop {
3703 if self.done {
3704 self.pin = None;
3705 return Ok(());
3706 }
3707 if self.pin.is_none() {
3708 // The ONE place this cursor takes a leaf pin, and so the one
3709 // place the leaf's own facts are read. `page` cannot change
3710 // while the pin is held -- `advance` runs only after the pin
3711 // is dropped -- so what is recorded here stays true for as
3712 // long as `peek_ref` may believe it.
3713 let pin = self.pool.get(self.page)?;
3714 {
3715 let p = open_cached(&pin, self.page)?;
3716 self.leaf_entries = p.nentries();
3717 self.leaf_markers = leaf_has_marker(&p);
3718 }
3719 self.pin = Some(pin);
3720 }
3721
3722 enum Step {
3723 Stay(usize),
3724 Overflow { idx: usize, key: Vec<u8>, marker: Vec<u8> },
3725 NextLeaf,
3726 Corrupt,
3727 }
3728 let step = {
3729 let pin = self.pin.as_ref().unwrap();
3730 let p = open_cached(pin, self.page)?;
3731 if p.tree_id() != self.tree_id {
3732 Step::Corrupt
3733 } else {
3734 let n = p.nentries();
3735 let mut idx = self.idx;
3736 // The EMPTY target -- what `peek_ref` asks with -- is below
3737 // every key, so the cursor is never before it and the
3738 // probe that decides whether to seek can only ever answer
3739 // no. Deciding that by decoding the record was 2.6% of a
3740 // key-only enumeration.
3741 if !target.is_empty() && idx < n {
3742 let (key0, _, _) = validated_leaf(p.slot(idx));
3743 if key0 < target {
3744 idx = lower_bound(&p, target)?.max(idx);
3745 }
3746 }
3747 if idx < n {
3748 let (key, value, is_marker) = validated_leaf(p.slot(idx));
3749 if is_marker {
3750 Step::Overflow { idx, key: key.to_vec(), marker: value.to_vec() }
3751 } else {
3752 Step::Stay(idx)
3753 }
3754 } else {
3755 Step::NextLeaf
3756 }
3757 }
3758 };
3759 match step {
3760 Step::Corrupt => {
3761 self.pin = None;
3762 self.done = true;
3763 return Err(Error::Corrupt {
3764 page_no: self.page,
3765 why: "sibling page belongs to another tree",
3766 });
3767 }
3768 Step::Stay(idx) => {
3769 self.idx = idx;
3770 return Ok(());
3771 }
3772 Step::Overflow { idx, key, marker } => {
3773 self.idx = idx + 1;
3774 self.pin = None;
3775 let val = read_overflow(self.pool, &marker)?;
3776 self.buf.push_front((key, val, false));
3777 return Ok(());
3778 }
3779 Step::NextLeaf => {
3780 self.pin = None;
3781 if !self.advance()? {
3782 self.done = true;
3783 }
3784 }
3785 }
3786 }
3787 }
3788
3789 fn current_ref(&self) -> Result<Option<(&[u8], &[u8])>> {
3790 if let Some((k, v, is_marker)) = self.buf.front() {
3791 debug_assert!(!*is_marker, "peek resolves overflow markers before yielding");
3792 return Ok(Some((k.as_slice(), v.as_slice())));
3793 }
3794 if self.done {
3795 return Ok(None);
3796 }
3797 let Some(pin) = self.pin.as_ref() else {
3798 return Ok(None);
3799 };
3800 // This pin was taken through `open_cached`, which either found the
3801 // frame's validated bit set or validated the residency and set it --
3802 // and a pinned frame cannot be evicted, so the bit cannot have been
3803 // cleared since. Asking the pool for it again costs a `RefCell`
3804 // borrow and a frame index PER ROW; `open_cached` is exactly this
3805 // call plus that question.
3806 let p = PageRef::open_resident_validated(pin, self.page)?;
3807 if self.idx >= p.nentries() {
3808 return Ok(None);
3809 }
3810 let (key, value, is_marker) = validated_leaf(p.slot(self.idx));
3811 debug_assert!(!is_marker, "in-leaf overflow must have been parked in buf");
3812 Ok(Some((key, value)))
3813 }
3814}
3815
3816impl Iterator for RangeIter<'_> {
3817 type Item = Result<(Vec<u8>, Vec<u8>)>;
3818 fn next(&mut self) -> Option<Self::Item> {
3819 loop {
3820 // ONE leaf pinned, ever. `advance` now keeps the leaf it landed
3821 // on so a following `for_each_ref`/`peek` does not get the same
3822 // page twice, and this loop is about to get that page itself --
3823 // so the pin is dropped at the TOP of every iteration, not once
3824 // on entry. It is also what keeps the `read_overflow` chain walks
3825 // below off a pinned leaf.
3826 self.pin = None;
3827 if let Some((k, v, is_marker)) = self.buf.pop_front() {
3828 if is_marker {
3829 let val = match read_overflow(self.pool, &v) {
3830 Ok(val) => val, Err(e) => return self.fail(e),
3831 };
3832 return Some(Ok((k, val)));
3833 }
3834 return Some(Ok((k, v)));
3835 }
3836 if self.done { return None; }
3837 let r = match self.pool.get(self.page) { Ok(r) => r, Err(e) => return self.fail(e) };
3838 let p = match open_cached(&r, self.page) { Ok(p) => p, Err(e) => return self.fail(e) };
3839 // `descend` checks this for the FIRST leaf only. Every later leaf is
3840 // reached by following a sibling pointer, and a well-formed page from
3841 // another tree passes its checksum perfectly well — five trees share
3842 // this file. Without this the scan decodes another tree's rows and
3843 // returns them as ours.
3844 if p.tree_id() != self.tree_id {
3845 let e = Error::Corrupt {
3846 page_no: self.page, why: "sibling page belongs to another tree" };
3847 return self.fail(e);
3848 }
3849 const SHORT_SCAN: u32 = 8;
3850 if self.served < SHORT_SCAN {
3851 if self.idx < p.nentries() {
3852 let rec = p.slot(self.idx);
3853 self.idx += 1;
3854 self.served += 1;
3855 let (key, value, is_marker) = validated_leaf(rec);
3856 let kv = (key.to_vec(), value.to_vec());
3857 if is_marker {
3858 drop(r);
3859 let v = match read_overflow(self.pool, &kv.1) {
3860 Ok(v) => v, Err(e) => return self.fail(e),
3861 };
3862 return Some(Ok((kv.0, v)));
3863 }
3864 return Some(Ok(kv));
3865 }
3866 } else {
3867 while self.idx < p.nentries() {
3868 let rec = p.slot(self.idx);
3869 self.idx += 1;
3870 // markers buffered as markers (explicit flag -- an in-band
3871 // tag would collide with real values of the same shape),
3872 // resolved on pop: the chain walk must not run while this
3873 // leaf is pinned
3874 let (key, value, flag) = validated_leaf(rec);
3875 self.buf.push_back((key.to_vec(), value.to_vec(), flag));
3876 }
3877 }
3878 drop(r); // released BEFORE re-descending: one leaf pinned, ever
3879 // done is a STATE, not an exit: the buffer may hold this final
3880 // leaf's records, and returning here dropped them -- 163 keys of a
3881 // 20,000-key scan, caught by the ordering test within seconds of
3882 // the batching change.
3883 match self.advance() {
3884 Ok(true) => {}
3885 Ok(false) => { self.done = true; }
3886 Err(e) => return self.fail(e),
3887 }
3888 }
3889 }
3890}
3891
3892#[cfg(test)]
3893mod tests {
3894 use super::*;
3895 use crate::test_support::scratch_pool;
3896
3897 #[test]
3898 fn a_key_written_is_a_key_found() {
3899 let (pool, _d) = scratch_pool(64);
3900 let last_leaf = Cell::new(None);
3901 let fast_path_hits = Cell::new(0);
3902 let fast_path_attempts = Cell::new(0);
3903 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
3904 t.insert(b"alpha", b"1").unwrap();
3905 t.insert(b"beta", b"2").unwrap();
3906 assert_eq!(t.get(b"alpha").unwrap().as_deref(), Some(&b"1"[..]));
3907 assert_eq!(t.get(b"beta").unwrap().as_deref(), Some(&b"2"[..]));
3908 assert_eq!(t.get(b"gamma").unwrap(), None);
3909 }
3910
3911 #[test]
3912 fn a_later_write_replaces_an_earlier_one() {
3913 let (pool, _d) = scratch_pool(64);
3914 let last_leaf = Cell::new(None);
3915 let fast_path_hits = Cell::new(0);
3916 let fast_path_attempts = Cell::new(0);
3917 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
3918 t.insert(b"k", b"first").unwrap();
3919 t.insert(b"k", b"second").unwrap();
3920 assert_eq!(t.get(b"k").unwrap().as_deref(), Some(&b"second"[..]));
3921 }
3922
3923 /// Splitting is the whole point: more keys than one page can hold, in an
3924 /// order that guarantees splits, with a pool far smaller than the tree.
3925 #[test]
3926 fn fifty_thousand_scattered_keys_are_all_findable() {
3927 let (pool, _d) = scratch_pool(16);
3928 let last_leaf = Cell::new(None);
3929 let fast_path_hits = Cell::new(0);
3930 let fast_path_attempts = Cell::new(0);
3931 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
3932 let n = 50_000u64;
3933 // A multiplicative hash scatters insert order without needing rand.
3934 let scatter = |i: u64| i.wrapping_mul(0x9E37_79B9_7F4A_7C15);
3935 for i in 0..n { t.insert(&scatter(i).to_be_bytes(), &i.to_le_bytes()).unwrap(); }
3936 for i in 0..n {
3937 let got = t.get(&scatter(i).to_be_bytes()).unwrap();
3938 assert_eq!(got.as_deref(), Some(&i.to_le_bytes()[..]), "key {i} missing");
3939 }
3940 }
3941
3942 #[test]
3943 fn a_value_too_large_for_a_page_is_refused_not_panicked() {
3944 let (pool, _d) = scratch_pool(16);
3945 let last_leaf = Cell::new(None);
3946 let fast_path_hits = Cell::new(0);
3947 let fast_path_attempts = Cell::new(0);
3948 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
3949 // An oversized VALUE now spills to an overflow chain and round-trips
3950 // exactly -- the refusal this test used to assert became a feature.
3951 let big = vec![7u8; 5000];
3952 t.insert(b"k", &big).unwrap();
3953 assert_eq!(t.get(b"k").unwrap().as_deref(), Some(big.as_slice()));
3954 // What must STILL be refused is a key that cannot fit a leaf record
3955 // even as a marker: keys never spill.
3956 let huge_key = vec![b'k'; crate::page::MAX_RECORD_LEN];
3957 assert!(matches!(t.insert(&huge_key, b"v"), Err(crate::Error::TooLarge)));
3958 }
3959
3960 /// The exact path that used to corrupt a leaf: fill one leaf as tightly
3961 /// as this record shape allows without splitting, then replace one
3962 /// entry's value with a longer one whose extra bytes only fit once the
3963 /// old entry's payload is actually reclaimed (not just its slot). With
3964 /// the old `exists || room` shape this replace would remove the entry,
3965 /// fail to fit the new one, and never finalise — leaving the whole leaf
3966 /// unreadable. With the fix, this is an ordinary in-place growth.
3967 #[test]
3968 fn replacing_a_value_with_a_longer_one_on_a_full_leaf_keeps_every_key() {
3969 let (pool, _d) = scratch_pool(8);
3970 let last_leaf = Cell::new(None);
3971 let fast_path_hits = Cell::new(0);
3972 let fast_path_attempts = Cell::new(0);
3973 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
3974
3975 let capacity = crate::page::PAGE_SIZE - crate::page::HEADER_LEN;
3976 let key_len = 4usize;
3977 let small_val_len = 8usize;
3978 // Page-budget cost of one record: 4 (slot directory) + 2 (klen) +
3979 // key + 2 (vlen) + value.
3980 let small_cost = 8 + key_len + small_val_len;
3981 let n = capacity / small_cost;
3982
3983 for i in 0..n as u32 {
3984 t.insert(&i.to_be_bytes(), &vec![0xABu8; small_val_len]).unwrap();
3985 }
3986 let root_before = t.root();
3987
3988 // Grow the replacement so it exactly consumes what's left after the
3989 // other (n - 1) entries plus the reclaimed cost of the one being
3990 // replaced — the boundary the bug lived on.
3991 let free_after_fill = capacity - n * small_cost;
3992 let big_val_len = free_after_fill + small_cost - 8 - key_len;
3993 let big_val = vec![0xCDu8; big_val_len];
3994 t.insert(&0u32.to_be_bytes(), &big_val).unwrap();
3995
3996 assert_eq!(t.root(), root_before, "an in-place replace must not split the leaf");
3997 assert_eq!(t.get(&0u32.to_be_bytes()).unwrap().as_deref(), Some(&big_val[..]));
3998 for i in 1..n as u32 {
3999 assert_eq!(
4000 t.get(&i.to_be_bytes()).unwrap().as_deref(),
4001 Some(&vec![0xABu8; small_val_len][..]),
4002 "key {i} missing after replacing an unrelated key"
4003 );
4004 }
4005 }
4006
4007 /// 200 replacements of ONE key must never split its leaf. If `compact`
4008 /// were decorative (or absent), each replace would abandon its old
4009 /// payload without reclaiming it, `free_ptr` would march toward zero
4010 /// every time regardless of how much is actually live, and the leaf
4011 /// would exhaust its free space and split long before 200 iterations —
4012 /// even though at most one entry is ever live at once. A stable root is
4013 /// what proves compaction is actually reclaiming space, not just
4014 /// present in the source.
4015 #[test]
4016 fn repeated_replacement_of_one_key_does_not_exhaust_its_leaf() {
4017 let (pool, _d) = scratch_pool(8);
4018 let last_leaf = Cell::new(None);
4019 let fast_path_hits = Cell::new(0);
4020 let fast_path_attempts = Cell::new(0);
4021 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4022 t.insert(b"k", &[0u8; 20]).unwrap();
4023 let root_before = t.root();
4024
4025 for i in 0..200u32 {
4026 let val = vec![i as u8; 20];
4027 t.insert(b"k", &val).unwrap();
4028 }
4029
4030 assert_eq!(t.root(), root_before, "200 replacements of one key must not split its leaf");
4031 assert_eq!(t.get(b"k").unwrap().as_deref(), Some(&vec![199u8; 20][..]));
4032 }
4033
4034 /// The pathological shape the byte-crossing heuristic got wrong: many small
4035 /// records plus one very large one landing near the cut. Cutting at the first
4036 /// prefix to cross half the bytes puts more than a page on the left.
4037 ///
4038 /// The big value's size (2500, not the 3000 originally specified) was
4039 /// picked by checking the arithmetic directly rather than by guessing: with
4040 /// 21 preceding 76-byte-cost small entries (1596 bytes) and 19 following
4041 /// (1444 bytes), a 3000-byte value costs 3016 in page-budget terms, and
4042 /// 1596 + 3016 = 4612 exceeds the 4056-byte usable page on EVERY possible
4043 /// cut, before or after the big record — no two-way split exists at all for
4044 /// that size, so `split_point` correctly returns `None` and the insert is
4045 /// correctly refused, which is not what this test is trying to demonstrate.
4046 /// 2500 costs 2516, low enough that a valid cut exists right before the big
4047 /// record (1596 / 3960, both under 4056), while still being large enough
4048 /// that the OLD "first prefix to cross half the bytes" heuristic picks the
4049 /// cut AFTER the big record instead (1596 + 2516 = 4112, over capacity) —
4050 /// so this size is still squarely in the region the fix is for.
4051 #[test]
4052 fn a_split_with_one_huge_record_among_many_small_ones_keeps_every_key() {
4053 let (pool, _d) = scratch_pool(32);
4054 let last_leaf = Cell::new(None);
4055 let fast_path_hits = Cell::new(0);
4056 let fast_path_attempts = Cell::new(0);
4057 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4058 let mut keys: Vec<[u8; 8]> = Vec::new();
4059 for i in 0..40u64 {
4060 let k = (i * 2).to_be_bytes();
4061 t.insert(&k, &[b'a'; 60]).unwrap();
4062 keys.push(k);
4063 }
4064 // A large value keyed to land in the middle of the run. The size is
4065 // derived, not chosen: see the doc comment above.
4066 let big = 41u64.to_be_bytes();
4067 t.insert(&big, &vec![b'Z'; 2500]).unwrap();
4068 keys.push(big);
4069
4070 for k in &keys[..40] {
4071 let got = t.get(k).unwrap();
4072 assert!(got.is_some(), "key {k:?} lost across the split");
4073 // Length AND content: outright loss is not the only way a split can
4074 // damage a neighbour.
4075 assert_eq!(got.unwrap(), vec![b'a'; 60], "key {k:?} was corrupted by the split");
4076 }
4077 assert_eq!(t.get(&big).unwrap().unwrap(), vec![b'Z'; 2500]);
4078 }
4079
4080 #[test]
4081 fn a_range_scan_returns_keys_in_order_across_leaf_boundaries() {
4082 let (pool, _d) = scratch_pool(8);
4083 let last_leaf = Cell::new(None);
4084 let fast_path_hits = Cell::new(0);
4085 let fast_path_attempts = Cell::new(0);
4086 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4087 let n = 20_000u64;
4088 let scatter = |i: u64| i.wrapping_mul(0x9E37_79B9_7F4A_7C15);
4089 for i in 0..n { t.insert(&scatter(i).to_be_bytes(), b"v").unwrap(); }
4090
4091 let mut expect: Vec<[u8; 8]> = (0..n).map(|i| scatter(i).to_be_bytes()).collect();
4092 expect.sort();
4093
4094 let got: Vec<Vec<u8>> = t.range(&[]).unwrap()
4095 .map(|r| r.unwrap().0).collect();
4096 assert_eq!(got.len(), expect.len());
4097 assert!(got.iter().zip(&expect).all(|(a, b)| a.as_slice() == b.as_slice()),
4098 "scan order must equal sorted order");
4099 }
4100
4101 #[test]
4102 fn a_range_scan_from_a_midpoint_starts_there() {
4103 let (pool, _d) = scratch_pool(8);
4104 let last_leaf = Cell::new(None);
4105 let fast_path_hits = Cell::new(0);
4106 let fast_path_attempts = Cell::new(0);
4107 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4108 for i in 0..1000u64 { t.insert(&i.to_be_bytes(), b"v").unwrap(); }
4109 let first = t.range(&500u64.to_be_bytes()).unwrap().next().unwrap().unwrap().0;
4110 assert_eq!(first, 500u64.to_be_bytes().to_vec());
4111 }
4112
4113 /// Law 1 for the scan path: peak memory is one leaf, whatever the result size.
4114 ///
4115 /// Assert on `peak_pins`, NOT on `frames_total`. `frames_total` is the pool's
4116 /// fixed capacity, set once at construction and never mutated, so an
4117 /// assertion that it did not change is true before the scan, true after it,
4118 /// and would still be true if the iterator pinned every leaf at once. It
4119 /// cannot fail. `peak_pins` is a real measurement.
4120 #[test]
4121 fn a_full_scan_pins_one_leaf_at_a_time() {
4122 let (pool, _d) = scratch_pool(8);
4123 let last_leaf = Cell::new(None);
4124 let fast_path_hits = Cell::new(0);
4125 let fast_path_attempts = Cell::new(0);
4126 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4127 for i in 0..50_000u64 { t.insert(&i.to_be_bytes(), b"v").unwrap(); }
4128
4129 pool.reset_peak_pins();
4130 let before = pool.stats();
4131 let n = t.range(&[]).unwrap().count();
4132 let after = pool.stats();
4133
4134 assert_eq!(n, 50_000);
4135 assert_eq!(after.peak_pins, 1, "a scan must hold exactly one leaf at a time");
4136 assert!(after.evictions > before.evictions, "50k keys over 8 frames must evict");
4137 }
4138
4139 /// A PULL cursor -- peek, use, step -- is how the query executor walks a
4140 /// scan, and it must cost what the callback walk costs. `peek_ref` used
4141 /// to run the whole `position_at_or_after` seek EVERY time, which is a
4142 /// second page open and two more record decodes per row to arrive back
4143 /// at the record the cursor was already standing on. Counted, not timed:
4144 /// a walk of 50,000 keys must seek about once per leaf, and there are
4145 /// nowhere near 50,000 leaves.
4146 #[test]
4147 fn a_pull_cursor_seeks_once_per_leaf_not_once_per_row() {
4148 let (pool, _d) = scratch_pool(64);
4149 let last_leaf = Cell::new(None);
4150 let fast_path_hits = Cell::new(0);
4151 let fast_path_attempts = Cell::new(0);
4152 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4153 for i in 0..50_000u64 {
4154 t.insert(&i.to_be_bytes(), b"v").unwrap();
4155 }
4156 let mut iter = t.range(&[]).unwrap();
4157 let mut rows = 0u64;
4158 while iter.peek_ref().unwrap().is_some() {
4159 iter.step();
4160 rows += 1;
4161 }
4162 assert_eq!(rows, 50_000);
4163 let leaves = u64::from(iter.leaves_stepped());
4164 assert!(
4165 iter.seeks() <= leaves + 2,
4166 "a pull walk of {rows} rows over {leaves} leaves seeked {} times; one seek per leaf (plus the first and the last) is the shape",
4167 iter.seeks()
4168 );
4169 }
4170
4171 /// The fast path above must never serve an overflow marker's 12-byte
4172 /// locator where the value belongs. A leaf holding one is excluded
4173 /// wholesale, so every record of it goes the seek way.
4174 #[test]
4175 fn a_pull_cursor_still_resolves_overflow_values() {
4176 let (pool, _d) = scratch_pool(64);
4177 let last_leaf = Cell::new(None);
4178 let fast_path_hits = Cell::new(0);
4179 let fast_path_attempts = Cell::new(0);
4180 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4181 let big = vec![7u8; 9_000];
4182 for i in 0..64u64 {
4183 if i % 8 == 0 {
4184 t.insert(&i.to_be_bytes(), &big).unwrap();
4185 } else {
4186 t.insert(&i.to_be_bytes(), b"v").unwrap();
4187 }
4188 }
4189 let mut iter = t.range(&[]).unwrap();
4190 let mut seen = 0u64;
4191 loop {
4192 let (key, value) = {
4193 let Some((key, value)) = iter.peek_ref().unwrap() else { break };
4194 (key.to_vec(), value.to_vec())
4195 };
4196 let i = u64::from_be_bytes(key.as_slice().try_into().unwrap());
4197 if i % 8 == 0 {
4198 assert_eq!(value, big, "key {i} must read back its overflow value");
4199 } else {
4200 assert_eq!(value, b"v", "key {i} must read back its inline value");
4201 }
4202 iter.step();
4203 seen += 1;
4204 }
4205 assert_eq!(seen, 64);
4206 }
4207
4208 #[test]
4209 fn a_scan_starting_past_the_last_key_yields_nothing() {
4210 let (pool, _d) = scratch_pool(8);
4211 let last_leaf = Cell::new(None);
4212 let fast_path_hits = Cell::new(0);
4213 let fast_path_attempts = Cell::new(0);
4214 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4215 for i in 0..1000u64 { t.insert(&i.to_be_bytes(), b"v").unwrap(); }
4216 assert_eq!(t.range(&u64::MAX.to_be_bytes()).unwrap().count(), 0);
4217 }
4218
4219 #[test]
4220 fn a_scan_of_an_empty_tree_yields_nothing() {
4221 let (pool, _d) = scratch_pool(8);
4222 let last_leaf = Cell::new(None);
4223 let fast_path_hits = Cell::new(0);
4224 let fast_path_attempts = Cell::new(0);
4225 let t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4226 assert_eq!(t.range(&[]).unwrap().count(), 0);
4227 assert_eq!(t.range(&42u64.to_be_bytes()).unwrap().count(), 0);
4228 }
4229
4230 fn peek_owned(iter: &mut RangeIter<'_>, target: &[u8]) -> Option<(Vec<u8>, Vec<u8>)> {
4231 iter.peek_at_or_after(target)
4232 .unwrap()
4233 .map(|(k, v)| (k.to_vec(), v.to_vec()))
4234 }
4235
4236 fn collect_next(t: &BTree<'_>) -> Vec<(Vec<u8>, Vec<u8>)> {
4237 t.range(&[]).unwrap().collect::<Result<Vec<_>>>().unwrap()
4238 }
4239
4240 /// `peek_at_or_after` on a sequence of ascending targets must return the
4241 /// same key/value as the first allocating `next()` record with key >= target.
4242 #[test]
4243 fn peek_at_or_after_matches_allocating_iterator_on_random_trees() {
4244 let (pool, _d) = scratch_pool(32);
4245 let last_leaf = Cell::new(None);
4246 let hits = Cell::new(0);
4247 let tries = Cell::new(0);
4248 let mut t = BTree::create(&pool, 1, &last_leaf, &hits, &tries).unwrap();
4249 let n = 800u64;
4250 let scatter = |i: u64| i.wrapping_mul(0x9E37_79B9_7F4A_7C15).to_be_bytes().to_vec();
4251 for i in 0..n {
4252 t.insert(&scatter(i), &i.to_le_bytes()).unwrap();
4253 }
4254 let all = collect_next(&t);
4255 assert_eq!(all.len(), n as usize);
4256
4257 let mut targets: Vec<Vec<u8>> = Vec::new();
4258 targets.push(Vec::new());
4259 for (i, (k, _)) in all.iter().enumerate() {
4260 targets.push(k.clone());
4261 if i + 1 < all.len() {
4262 let mut mid = k.clone();
4263 if let Some(last) = mid.last_mut() {
4264 *last = last.saturating_add(1);
4265 }
4266 if mid.as_slice() < all[i + 1].0.as_slice() {
4267 targets.push(mid);
4268 }
4269 }
4270 }
4271 targets.push(vec![0xff; 16]);
4272 targets.sort();
4273 targets.dedup();
4274
4275 let mut peek = t.range(&[]).unwrap();
4276 for target in &targets {
4277 let expected = all
4278 .iter()
4279 .find(|(k, _)| k.as_slice() >= target.as_slice())
4280 .cloned();
4281 assert_eq!(peek_owned(&mut peek, target), expected, "target {target:?}");
4282 }
4283 }
4284
4285 #[test]
4286 fn peek_at_or_after_crosses_leaf_boundaries() {
4287 let (pool, _d) = scratch_pool(16);
4288 let last_leaf = Cell::new(None);
4289 let hits = Cell::new(0);
4290 let tries = Cell::new(0);
4291 let mut t = BTree::create(&pool, 1, &last_leaf, &hits, &tries).unwrap();
4292 let n = 2_000u64;
4293 for i in 0..n {
4294 t.insert(&i.to_be_bytes(), &i.to_le_bytes()).unwrap();
4295 }
4296 let all = collect_next(&t);
4297 assert_eq!(all.len(), n as usize);
4298
4299 let mut peek = t.range(&[]).unwrap();
4300 for (k, v) in &all {
4301 assert_eq!(peek_owned(&mut peek, k).as_ref(), Some(&(k.clone(), v.clone())));
4302 }
4303 // Jumping onto a key that is not first in its leaf, then walking to the end.
4304 let mid = &all[all.len() / 2].0;
4305 let mut peek = t.range(&[]).unwrap();
4306 let got = peek_owned(&mut peek, mid);
4307 assert_eq!(got.as_ref(), Some(&all[all.len() / 2]));
4308 let last = &all[all.len() - 1].0;
4309 assert_eq!(peek_owned(&mut peek, last).as_ref(), Some(&all[all.len() - 1]));
4310 }
4311
4312 #[test]
4313 fn peek_at_or_after_target_beyond_the_end_is_none() {
4314 let (pool, _d) = scratch_pool(8);
4315 let last_leaf = Cell::new(None);
4316 let hits = Cell::new(0);
4317 let tries = Cell::new(0);
4318 let mut t = BTree::create(&pool, 1, &last_leaf, &hits, &tries).unwrap();
4319 for i in 0..200u64 {
4320 t.insert(&i.to_be_bytes(), b"v").unwrap();
4321 }
4322 let mut peek = t.range(&[]).unwrap();
4323 assert!(peek_owned(&mut peek, &u64::MAX.to_be_bytes()).is_none());
4324 assert!(peek_owned(&mut peek, &u64::MAX.to_be_bytes()).is_none());
4325 let mut peek = t.range(&500u64.to_be_bytes()).unwrap();
4326 assert!(peek_owned(&mut peek, &u64::MAX.to_be_bytes()).is_none());
4327 }
4328
4329 #[test]
4330 fn peek_at_or_after_on_an_empty_range_is_none() {
4331 let (pool, _d) = scratch_pool(8);
4332 let last_leaf = Cell::new(None);
4333 let hits = Cell::new(0);
4334 let tries = Cell::new(0);
4335 let t = BTree::create(&pool, 1, &last_leaf, &hits, &tries).unwrap();
4336 let mut peek = t.range(&[]).unwrap();
4337 assert!(peek_owned(&mut peek, b"").is_none());
4338 assert!(peek_owned(&mut peek, b"z").is_none());
4339 let mut peek = t.range(b"mid").unwrap();
4340 assert!(peek_owned(&mut peek, b"mid").is_none());
4341 }
4342
4343 /// Sequential peeks of every key must pin each leaf once, not once per row.
4344 #[test]
4345 fn peek_at_or_after_pool_gets_are_per_leaf_not_per_row() {
4346 let (pool, _d) = scratch_pool(32);
4347 let last_leaf = Cell::new(None);
4348 let hits = Cell::new(0);
4349 let tries = Cell::new(0);
4350 let mut t = BTree::create(&pool, 1, &last_leaf, &hits, &tries).unwrap();
4351 let n = 3_000u64;
4352 for i in 0..n {
4353 t.insert(&i.to_be_bytes(), &i.to_le_bytes()).unwrap();
4354 }
4355 let keys: Vec<_> = collect_next(&t).into_iter().map(|(k, _)| k).collect();
4356 let before = pool.stats();
4357 let mut peek = t.range(&[]).unwrap();
4358 for k in &keys {
4359 assert!(peek_owned(&mut peek, k).is_some());
4360 }
4361 let after = pool.stats();
4362 let gets = (after.hits + after.misses) - (before.hits + before.misses);
4363 println!("peek_at_or_after over {n} rows: {gets} pool gets");
4364 assert!(
4365 gets < n / 8,
4366 "peek over {n} rows charged {gets} pool gets; expected O(leaves)"
4367 );
4368 }
4369
4370 #[test]
4371 fn peek_at_or_after_does_not_allocate_per_row() {
4372 let (pool, _d) = scratch_pool(32);
4373 let last_leaf = Cell::new(None);
4374 let hits = Cell::new(0);
4375 let tries = Cell::new(0);
4376 let mut t = BTree::create(&pool, 1, &last_leaf, &hits, &tries).unwrap();
4377 let n = 2_000u64;
4378 for i in 0..n {
4379 t.insert(&i.to_be_bytes(), &i.to_le_bytes()).unwrap();
4380 }
4381 let (_, allocs, bytes) = crate::test_alloc::measured(|| {
4382 let mut peek = t.range(&[]).unwrap();
4383 for i in 0..n {
4384 let key = i.to_be_bytes();
4385 let hit = peek.peek_at_or_after(&key).unwrap();
4386 let (k, v) = hit.expect("key present");
4387 assert_eq!(k, key);
4388 assert_eq!(v, i.to_le_bytes());
4389 }
4390 });
4391 println!("peek_at_or_after over {n} rows: {allocs} allocations, {bytes} bytes");
4392 assert!(
4393 allocs < 32,
4394 "peek over {n} in-leaf rows allocated {allocs} times; expected O(1)"
4395 );
4396 }
4397
4398 #[test]
4399 fn deleted_keys_are_gone_and_their_neighbours_are_not() {
4400 let (pool, _d) = scratch_pool(8);
4401 let last_leaf = Cell::new(None);
4402 let fast_path_hits = Cell::new(0);
4403 let fast_path_attempts = Cell::new(0);
4404 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4405 let n = 10_000u64;
4406 for i in 0..n { t.insert(&i.to_be_bytes(), &i.to_le_bytes()).unwrap(); }
4407 for i in (0..n).step_by(2) { assert!(t.delete(&i.to_be_bytes()).unwrap()); }
4408
4409 for i in 0..n {
4410 let got = t.get(&i.to_be_bytes()).unwrap();
4411 if i % 2 == 0 { assert!(got.is_none(), "{i} should be gone"); }
4412 else { assert_eq!(got.as_deref(), Some(&i.to_le_bytes()[..]), "{i} was collateral"); }
4413 }
4414 // And the scan agrees with the point lookups.
4415 let scanned = t.range(&[]).unwrap().count();
4416 assert_eq!(scanned as u64, n / 2);
4417 }
4418
4419 #[test]
4420 fn deleting_a_key_that_is_not_there_reports_so() {
4421 let (pool, _d) = scratch_pool(8);
4422 let last_leaf = Cell::new(None);
4423 let fast_path_hits = Cell::new(0);
4424 let fast_path_attempts = Cell::new(0);
4425 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4426 t.insert(b"a", b"1").unwrap();
4427 assert!(!t.delete(b"zzz").unwrap());
4428 }
4429
4430 /// Deletion must leave the sibling chain walkable. A leaf emptied by
4431 /// deletion is not unlinked — it stays in the chain with zero entries — so
4432 /// the scan has to pass through it rather than stopping there.
4433 #[test]
4434 fn a_scan_still_walks_leaves_that_deletion_emptied() {
4435 let (pool, _d) = scratch_pool(16);
4436 let last_leaf = Cell::new(None);
4437 let fast_path_hits = Cell::new(0);
4438 let fast_path_attempts = Cell::new(0);
4439 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4440 for i in 0..5_000u64 { t.insert(&i.to_be_bytes(), b"v").unwrap(); }
4441
4442 // Delete a long contiguous run, which should empty at least one leaf
4443 // outright while leaving keys on both sides of it.
4444 for i in 1_000..3_000u64 { assert!(t.delete(&i.to_be_bytes()).unwrap()); }
4445
4446 let seen: Vec<u64> = t.range(&[]).unwrap()
4447 .map(|r| u64::from_be_bytes(r.unwrap().0.try_into().unwrap()))
4448 .collect();
4449 assert_eq!(seen.len(), 3_000, "keys on the far side of an emptied leaf must survive");
4450 assert_eq!(seen.first().copied(), Some(0));
4451 assert_eq!(seen.last().copied(), Some(4_999));
4452 assert!(seen.windows(2).all(|w| w[0] < w[1]), "scan order must still be sorted");
4453 }
4454
4455 /// The cycle guard, actually exercised. Without it this scan never returns.
4456 ///
4457 /// The guard is the one path in the scan with no other coverage: the two
4458 /// boundary tests are about where a scan starts, not about how it refuses to
4459 /// run forever. An unbounded loop on a damaged sibling pointer is how the
4460 /// predecessor engine died, so the guard needs a test that would notice its
4461 /// removal.
4462 #[test]
4463 fn a_cyclic_sibling_chain_is_inert_because_scans_descend() {
4464 let (pool, _d) = scratch_pool(16);
4465 let last_leaf = Cell::new(None);
4466 let fast_path_hits = Cell::new(0);
4467 let fast_path_attempts = Cell::new(0);
4468 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4469 for i in 0..5000u64 { t.insert(&i.to_be_bytes(), b"v").unwrap(); }
4470
4471 // The leftmost leaf, and the one it points at.
4472 let first = { let (leaf, _) = t.descend(&[]).unwrap(); leaf };
4473 let second = {
4474 let r = pool.get(first).unwrap();
4475 PageRef::open_resident(&r, first).unwrap().next_leaf()
4476 };
4477 assert_ne!(second, 0, "this fixture needs at least two leaves");
4478
4479 // Point the second leaf back at the first: A -> B -> A.
4480 {
4481 let mut w = pool.get_mut(second).unwrap();
4482 let mut p = PageMut::reopen(w.bytes_mut());
4483 p.set_next_leaf(first);
4484 p.finalise(0);
4485 }
4486
4487 // 2f: scans advance by separator re-descent, never by the sibling
4488 // pointer, so the planted cycle must be INERT -- the scan completes,
4489 // terminates, and serves every key exactly once. (Before 2f this
4490 // fixture asserted the cycle was detected and refused; now the walk
4491 // that could meet it no longer exists. The leaves-vs-file-size guard
4492 // in `advance` still bounds a corrupt-interior descent loop.)
4493 let mut n = 0u64;
4494 for item in t.range(&[]).unwrap() {
4495 let (k, _) = item.expect("cycle in the DEAD sibling chain must not affect the scan");
4496 assert_eq!(k, n.to_be_bytes().to_vec(), "keys in order, exactly once");
4497 n += 1;
4498 }
4499 assert_eq!(n, 5000, "every key served exactly once despite the cycle");
4500 }
4501
4502 // -- Task 15: one write guard per insert, and the append fast path --
4503
4504 /// Ascending keys never require a split within this run (100 tiny
4505 /// records fit easily in one 4056-byte-usable leaf), so only the very
4506 /// first insert -- before `last_leaf` is ever set -- pays for a
4507 /// descent. Every insert after it satisfies all five fast-path checks:
4508 /// same leaf, same tree, rightmost (no split ever occurs), room, and a
4509 /// strictly increasing key. Asserting the exact count (not `> 0`) is
4510 /// what would catch a fast path that only fires sometimes.
4511 #[test]
4512 fn insert_ascending_uses_fast_path() {
4513 let (pool, _d) = scratch_pool(4);
4514 let last_leaf = Cell::new(None);
4515 let fast_path_hits = Cell::new(0);
4516 let fast_path_attempts = Cell::new(0);
4517 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4518 let n = 100u64;
4519 for i in 0..n { t.insert(&i.to_be_bytes(), b"v").unwrap(); }
4520 assert_eq!(
4521 t.fast_path_hits.get(), n - 1,
4522 "every insert but the first must hit the append fast path"
4523 );
4524 }
4525
4526 /// Descending keys can never satisfy "strictly greater than the last
4527 /// key on the page" -- each new key is smaller than everything already
4528 /// there -- so the fast path must never fire, not even once after the
4529 /// first insert sets the hint.
4530 #[test]
4531 fn insert_descending_never_uses_fast_path() {
4532 let (pool, _d) = scratch_pool(4);
4533 let last_leaf = Cell::new(None);
4534 let fast_path_hits = Cell::new(0);
4535 let fast_path_attempts = Cell::new(0);
4536 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4537 let n = 100u64;
4538 for i in (0..n).rev() { t.insert(&i.to_be_bytes(), b"v").unwrap(); }
4539 assert_eq!(
4540 t.fast_path_hits.get(), 0,
4541 "a descending insert order must never satisfy the strictly-greater check"
4542 );
4543 }
4544
4545 /// A hint left pointing at another tree's leaf must be refused by the
4546 /// tree_id check, not used -- five trees can share one file, and a page
4547 /// from another tree parses as a perfectly valid page. Build two trees
4548 /// in one store, force tree 1's hint onto tree 2's leaf, and confirm
4549 /// both trees come out uncorrupted.
4550 #[test]
4551 fn fast_path_rejects_wrong_tree() {
4552 let (pool, _d) = scratch_pool(8);
4553 let last_leaf1 = Cell::new(None);
4554 let fast_path_hits1 = Cell::new(0);
4555 let fast_path_attempts1 = Cell::new(0);
4556 let last_leaf2 = Cell::new(None);
4557 let fast_path_hits2 = Cell::new(0);
4558 let fast_path_attempts2 = Cell::new(0);
4559 let mut t1 = BTree::create(&pool, 1, &last_leaf1, &fast_path_hits1, &fast_path_attempts1).unwrap();
4560 let mut t2 = BTree::create(&pool, 2, &last_leaf2, &fast_path_hits2, &fast_path_attempts2).unwrap();
4561 t1.insert(b"a", b"1").unwrap();
4562 t2.insert(b"z", b"2").unwrap();
4563
4564 // Force t1's hint onto t2's leaf, as a stale/corrupted hint would.
4565 let t2_leaf = t2.root();
4566 t1.last_leaf.set(Some(t2_leaf));
4567
4568 // "zz" sorts after "z", t2's only (and last) key -- so the
4569 // ordering check alone would let this through. Only the tree_id
4570 // check stops it from landing on t2's leaf.
4571 t1.insert(b"zz", b"3").unwrap();
4572
4573 assert_eq!(
4574 t1.fast_path_hits.get(), 0,
4575 "a hint pointing into another tree must fall back, not be used"
4576 );
4577 assert_eq!(t1.get(b"a").unwrap().as_deref(), Some(&b"1"[..]));
4578 assert_eq!(t1.get(b"zz").unwrap().as_deref(), Some(&b"3"[..]));
4579 assert_eq!(
4580 t2.get(b"z").unwrap().as_deref(), Some(&b"2"[..]),
4581 "t2 must be untouched by t1's misdirected hint"
4582 );
4583 assert_eq!(
4584 t2.range(&[]).unwrap().count(), 1,
4585 "t2 must not have gained a record that belonged to t1"
4586 );
4587 }
4588
4589 /// After a split, the leaf that used to be rightmost has a right
4590 /// sibling and must never be used as an append target again, even for
4591 /// a key that legitimately belongs on it. Force the hint back onto that
4592 /// now-interior-ish (non-rightmost) leaf and confirm the insert still
4593 /// lands correctly, via fallback, with nothing else disturbed.
4594 #[test]
4595 fn fast_path_rejects_non_rightmost() {
4596 let (pool, _d) = scratch_pool(8);
4597 let last_leaf = Cell::new(None);
4598 let fast_path_hits = Cell::new(0);
4599 let fast_path_attempts = Cell::new(0);
4600 let mut t = BTree::create(&pool, 1, &last_leaf, &fast_path_hits, &fast_path_attempts).unwrap();
4601
4602 // Even keys only, so odd keys are legitimate "new, in-range" keys
4603 // with no split-order ambiguity. Enough of them (400, at ~17 bytes
4604 // of page-budget cost each) to force at least one split of the
4605 // single starting leaf.
4606 let n = 400u64;
4607 for i in 0..n { t.insert(&(i * 2).to_be_bytes(), b"v").unwrap(); }
4608
4609 let (left_leaf, _) = t.descend(&0u64.to_be_bytes()).unwrap();
4610 let last_in_left = {
4611 let r = pool.get(left_leaf).unwrap();
4612 let p = PageRef::open_resident(&r, left_leaf).unwrap();
4613 assert_ne!(p.next_leaf(), 0, "fixture needs the left leaf to have a right sibling");
4614 u64::from_be_bytes(
4615 validated_key(p.slot(p.nentries() - 1))
4616 .try_into()
4617 .unwrap(),
4618 )
4619 };
4620
4621 t.last_leaf.set(Some(left_leaf));
4622 let hits_before = t.fast_path_hits.get();
4623
4624 // The odd key immediately after the left leaf's own last key: this
4625 // is the one candidate that is BOTH strictly greater than the last
4626 // key on the (stale) hinted leaf -- satisfying the ordering check
4627 // on its own -- AND still genuinely in-range for that leaf (it
4628 // sorts below the separator, since the next even key belongs to the
4629 // right leaf). Only the rightmost check can still catch it.
4630 let candidate = last_in_left + 1;
4631 t.insert(&candidate.to_be_bytes(), b"new").unwrap();
4632
4633 assert_eq!(
4634 t.fast_path_hits.get(), hits_before,
4635 "a non-rightmost hint must never be used, even for a key that is both \
4636 in-range and greater than the hinted leaf's last key"
4637 );
4638 assert_eq!(
4639 t.get(&candidate.to_be_bytes()).unwrap().as_deref(), Some(&b"new"[..]),
4640 "the insert must still land correctly via fallback"
4641 );
4642 for i in 0..n {
4643 assert_eq!(
4644 t.get(&(i * 2).to_be_bytes()).unwrap().as_deref(), Some(&b"v"[..]),
4645 "key {i} corrupted by the stale hint"
4646 );
4647 }
4648 }
4649
4650 /// The correctness net for both changes at once: the same 20k keys,
4651 /// inserted once in ascending order (exercising the fast path
4652 /// constantly) and once in a scattered order (exercising
4653 /// `descend_for_write` and splits constantly, since a scattered key is
4654 /// essentially never greater than the rightmost leaf's last key), must
4655 /// produce byte-identical range scans.
4656 #[test]
4657 fn random_order_matches_sequential() {
4658 let n = 20_000u64;
4659 let scatter = |i: u64| i.wrapping_mul(0x9E37_79B9_7F4A_7C15);
4660
4661 let (pool_a, _da) = scratch_pool(16);
4662 let last_leaf_a = Cell::new(None);
4663 let fast_path_hits_a = Cell::new(0);
4664 let fast_path_attempts_a = Cell::new(0);
4665 let mut a = BTree::create(&pool_a, 1, &last_leaf_a, &fast_path_hits_a, &fast_path_attempts_a).unwrap();
4666 for i in 0..n { a.insert(&i.to_be_bytes(), &i.to_le_bytes()).unwrap(); }
4667
4668 let (pool_b, _db) = scratch_pool(16);
4669 let last_leaf_b = Cell::new(None);
4670 let fast_path_hits_b = Cell::new(0);
4671 let fast_path_attempts_b = Cell::new(0);
4672 let mut b = BTree::create(&pool_b, 1, &last_leaf_b, &fast_path_hits_b, &fast_path_attempts_b).unwrap();
4673 let mut order: Vec<u64> = (0..n).collect();
4674 order.sort_by_key(|&i| scatter(i));
4675 for &i in &order { b.insert(&i.to_be_bytes(), &i.to_le_bytes()).unwrap(); }
4676
4677 let seq_a: Vec<(Vec<u8>, Vec<u8>)> = a.range(&[]).unwrap().map(|r| r.unwrap()).collect();
4678 let seq_b: Vec<(Vec<u8>, Vec<u8>)> = b.range(&[]).unwrap().map(|r| r.unwrap()).collect();
4679 assert_eq!(seq_a.len(), n as usize);
4680 assert_eq!(
4681 seq_a, seq_b,
4682 "ascending vs scattered insertion order must produce identical range scans"
4683 );
4684 }
4685
4686 // ---------------------------------------------------------- D9 per-keyspace append
4687 //
4688 // One tree holds several key TAGS (D3/D4: vectors, rows and external-key
4689 // mappings are keyspaces, not files). `src/collections/mod.rs` writes one
4690 // document as three keys -- 0x60 vector, 0x40 row, 0x20 mapping -- and each
4691 // of those three runs is ASCENDING in itself. Only the highest tag is ever
4692 // the TREE's rightmost leaf, so D9's `next_leaf() == 0` guard fires for
4693 // 0x60 and never for 0x40 or 0x20.
4694 //
4695 // What that costs is WORK, not space: `redistribute_neighbors` keeps the
4696 // lower two runs densely packed (measured below), and pays for it with a
4697 // clone of the parent's records plus up to three siblings' records and up
4698 // to four rebuilt page images on every leaf fill, forever.
4699
4700 /// `src/collections/mod.rs`'s width-tagged big-endian integer component.
4701 fn ordered(n: u64) -> Vec<u8> {
4702 let b = n.to_be_bytes();
4703 let start = b.iter().position(|x| *x != 0).unwrap_or(7);
4704 let mut k = vec![0x80 + (8 - start) as u8];
4705 k.extend_from_slice(&b[start..]);
4706 k
4707 }
4708
4709 /// One document's key for `tag`, ascending in `i`. Shaped exactly like
4710 /// `src/collections/mod.rs`: `[tag][ordered(collection)][...]`.
4711 fn tagged(tag: u8, i: u64) -> Vec<u8> {
4712 let mut k = vec![tag];
4713 k.extend(ordered(1));
4714 match tag {
4715 // mapping: the caller's external key, a string
4716 0x20 => k.extend_from_slice(format!("key-{i:012}").as_bytes()),
4717 // row: the entity sequence
4718 0x40 => k.extend(ordered(i + 1)),
4719 // vector: the row key plus a field ordinal
4720 _ => {
4721 k.extend(ordered(i + 1));
4722 k.extend(ordered(0));
4723 }
4724 }
4725 k
4726 }
4727
4728 /// The three keyspaces store very differently sized values, and that is the
4729 /// point: a mapping row is a few bytes, a document row a few hundred.
4730 fn tagged_value(tag: u8, i: u64) -> Vec<u8> {
4731 match tag {
4732 0x20 => ordered(i + 1),
4733 // A document, varying in length the way real documents do.
4734 0x40 => vec![b'd'; 200 + (i % 97) as usize * 2],
4735 // Eight f32 lanes.
4736 _ => vec![b'f'; 32],
4737 }
4738 }
4739
4740 /// Insert `docs` documents, each as one key per tag in `tags`, in that
4741 /// order -- `src/collections/mod.rs` writes vector, then row, then mapping.
4742 fn load_interleaved(t: &mut BTree<'_>, tags: &[u8], docs: u64) {
4743 for i in 0..docs {
4744 for tag in tags {
4745 t.insert(&tagged(*tag, i), &tagged_value(*tag, i)).unwrap();
4746 }
4747 }
4748 }
4749
4750 /// Every leaf of the tree, as (tag of its first key, payload bytes used,
4751 /// whether the leaf mixes tags). Walks THROUGH THE PARENTS rather than the
4752 /// sibling chain, which is the only walk this file trusts.
4753 fn leaves(pool: &BufferPool, page_no: u32, out: &mut Vec<(u8, usize, bool)>) {
4754 let kids = {
4755 let r = pool.get(page_no).unwrap();
4756 let p = PageRef::open_resident(&r, page_no).unwrap();
4757 match p.kind() {
4758 PageKind::Leaf => {
4759 if p.nentries() > 0 {
4760 let tag = validated_key(p.slot(0))[0];
4761 let mixed = (0..p.nentries()).any(|i| validated_key(p.slot(i))[0] != tag);
4762 let used: usize = (0..p.nentries()).map(|i| p.slot(i).len() + 4).sum();
4763 out.push((tag, used, mixed));
4764 }
4765 return;
4766 }
4767 PageKind::Interior => {
4768 let mut kids = vec![p.child0()];
4769 kids.extend((0..p.nentries()).map(|i| validated_child(p.slot(i))));
4770 kids
4771 }
4772 other => panic!("unexpected page kind in the tree: {other:?}"),
4773 }
4774 };
4775 for k in kids {
4776 leaves(pool, k, out);
4777 }
4778 }
4779
4780 /// Mean fill of the leaves whose keys all carry `tag`, and how many there
4781 /// are. Mixed-tag leaves are excluded: one per keyspace boundary exists by
4782 /// construction and it belongs to no single run.
4783 fn fill_of(all: &[(u8, usize, bool)], tag: u8) -> (f64, usize) {
4784 let usable = (PAGE_SIZE - crate::page::HEADER_LEN) as f64;
4785 let mine: Vec<usize> = all
4786 .iter()
4787 .filter(|(t, _, mixed)| *t == tag && !*mixed)
4788 .map(|(_, used, _)| *used)
4789 .collect();
4790 assert!(!mine.is_empty(), "no pure leaves for tag {tag:#04x}");
4791 (mine.iter().sum::<usize>() as f64 / mine.len() as f64 / usable, mine.len())
4792 }
4793
4794 /// Buffer-pool page accesses per insert for one interleaved load.
4795 ///
4796 /// No `TagHints` attached, deliberately: this is D9's own measurement and
4797 /// the number below is its control. K1's per-keyspace HINT is measured
4798 /// separately, by `accesses_for_tag`, on a tree that has one.
4799 ///
4800 /// Page accesses are what this repo measures cost in (D14's 1.1 reads per
4801 /// hop, D13's 4.2 vs 90.3 reads per trace query). They are exact, they are
4802 /// already instrumented, and they do not move with the machine.
4803 fn accesses_per_insert(tags: &[u8], docs: u64) -> f64 {
4804 let (pool, _d) = scratch_pool(512);
4805 let last_leaf = Cell::new(None);
4806 let hits = Cell::new(0);
4807 let attempts = Cell::new(0);
4808 let mut t = BTree::create(&pool, 1, &last_leaf, &hits, &attempts).unwrap();
4809 load_interleaved(&mut t, tags, docs);
4810 let s = pool.stats();
4811 (s.hits + s.misses) as f64 / (docs * tags.len() as u64) as f64
4812 }
4813
4814 /// THE COST PROPERTY. An ascending run's per-insert page work must not
4815 /// depend on how many other keyspaces happen to sit to its right.
4816 ///
4817 /// Each of these three runs, loaded on its own, costs about one page access
4818 /// per insert. Interleaved into one tree they cost several, and most of the
4819 /// difference is not the deeper tree: it is `redistribute_neighbors`
4820 /// re-reading and rewriting a parent and up to three siblings on every fill
4821 /// of a 0x40 or 0x20 leaf, because neither run is the tree's rightmost.
4822 ///
4823 /// Measured on this fixture, 5,000 documents = 15,000 inserts:
4824 /// `compact-cells,sqlite-balance` 71,007 accesses = 4.734/insert
4825 /// `compact-cells,sqlite-balance,keyspace-append` 60,882 accesses = 4.059/insert
4826 /// The single-tag arms are byte-identical between the two builds, which is
4827 /// the guard that nothing D9 already covered was disturbed.
4828 ///
4829 /// The bound below sits between those two figures. It is an absolute number
4830 /// because this fixture is deterministic -- no randomness, no timing -- and
4831 /// because the quantity it bounds is the one the CPU sample of the 100K
4832 /// typed load pointed at: 23.3% of load time inside `split_leaf_and_insert`.
4833 #[test]
4834 fn sharing_a_tree_must_not_multiply_an_ascending_runs_page_work() {
4835 const DOCS: u64 = 5_000;
4836 let mixed = accesses_per_insert(&[0x60, 0x40, 0x20], DOCS);
4837 let alone: Vec<f64> = [0x60u8, 0x40, 0x20]
4838 .iter()
4839 .map(|t| accesses_per_insert(std::slice::from_ref(t), DOCS))
4840 .collect();
4841 eprintln!("interleaved {mixed:.3} accesses/insert; alone {alone:.3?}");
4842
4843 // Each run on its own already gets D9 and the append hint. This is the
4844 // control arm: it must not move, in either build.
4845 for (tag, a) in [0x60u8, 0x40, 0x20].iter().zip(&alone) {
4846 assert!(*a <= 1.50, "tag {tag:#04x} alone costs {a:.3} accesses/insert");
4847 }
4848 // This is the append optimization's acceptance bound. The balancing
4849 // control deliberately lacks it (4.734 in the measurements above).
4850 // Keep the standalone controls in every build.
4851 if cfg!(feature = "keyspace-append") {
4852 assert!(
4853 mixed <= 4.30,
4854 "interleaved keyspaces cost {mixed:.3} page accesses per insert, against {alone:.3?} \
4855 for the same three runs loaded separately"
4856 );
4857 }
4858 }
4859
4860 /// The density guard. `redistribute_neighbors` already packs these runs to
4861 /// ~0.96-0.99 of a page, so the append shortcut must not buy its cheaper
4862 /// splits with a fatter file -- which is the exact trade D9's own 2/3-fill
4863 /// discipline exists to refuse.
4864 #[cfg(feature = "sqlite-balance")]
4865 #[test]
4866 fn every_keyspace_packs_its_own_ascending_run() {
4867 const DOCS: u64 = 3_000;
4868 let (pool, _d) = scratch_pool(16);
4869 let last_leaf = Cell::new(None);
4870 let hits = Cell::new(0);
4871 let attempts = Cell::new(0);
4872 let mut t = BTree::create(&pool, 1, &last_leaf, &hits, &attempts).unwrap();
4873 load_interleaved(&mut t, &[0x60, 0x40, 0x20], DOCS);
4874
4875 let mut all = Vec::new();
4876 leaves(&pool, t.root, &mut all);
4877 let usable = PAGE_SIZE - crate::page::HEADER_LEN;
4878 let payload: usize = all.iter().map(|(_, used, _)| *used).sum();
4879 // The floor no policy can beat: the records themselves, wall to wall.
4880 let floor = payload.div_ceil(usable);
4881 let (top, top_n) = fill_of(&all, 0x60);
4882 let (row, row_n) = fill_of(&all, 0x40);
4883 let (map, map_n) = fill_of(&all, 0x20);
4884 eprintln!(
4885 "fill 0x20 {map:.3} ({map_n}) 0x40 {row:.3} ({row_n}) 0x60 {top:.3} ({top_n}) \
4886 leaves {} floor {floor}",
4887 all.len()
4888 );
4889
4890 for (tag, f) in [(0x20u8, map), (0x40, row), (0x60, top)] {
4891 assert!(f >= 0.90, "tag {tag:#04x} packs its leaves only {f:.3} full");
4892 }
4893 // Measured: 292 leaves against a 281-leaf floor without the shortcut,
4894 // 294 with it. 6% is the room that leaves, and no more.
4895 assert!(
4896 (all.len() as f64) <= floor as f64 * 1.06,
4897 "{} leaves for a {floor}-leaf payload",
4898 all.len()
4899 );
4900 }
4901
4902 // ------------------------------------------------ K1 per-keyspace append HINT
4903 //
4904 // D9 widened the append SPLIT to a key's own tag. The HINT stayed narrow:
4905 // `fast_path_leaf` believes a leaf only when `next_leaf() == 0`, the
4906 // rightmost leaf of the whole TREE, so a run behind a higher tag paid a
4907 // full root-to-leaf descent for every single row. Measured on the
4908 // relationship load (200K rows, 99,840 edges): 4.40 page accesses to write
4909 // one forward edge row and 4.35 to write its reverse, 95% of what was left
4910 // of the per-edge write once the endpoint and preflight reads were gone.
4911 //
4912 // `TagHints` remembers the leaf per (tree, tag) together with the FENCES
4913 // the arming descent walked, so the same append lands with one access.
4914
4915 /// A tree that keeps per-keyspace hints, and the state they borrow.
4916 /// The `Cell`s must outlive the tree, so the caller owns them.
4917 struct Hinted {
4918 last_leaf: Cell<Option<u32>>,
4919 hits: Cell<u64>,
4920 attempts: Cell<u64>,
4921 tags: TagHints,
4922 }
4923 impl Hinted {
4924 fn new() -> Self {
4925 Hinted { last_leaf: Cell::new(None), hits: Cell::new(0), attempts: Cell::new(0),
4926 tags: TagHints::default() }
4927 }
4928 fn tree<'p>(&'p self, pool: &'p BufferPool) -> BTree<'p> {
4929 BTree::create(pool, 1, &self.last_leaf, &self.hits, &self.attempts)
4930 .unwrap()
4931 .with_tags(&self.tags)
4932 }
4933 }
4934
4935 /// Page accesses per insert for the run under `measured`, with the other
4936 /// tags interleaved between them, counted only after `warm` documents so
4937 /// the tree has its final height and the hint has been armed at least once.
4938 fn accesses_for_tag(tags: &[u8], measured: u8, docs: u64, warm: u64) -> f64 {
4939 let (pool, _d) = scratch_pool(512);
4940 let h = Hinted::new();
4941 let mut t = h.tree(&pool);
4942 let at = |p: &BufferPool| { let s = p.stats(); s.hits + s.misses };
4943 let (mut spent, mut counted) = (0u64, 0u64);
4944 for i in 0..docs {
4945 for tag in tags {
4946 let before = at(&pool);
4947 t.insert(&tagged(*tag, i), &tagged_value(*tag, i)).unwrap();
4948 if *tag == measured && i >= warm {
4949 spent += at(&pool) - before;
4950 counted += 1;
4951 }
4952 }
4953 }
4954 spent as f64 / counted.max(1) as f64
4955 }
4956
4957 /// THE BUDGET. An ascending run under a tag that is NOT the tree's last
4958 /// must reach its leaf without descending.
4959 ///
4960 /// 0x71 is the forward-edge tag; 0x40 (rows) and 0x60 (vectors) are written
4961 /// between its keys exactly as the multimodel load writes them, and 0x71
4962 /// sorts after both, so nothing about it is the tree's rightmost leaf.
4963 ///
4964 /// One page access is the floor: the leaf itself has to be read. Splits
4965 /// happen about once per leaf-full of rows and cost a descent each, so the
4966 /// bound is a little above the floor rather than at it.
4967 #[test]
4968 fn an_ascending_run_behind_other_keyspaces_reaches_its_leaf_without_descending() {
4969 const DOCS: u64 = 1_000;
4970 let edges = accesses_for_tag(&[0x40, 0x60, 0x71], 0x71, DOCS, DOCS / 5);
4971 eprintln!("tag 0x71 behind 0x40 and 0x60: {edges:.3} page accesses per insert");
4972 assert!(
4973 edges <= 1.20,
4974 "an ascending run behind two other keyspaces costs {edges:.3} page accesses per \
4975 insert; before the per-keyspace hint it was the tree's full height"
4976 );
4977 }
4978
4979 /// THE PLACEMENT PROPERTY. A key that does NOT fall inside the hinted
4980 /// leaf's fences must still land where a descent would put it.
4981 ///
4982 /// The tree is loaded with three ascending runs so every tag holds a live
4983 /// hint, then written scattered over the SAME tags -- every one of those
4984 /// keys finds a hint armed and almost none of them belongs to its leaf. A
4985 /// `BTreeMap` is the oracle: the full scan must equal it exactly, and every
4986 /// key must also be reachable by `get`, which is the half a wrong fence
4987 /// breaks first. A record appended past its separator stays visible to the
4988 /// scan and disappears from the descent.
4989 #[test]
4990 fn a_key_outside_the_hinted_leaf_still_lands_where_a_descent_would_put_it() {
4991 const ASCENDING: u64 = 1_500;
4992 const SCATTERED: u64 = 1_500;
4993 let (pool, _d) = scratch_pool(16);
4994 let h = Hinted::new();
4995 let mut t = h.tree(&pool);
4996 let mut model: std::collections::BTreeMap<Vec<u8>, Vec<u8>> = Default::default();
4997 let value = |tag: u8, i: u64| {
4998 let mut v = tagged_value(tag, i);
4999 v.extend_from_slice(format!("|{tag:#04x}/{i}").as_bytes());
5000 v
5001 };
5002 let tags = [0x20u8, 0x40, 0x60, 0x71, 0x72];
5003 for i in 0..ASCENDING {
5004 for tag in tags {
5005 let (k, v) = (tagged(tag, i), value(tag, i));
5006 t.insert(&k, &v).unwrap();
5007 model.insert(k, v);
5008 }
5009 }
5010 // Scattered, but INSIDE the range the ascending runs already cover, so
5011 // the hinted leaf is a plausible-looking neighbour rather than an
5012 // obvious miss -- and every third key is deliberately aimed just above
5013 // whatever leaf the hint currently names.
5014 for j in 0..SCATTERED {
5015 let i = j.wrapping_mul(0x9E37_79B9_7F4A_7C15) % ASCENDING;
5016 for tag in tags {
5017 let (k, v) = (tagged(tag, i), value(tag, i + 1));
5018 t.insert(&k, &v).unwrap();
5019 model.insert(k, v);
5020 }
5021 }
5022 let seen: Vec<(Vec<u8>, Vec<u8>)> = t.range(&[]).unwrap().map(|r| r.unwrap()).collect();
5023 let expect: Vec<(Vec<u8>, Vec<u8>)> = model.into_iter().collect();
5024 assert_eq!(seen.len(), expect.len(), "the scan returned a different number of keys");
5025 assert_eq!(seen, expect, "the scan is not every key exactly once in byte order");
5026 for (k, v) in &expect {
5027 assert_eq!(
5028 t.get(k).unwrap().as_deref(), Some(&v[..]),
5029 "a key the scan can see is not reachable by a descent: {k:?}"
5030 );
5031 }
5032 }
5033
5034 /// THE SHAPE PROPERTY. Splitting under the hint must leave an ordinary
5035 /// tree: every leaf reachable through its parents, every leaf non-empty,
5036 /// keys strictly ascending across the whole walk, and the sibling chain
5037 /// agreeing with the parent walk. `insert_tag_fast` never splits -- it is
5038 /// the fallback descent that does -- so this is the guard that the hint
5039 /// does not quietly push records onto a page the split policy then
5040 /// mis-cuts.
5041 #[test]
5042 fn splitting_under_the_per_keyspace_hint_leaves_an_ordinary_tree() {
5043 const DOCS: u64 = 4_000;
5044 let (pool, _d) = scratch_pool(64);
5045 let h = Hinted::new();
5046 let mut t = h.tree(&pool);
5047 for i in 0..DOCS {
5048 for tag in [0x40u8, 0x60, 0x71] {
5049 t.insert(&tagged(tag, i), &tagged_value(tag, i)).unwrap();
5050 }
5051 }
5052 let mut all = Vec::new();
5053 leaves(&pool, t.root, &mut all);
5054 assert!(all.len() > 8, "the fixture did not split enough to test anything");
5055
5056 // The parent walk and the sibling chain must describe the same tree.
5057 let scanned: Vec<Vec<u8>> = t.range(&[]).unwrap().map(|r| r.unwrap().0).collect();
5058 assert_eq!(scanned.len() as u64, DOCS * 3, "the walk lost or duplicated keys");
5059 for w in scanned.windows(2) {
5060 assert!(w[0] < w[1], "the walk is not strictly ascending at {:?}", w[0]);
5061 }
5062 for k in &scanned {
5063 assert!(t.get(k).unwrap().is_some(), "a walked key is not reachable by a descent");
5064 }
5065 }
5066
5067 /// Every leaf of the tree, through its parents, as
5068 /// (page, first key, last key, `next_leaf`, free space).
5069 fn leaf_details(pool: &BufferPool, page_no: u32,
5070 out: &mut Vec<(u32, Vec<u8>, Vec<u8>, u32, usize)>) {
5071 let kids = {
5072 let r = pool.get(page_no).unwrap();
5073 let p = PageRef::open_resident(&r, page_no).unwrap();
5074 match p.kind() {
5075 PageKind::Leaf => {
5076 if p.nentries() > 0 {
5077 out.push((page_no,
5078 validated_key(p.slot(0)).to_vec(),
5079 validated_key(p.slot(p.nentries() - 1)).to_vec(),
5080 p.next_leaf(), p.free_space()));
5081 }
5082 return;
5083 }
5084 PageKind::Interior => {
5085 let mut kids = vec![p.child0()];
5086 kids.extend((0..p.nentries()).map(|i| validated_child(p.slot(i))));
5087 kids
5088 }
5089 other => panic!("unexpected page kind in the tree: {other:?}"),
5090 }
5091 };
5092 for k in kids { leaf_details(pool, k, out); }
5093 }
5094
5095 /// THE FENCE IS EXCLUSIVE AT THE TOP. A key EQUAL to the separator above
5096 /// the hinted leaf belongs to the NEXT leaf, and the hint must refuse it.
5097 ///
5098 /// This is the one boundary a cached fence can get wrong in the silent
5099 /// direction. `upper_bound` routes a key equal to a separator to the child
5100 /// on its RIGHT, so a separator key is always the first key of the leaf to
5101 /// the right -- it already exists there. Accepting it on the hinted leaf
5102 /// appends a SECOND copy: the scan returns the key twice, and `get` keeps
5103 /// answering with the old value forever. Relaxing the comparison in
5104 /// `TagHints::find` from `key < upper` to `key <= upper` fails this test
5105 /// with "the key is in the tree twice".
5106 ///
5107 /// The hint is forced onto a chosen leaf the way the stale-hint tests above
5108 /// force `last_leaf`: a real workload reaches this state rarely, and a test
5109 /// that waits for it to happen by chance is testing nothing.
5110 #[test]
5111 fn a_key_equal_to_the_fence_belongs_to_the_next_leaf() {
5112 const DOCS: u64 = 3_000;
5113 let (pool, _d) = scratch_pool(64);
5114 let h = Hinted::new();
5115 let mut t = h.tree(&pool);
5116 // The 0x71 run is written SCATTERED here on purpose: an append split
5117 // closes each left page nearly full, and this test needs a leaf with
5118 // room in it. The hint is armed by hand below, so the write order of
5119 // the fixture decides nothing else.
5120 for i in 0..DOCS {
5121 let scattered = i.wrapping_mul(0x9E37_79B9_7F4A_7C15) % DOCS;
5122 t.insert(&tagged(0x40, i), &tagged_value(0x40, i)).unwrap();
5123 t.insert(&tagged(0x71, scattered), &tagged_value(0x71, scattered)).unwrap();
5124 }
5125 let mut all = Vec::new();
5126 leaf_details(&pool, t.root, &mut all);
5127
5128 // A 0x71 leaf with room, whose right neighbour is also a 0x71 leaf.
5129 let by_page: std::collections::HashMap<u32, usize> =
5130 all.iter().enumerate().map(|(i, l)| (l.0, i)).collect();
5131 let chosen = all.iter().find(|l| {
5132 l.1[0] == 0x71 && l.2[0] == 0x71 && l.4 >= 64
5133 && by_page.get(&l.3).is_some_and(|&j| all[j].1[0] == 0x71)
5134 }).expect("the fixture has no 0x71 leaf with room beside another");
5135 let right = &all[by_page[&chosen.3]];
5136 let separator = right.1.clone();
5137 assert!(separator > chosen.2, "the fixture's leaves are not in order");
5138
5139 // Arm the hint exactly as the arming descent would have, then hand it
5140 // the separator key.
5141 let mut fences = LeafFences::NONE;
5142 fences.narrow_lower(&chosen.1);
5143 fences.narrow_upper(&separator);
5144 assert!(fences.armable, "the fixture's separators must fit the inline fence");
5145 h.tags.arm(1, &chosen.2, chosen.0, chosen.3, fences);
5146 t.insert(&separator, b"replaced").unwrap();
5147
5148 let seen: Vec<Vec<u8>> = t.range(&[]).unwrap().map(|r| r.unwrap().0).collect();
5149 assert_eq!(
5150 seen.iter().filter(|k| **k == separator).count(), 1,
5151 "the key is in the tree twice: the hint appended it below its own fence"
5152 );
5153 for w in seen.windows(2) {
5154 assert!(w[0] < w[1], "the scan is not strictly ascending at {:?}", w[0]);
5155 }
5156 assert_eq!(
5157 t.get(&separator).unwrap().as_deref(), Some(&b"replaced"[..]),
5158 "the descent still answers with the old value, so the new record went elsewhere"
5159 );
5160 }
5161
5162 /// THE INVALIDATION PROPERTY. A delete can merge leaves, collapse the root
5163 /// and hand a freed page straight back to the allocator, so the fences a
5164 /// hint cached describe a shape that no longer exists. Writing the same
5165 /// ascending run again afterwards must place every key correctly.
5166 #[test]
5167 fn a_delete_forgets_the_fences_it_invalidated() {
5168 const DOCS: u64 = 1_200;
5169 let (pool, _d) = scratch_pool(16);
5170 let h = Hinted::new();
5171 let mut t = h.tree(&pool);
5172 let mut model: std::collections::BTreeMap<Vec<u8>, Vec<u8>> = Default::default();
5173 for i in 0..DOCS {
5174 for tag in [0x40u8, 0x71] {
5175 let (k, v) = (tagged(tag, i), tagged_value(tag, i));
5176 t.insert(&k, &v).unwrap();
5177 model.insert(k, v);
5178 }
5179 }
5180 // Empty most of the forward-edge run, which is where the hint sits.
5181 for i in 0..DOCS * 3 / 4 {
5182 let k = tagged(0x71, i);
5183 assert!(t.delete(&k).unwrap());
5184 model.remove(&k);
5185 }
5186 // And write it straight back, ascending, through whatever the delete
5187 // left behind.
5188 for i in 0..DOCS * 3 / 4 {
5189 let (k, v) = (tagged(0x71, i), tagged_value(0x71, i + 7));
5190 t.insert(&k, &v).unwrap();
5191 model.insert(k, v);
5192 }
5193 let seen: Vec<(Vec<u8>, Vec<u8>)> = t.range(&[]).unwrap().map(|r| r.unwrap()).collect();
5194 assert_eq!(seen, model.into_iter().collect::<Vec<_>>(),
5195 "the tree written after a delete is not the tree the model describes");
5196 for (k, v) in &seen {
5197 assert_eq!(t.get(k).unwrap().as_deref(), Some(&v[..]),
5198 "a key is in the scan but not reachable by a descent: {k:?}");
5199 }
5200 }
5201
5202 // ------------------------------- K1 byte-equivalence: placement is unchanged
5203 //
5204 // Phase 1's rule for a speed change in the split/append path is that the
5205 // page IMAGES do not move (`split_byte_equivalence` below). The same rule
5206 // applies here, and more sharply: this cache decides which leaf a write
5207 // reaches WITHOUT descending, so if it ever reaches a different one than
5208 // the descent would, the file says so.
5209 //
5210 // The claim under test: the per-keyspace hint changes where a write
5211 // descends FROM, never where it lands. Run one deterministic multimodel
5212 // workload twice into two stores -- once with the cache, once with it
5213 // switched off -- and the two data files must be equal byte for byte,
5214 // every page image, after the final checkpoint.
5215
5216 use crate::store::{Config, Store, SyncMode};
5217 use crate::io::IoMode;
5218
5219 fn cfg() -> Config {
5220 Config { budget_bytes: 32 << 20, io: IoMode::Buffered, sync: SyncMode::Off }
5221 }
5222
5223 /// `[tag][collection][sequence]` -- a data row, `src/collections/mod.rs`'s shape.
5224 fn row_key(i: u64) -> Vec<u8> {
5225 let mut k = vec![0x40u8, 1];
5226 k.extend_from_slice(&i.to_be_bytes());
5227 k
5228 }
5229 /// `[tag][collection][sequence][field]` -- an embedding row.
5230 fn vec_key(i: u64) -> Vec<u8> {
5231 let mut k = vec![0x60u8, 1];
5232 k.extend_from_slice(&i.to_be_bytes());
5233 k.push(0);
5234 k
5235 }
5236 /// `[tag][first collection][first seq][ctx][type][last collection][last seq]`
5237 /// -- `src/index/graph/mod.rs`'s edge key, narrowed to one byte per
5238 /// small identity. 0x71 is the forward row (first = source), 0x72 the
5239 /// reverse (first = destination). Organizations live in collection 2, so
5240 /// their rows sort ABOVE every person's, exactly as in the bench.
5241 fn edge_key(tag: u8, a: (u8, u64), ty: u8, b: (u8, u64)) -> Vec<u8> {
5242 let mut k = vec![tag, a.0];
5243 k.extend_from_slice(&a.1.to_be_bytes());
5244 k.extend_from_slice(&[0, ty, b.0]);
5245 k.extend_from_slice(&b.1.to_be_bytes());
5246 k
5247 }
5248
5249 /// THE WORKLOAD. Deterministic, no randomness, no timing: per document it
5250 /// writes a row, an embedding, three forward-edge rows and their three
5251 /// reverse rows, and every so often deletes one of each. The destinations
5252 /// reproduce the relationship load's shape -- `(i+1)` and `(i+7)` make two
5253 /// near-ascending reverse runs six keys apart, and `i % 100` makes a
5254 /// hundred hot organizations whose reverse rows are a hundred separate
5255 /// cold runs. 5,000 documents is 40,000 puts, far past the volume at which
5256 /// every tag's run splits and redistributes across the keyspace boundary
5257 /// it shares with the tag above it.
5258 fn multimodel_workload(s: &mut Store, docs: u64) {
5259 for i in 0..docs {
5260 s.put(&row_key(i), &vec![b'd'; 200 + (i % 97) as usize * 2]).unwrap();
5261 s.put(&vec_key(i), &vec![b'f'; 32]).unwrap();
5262 let people = [(1u8, (i + 1) % docs), (1, (i + 7) % docs)];
5263 for (n, dst) in people.iter().enumerate() {
5264 s.put(&edge_key(0x71, (1, i), 1, *dst), &[b'e', n as u8]).unwrap();
5265 s.put(&edge_key(0x72, *dst, 1, (1, i)), &[]).unwrap();
5266 }
5267 let org = (2u8, i % 100 + 1);
5268 s.put(&edge_key(0x71, (1, i), 2, org), b"m").unwrap();
5269 s.put(&edge_key(0x72, org, 2, (1, i)), &[]).unwrap();
5270
5271 // Interleaved deletes: a merge, a freed page and a collapsed root
5272 // are what a cached fence cannot survive, so the oracle has to
5273 // contain them or it is not testing the invalidation at all.
5274 if i % 37 == 36 && i > 40 {
5275 s.delete(&row_key(i - 20)).unwrap();
5276 s.delete(&edge_key(0x71, (1, i - 13), 1, (1, (i - 12) % docs))).unwrap();
5277 s.delete(&edge_key(0x72, (1, (i - 12) % docs), 1, (1, i - 13))).unwrap();
5278 }
5279 if i % 256 == 255 { s.commit().unwrap(); }
5280 if i % 1024 == 1023 { s.checkpoint().unwrap(); }
5281 }
5282 s.commit().unwrap();
5283 s.checkpoint().unwrap();
5284
5285 // THE UPDATE SWEEP, and it is here for the negative control. A key
5286 // EQUAL to a hinted leaf's upper fence is the one placement error a
5287 // cached fence can make silently, and it only ever arrives as a
5288 // REWRITE: the separator key already lives in the leaf to the right.
5289 // Rewriting every key in ascending order walks the cache onto each
5290 // leaf in turn and then hands it exactly that leaf's separator, at
5291 // every boundary in the tree. The values are shorter than the
5292 // originals so the leaf has the room a wrong placement would need --
5293 // a control that cannot fit its record proves nothing either.
5294 for i in 0..docs {
5295 s.put(&row_key(i), &vec![b'u'; 48]).unwrap();
5296 s.put(&vec_key(i), b"u").unwrap();
5297 s.put(&edge_key(0x71, (1, i), 1, (1, (i + 1) % docs)), b"U").unwrap();
5298 s.put(&edge_key(0x72, (1, (i + 1) % docs), 1, (1, i)), &[]).unwrap();
5299 if i % 256 == 255 { s.commit().unwrap(); }
5300 }
5301 s.commit().unwrap();
5302 s.checkpoint().unwrap();
5303 }
5304
5305 /// Run the workload into a fresh store and return its data file, page by
5306 /// page, plus the rows a full scan sees.
5307 fn workload_file(docs: u64, hints: bool, relaxed: bool)
5308 -> (Vec<Vec<u8>>, Vec<(Vec<u8>, Vec<u8>)>, u64) {
5309 let d = tempfile::tempdir().unwrap();
5310 let mut s = Store::create(&d.path().join("s"), cfg()).unwrap();
5311 s.tag_hints().set_enabled(hints);
5312 s.tag_hints().set_relaxed_upper_fence(relaxed);
5313 multimodel_workload(&mut s, docs);
5314 let mut rows = Vec::new();
5315 s.scan(&[]).unwrap()
5316 .for_each_ref(|k, v| { rows.push((k.to_vec(), v.to_vec())); true })
5317 .unwrap();
5318 let served = s.tag_hints().hits();
5319 drop(s);
5320 let bytes = std::fs::read(d.path().join("s").join("data")).unwrap();
5321 (bytes.chunks(PAGE_SIZE).map(<[u8]>::to_vec).collect(), rows, served)
5322 }
5323
5324 /// Report the first page and SLOT at which two files disagree, in the
5325 /// terms a placement bug is stated in: which leaf, which slot, which key,
5326 /// and how the two sides differ there.
5327 fn first_difference(a: &[Vec<u8>], b: &[Vec<u8>]) -> Option<String> {
5328 if a.len() != b.len() {
5329 return Some(format!("page COUNT differs: {} pages with the hint, {} without",
5330 a.len(), b.len()));
5331 }
5332 let records = |page: &[u8], no: u32| -> std::result::Result<Vec<Vec<u8>>, String> {
5333 let p = PageRef::open(page, no).map_err(|e| format!("unreadable: {e:?}"))?;
5334 Ok((0..p.nentries()).map(|i| p.slot(i).to_vec()).collect())
5335 };
5336 for (no, (pa, pb)) in a.iter().zip(b).enumerate() {
5337 if pa == pb { continue; }
5338 let no = no as u32;
5339 let (ra, rb) = match (records(pa, no), records(pb, no)) {
5340 (Ok(x), Ok(y)) => (x, y),
5341 (x, y) => return Some(format!("page {no}: {x:?} vs {y:?}")),
5342 };
5343 let header = pa[..crate::page::HEADER_LEN] != pb[..crate::page::HEADER_LEN];
5344 if ra == rb {
5345 return Some(format!(
5346 "page {no} holds the same {} records but differs in its bytes \
5347 (header differs: {header}) -- a difference that is NOT placement",
5348 ra.len()));
5349 }
5350 let slot = (0..ra.len().max(rb.len()))
5351 .find(|&i| ra.get(i) != rb.get(i)).unwrap_or(0);
5352 let show = |r: Option<&Vec<u8>>| match r {
5353 Some(rec) => format!("key {:02x?} ({} bytes)", validated_key(rec), rec.len()),
5354 None => "no record".into(),
5355 };
5356 return Some(format!(
5357 "page {no} (a {:?}) first differs at SLOT {slot}: {} records with the hint, \
5358 {} without\n with the hint: {}\n without: {}",
5359 PageRef::open(pa, no).map(|p| p.kind()).unwrap(),
5360 ra.len(), rb.len(), show(ra.get(slot)), show(rb.get(slot))));
5361 }
5362 None
5363 }
5364
5365 /// THE PROOF. Two stores, one workload, one difference: whether the
5366 /// per-keyspace cache was on. A hinted placement has to land in the same
5367 /// leaf AND the same slot a descent would have chosen, and a split that
5368 /// re-arms the cache afterwards must not change which half a record went
5369 /// to -- either would move a byte, and every byte is compared.
5370 #[test]
5371 fn the_hint_changes_where_a_write_descends_from_not_where_it_lands() {
5372 const DOCS: u64 = 5_000; // 40,000 puts
5373 let (hinted, hinted_rows, served) = workload_file(DOCS, true, false);
5374 let (plain, plain_rows, none) = workload_file(DOCS, false, false);
5375 eprintln!("{} pages with the hint and {} without; the cache served {served} writes \
5376 with it on and {none} with it off", hinted.len(), plain.len());
5377 // Neither arm may be vacuous: the control must really have the cache
5378 // off, and the candidate must really be using it.
5379 assert_eq!(none, 0, "the switch did not turn the cache off");
5380 assert!(served > DOCS * 4, "the cache served only {served} of this workload's writes");
5381 assert_eq!(hinted_rows, plain_rows, "the two stores do not hold the same rows");
5382 if let Some(why) = first_difference(&hinted, &plain) {
5383 panic!("the per-keyspace hint moved a record:\n{why}");
5384 }
5385
5386 // THE NEGATIVE CONTROL. Without this the assertion above could pass
5387 // because the workload never armed a hint at all. Relaxing the upper
5388 // fence from `key < separator` to `key <= separator` is the smallest
5389 // real placement error this cache can make: the separator key already
5390 // lives in the NEXT leaf, so the relaxed rule appends a second copy
5391 // here. It must make the files differ.
5392 let (relaxed, relaxed_rows, _) = workload_file(DOCS, true, true);
5393 let moved = first_difference(&hinted, &relaxed);
5394 assert!(
5395 moved.is_some() || relaxed_rows != hinted_rows,
5396 "relaxing the upper fence changed nothing, so this workload never \
5397 exercised the fence and the comparison above is vacuous"
5398 );
5399 eprintln!("negative control: {}", moved.unwrap_or_else(|| "rows differ".into()));
5400 }
5401
5402 /// The ordering oracle that rejected pair-packing and scattered-packing
5403 /// (docs/PAIR_PACKING.md, docs/SCATTERED_PACKING.md): a cheaper split policy
5404 /// is worth nothing if the tree stops answering exactly. Interleaved
5405 /// ascending runs FIRST, then a scattered phase over the same keyspaces, so
5406 /// the append shortcut and the balanced split both run against one tree.
5407 #[test]
5408 fn interleaved_then_scattered_keeps_every_key_exactly_once_in_order() {
5409 const DOCS: u64 = 2_000;
5410 const SCATTER: u64 = 2_000;
5411 let (pool, _d) = scratch_pool(16);
5412 let last_leaf = Cell::new(None);
5413 let hits = Cell::new(0);
5414 let attempts = Cell::new(0);
5415 let mut t = BTree::create(&pool, 1, &last_leaf, &hits, &attempts).unwrap();
5416
5417 let mut expected: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
5418 let value = |tag: u8, i: u64| {
5419 let mut v = tagged_value(tag, i);
5420 v.extend_from_slice(format!("|{tag:#04x}/{i}").as_bytes());
5421 v
5422 };
5423 for i in 0..DOCS {
5424 for tag in [0x60u8, 0x40, 0x20] {
5425 let (k, v) = (tagged(tag, i), value(tag, i));
5426 t.insert(&k, &v).unwrap();
5427 expected.push((k, v));
5428 }
5429 }
5430 // A multiplicative hash scatters insert order without needing rand.
5431 for j in 0..SCATTER {
5432 let i = DOCS + j.wrapping_mul(0x9E37_79B9_7F4A_7C15) % 1_000_000;
5433 for tag in [0x20u8, 0x60, 0x40] {
5434 let (k, v) = (tagged(tag, i), value(tag, i));
5435 t.insert(&k, &v).unwrap();
5436 expected.push((k, v));
5437 }
5438 }
5439 expected.sort();
5440 expected.dedup_by(|a, b| a.0 == b.0);
5441
5442 let seen: Vec<(Vec<u8>, Vec<u8>)> = t.range(&[]).unwrap().map(|r| r.unwrap()).collect();
5443 assert_eq!(seen.len(), expected.len(), "scan returned a different number of keys");
5444 assert_eq!(seen, expected, "full scan is not every key exactly once in byte order");
5445 for w in seen.windows(2) {
5446 assert!(w[0].0 < w[1].0, "scan is not strictly ascending at {:?}", w[0].0);
5447 }
5448 for (k, v) in &expected {
5449 assert_eq!(t.get(k).unwrap().as_deref(), Some(&v[..]), "point get disagrees for {k:?}");
5450 }
5451 }
5452}
5453
5454/// L2.3-2 -- the byte-equivalence oracle for the allocation-free split.
5455///
5456/// The candidate changes only HOW a split moves bytes, never WHICH bytes it
5457/// writes: same `split_point`, same `neighbor_cell_cuts`, same `at_point`
5458/// decision, same page images, same separators. That is a claim about output,
5459/// so it is tested as one: every corpus case is run through
5460/// `split_leaf_and_insert_vec` (the record-per-`Vec` implementation) and
5461/// through `split_leaf_and_insert_ref` (the `SlotRef` one) in two independent
5462/// stores, and EVERY page of the two files is compared byte for byte -- which
5463/// covers separator keys too, since a separator is bytes on a parent page.
5464///
5465/// Both implementations are compiled under `cfg(test)` precisely so this can
5466/// run in ONE build; in a shipping build the `slotref-split` feature selects
5467/// one of them and the other is not compiled at all.
5468#[cfg(test)]
5469mod split_byte_equivalence {
5470 use super::*;
5471 use crate::test_support::scratch_pool;
5472
5473 #[derive(Clone, Copy, PartialEq, Debug)]
5474 enum Impl { Vec, Ref }
5475
5476 struct Case {
5477 name: &'static str,
5478 prefix: Vec<(Vec<u8>, Vec<u8>)>,
5479 last: (Vec<u8>, Vec<u8>),
5480 }
5481
5482 /// `src/collections/mod.rs`'s width-tagged big-endian integer component --
5483 /// the shape that produces `compact-cells` 0xff cells.
5484 fn ordered(n: u64) -> Vec<u8> {
5485 let b = n.to_be_bytes();
5486 let start = b.iter().position(|x| *x != 0).unwrap_or(7);
5487 let mut k = vec![0x80 + (8 - start) as u8];
5488 k.extend_from_slice(&b[start..]);
5489 k
5490 }
5491
5492 fn tagged(tag: u8, i: u64) -> Vec<u8> {
5493 let mut k = vec![tag];
5494 k.extend(ordered(1));
5495 match tag {
5496 0x20 => k.extend_from_slice(format!("key-{i:012}").as_bytes()),
5497 0x40 => k.extend(ordered(i + 1)),
5498 _ => { k.extend(ordered(i + 1)); k.extend(ordered(0)); }
5499 }
5500 k
5501 }
5502
5503 fn tagged_value(tag: u8, i: u64) -> Vec<u8> {
5504 match tag {
5505 0x20 => ordered(i + 1),
5506 0x40 => vec![b'd'; 200 + (i % 97) as usize * 2],
5507 _ => vec![b'f'; 32],
5508 }
5509 }
5510
5511 fn scatter(i: u64) -> [u8; 8] { i.wrapping_mul(0x9E37_79B9_7F4A_7C15).to_be_bytes() }
5512
5513 /// Load the prefix, then force the LAST record through the split path --
5514 /// whichever implementation is named. Returns every page of the resulting
5515 /// file, the root, and the full key/value sequence a scan sees.
5516 fn run(case: &Case, which: Impl) -> (Vec<Vec<u8>>, u32, Vec<(Vec<u8>, Vec<u8>)>, u32) {
5517 let (pool, _d) = scratch_pool(192);
5518 let last_leaf = Cell::new(None);
5519 let hits = Cell::new(0);
5520 let tries = Cell::new(0);
5521 let mut t = BTree::create(&pool, 1, &last_leaf, &hits, &tries).unwrap();
5522 for (k, v) in &case.prefix { t.insert(k, v).unwrap(); }
5523
5524 let (k, v) = &case.last;
5525 let before = pool.page_count();
5526 let rec = enc_leaf(k, v, pool.compact_cells());
5527 let (w, path) = t.descend_for_write(k).unwrap();
5528 let leaf = w.page_no();
5529 let at = {
5530 let p = PageRef::open_resident(w.bytes(), leaf).unwrap();
5531 validate_records(&p).unwrap();
5532 lower_bound(&p, k).unwrap()
5533 };
5534 match which {
5535 Impl::Vec => t.split_leaf_and_insert_vec(w, path, at, rec, k, None).unwrap(),
5536 Impl::Ref => t.split_leaf_and_insert_ref(w, path, at, rec, k, None).unwrap(),
5537 }
5538
5539 let root = t.root();
5540 let mut rows = Vec::new();
5541 t.range(&[]).unwrap()
5542 .for_each_ref(|k, v| { rows.push((k.to_vec(), v.to_vec())); true })
5543 .unwrap();
5544 let pages: Vec<Vec<u8>> = (0..pool.page_count())
5545 .map(|n| pool.get(n).unwrap().to_vec())
5546 .collect();
5547 (pages, root, rows, before)
5548 }
5549
5550 /// The corpus. Each entry names one shape the split path has to handle:
5551 /// which branch it takes, what its records look like, and where in the
5552 /// tree the splitting leaf sits.
5553 fn corpus() -> Vec<Case> {
5554 let mut cases = Vec::new();
5555
5556 // 1. Ascending into the TREE's rightmost leaf: D9's append split
5557 // (at_point), left page closed full, right page holds one record.
5558 cases.push(Case {
5559 name: "ascending append at the tree's rightmost leaf",
5560 prefix: (0..64u64).map(|i| (tagged(0x40, i), tagged_value(0x40, i))).collect(),
5561 last: (tagged(0x40, 64), tagged_value(0x40, 64)),
5562 });
5563
5564 // 2. Three keyspaces in one tree, written one document at a time --
5565 // `src/collections/mod.rs`'s exact shape. The 0x40 run is ascending
5566 // but never at the tree's rightmost leaf, so this is the case
5567 // `keyspace-append` widens and `redistribute_neighbors` owned
5568 // before it.
5569 let mut prefix = Vec::new();
5570 for i in 0..90u64 {
5571 for tag in [0x60u8, 0x40, 0x20] { prefix.push((tagged(tag, i), tagged_value(tag, i))); }
5572 }
5573 cases.push(Case {
5574 name: "ascending row run under a higher keyspace",
5575 prefix,
5576 last: (tagged(0x40, 90), tagged_value(0x40, 90)),
5577 });
5578
5579 // 3. Scattered keys: the balanced `split_point` cut, and the
5580 // neighbour redistribution that fires when siblings have room.
5581 cases.push(Case {
5582 name: "scattered keys, balanced cut",
5583 prefix: (0..400u64).map(|i| (scatter(i).to_vec(), vec![b'x'; 90])).collect(),
5584 last: (scatter(400).to_vec(), vec![b'x'; 90]),
5585 });
5586
5587 // 4. The new record lands FIRST in its leaf (at == 0).
5588 let mut prefix: Vec<(Vec<u8>, Vec<u8>)> =
5589 (10..70u64).map(|i| ((i * 4).to_be_bytes().to_vec(), vec![b'a'; 120])).collect();
5590 prefix.push((0u64.to_be_bytes().to_vec(), vec![b'a'; 120]));
5591 cases.push(Case {
5592 name: "new record first in its leaf",
5593 prefix,
5594 last: (1u64.to_be_bytes().to_vec(), vec![b'b'; 120]),
5595 });
5596
5597 // 5. The boundary leaf: one leaf holding the top of tag 0x40 and the
5598 // bottom of tag 0x60, so an ascending 0x40 insert is NOT last in
5599 // the leaf and the append shortcut is never consulted.
5600 let mut prefix = Vec::new();
5601 for i in 0..40u64 { prefix.push((tagged(0x40, i), tagged_value(0x40, i))); }
5602 for i in 0..40u64 { prefix.push((tagged(0x60, i), tagged_value(0x60, i))); }
5603 cases.push(Case {
5604 name: "mixed-tag boundary leaf",
5605 prefix,
5606 last: (tagged(0x40, 40), tagged_value(0x40, 40)),
5607 });
5608
5609 // 6. One indivisible record too large to pair with its neighbours:
5610 // the three-way cut that allocates TWO pages and pushes TWO
5611 // separators. Sizes taken from the pinned huge-record test.
5612 let mut prefix: Vec<(Vec<u8>, Vec<u8>)> =
5613 (0..40u64).map(|i| ((i * 2).to_be_bytes().to_vec(), vec![b'a'; 60])).collect();
5614 prefix.push((41u64.to_be_bytes().to_vec(), vec![b'Z'; 2500]));
5615 cases.push(Case {
5616 name: "one huge record among small ones",
5617 prefix,
5618 last: (42u64.to_be_bytes().to_vec(), vec![b'Y'; 2400]),
5619 });
5620
5621 // 6b. The three-way cut actually taken. A ROOT leaf has no parent, so
5622 // the neighbour redistribution cannot absorb the insert, and the
5623 // sizes are chosen so that no single cut leaves both halves inside
5624 // a page: 44 records of 74 page-bytes each with a 2514-byte record
5625 // landing exactly in the middle. Left-of-big and right-of-big are
5626 // each 1628 bytes, so big + either side is 4142 > 4056.
5627 let mut prefix: Vec<(Vec<u8>, Vec<u8>)> =
5628 (0..44u64).map(|i| ((i * 2).to_be_bytes().to_vec(), vec![b'a'; 60])).collect();
5629 prefix.sort();
5630 cases.push(Case {
5631 name: "three-way cut on a root leaf",
5632 prefix,
5633 last: (43u64.to_be_bytes().to_vec(), vec![b'Z'; 2500]),
5634 });
5635
5636 // 7. Records close to the per-record limit: two or three to a leaf,
5637 // so the cut has almost no freedom and every byte of slack shows.
5638 cases.push(Case {
5639 name: "records near the size limit",
5640 prefix: (0..12u64).map(|i| (i.to_be_bytes().to_vec(), vec![b'L'; 1300])).collect(),
5641 last: (12u64.to_be_bytes().to_vec(), vec![b'L'; 1300]),
5642 });
5643
5644 // 8. Compact integer cells (the 0xff + width-tag encoding) with tiny
5645 // values: hundreds of very short records in one leaf.
5646 cases.push(Case {
5647 name: "compact integer cells, many per leaf",
5648 prefix: (0..600u64).map(|i| (ordered(i + 1), vec![b'v'; 4])).collect(),
5649 last: (ordered(601), vec![b'v'; 4]),
5650 });
5651
5652 // 9. A leaf that is its PARENT's last child: its right neighbour
5653 // lives under a different parent, which is the branch
5654 // `keyspace_rightmost` falls through to and the window
5655 // `redistribute_neighbors` has to clamp.
5656 let mut prefix: Vec<(Vec<u8>, Vec<u8>)> =
5657 (0..900u64).map(|i| (scatter(i).to_vec(), vec![b'p'; 40])).collect();
5658 prefix.extend((0..300u64).map(|i| (tagged(0x60, i), tagged_value(0x60, i))));
5659 cases.push(Case {
5660 name: "deep tree, last child of a parent",
5661 prefix,
5662 last: (scatter(901).to_vec(), vec![b'p'; 40]),
5663 });
5664
5665 cases
5666 }
5667
5668 #[test]
5669 fn both_split_implementations_write_identical_pages_for_every_corpus_state() {
5670 let mut reached = [false; 3];
5671 for case in corpus() {
5672 let (a, root_a, rows_a, before) = run(&case, Impl::Vec);
5673 let (b, root_b, rows_b, _) = run(&case, Impl::Ref);
5674 // Which branch a case reached shows in how many pages the split
5675 // added: 0 = the neighbour redistribution absorbed it, 1 = an
5676 // ordinary or append split into one new leaf, more = the
5677 // three-way cut (two leaves, and a new root when the leaf that
5678 // split WAS the root). A corpus that stops reaching one of the
5679 // three stops testing it, silently -- hence the tally below.
5680 let added = a.len() as u32 - before;
5681 println!("{}: {added} pages added", case.name);
5682 reached[(added as usize).min(2)] = true;
5683 assert_eq!(root_a, root_b, "{}: root page differs", case.name);
5684 assert_eq!(rows_a, rows_b, "{}: visible rows differ", case.name);
5685 assert_eq!(a.len(), b.len(), "{}: page count differs", case.name);
5686 for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
5687 assert!(x == y, "{}: page {i} differs byte for byte", case.name);
5688 }
5689 }
5690 // Redistribution does not exist in a build without sqlite-balance.
5691 // Byte identity and the remaining split coverage apply in every mode.
5692 assert_eq!(reached[0], cfg!(feature = "sqlite-balance"),
5693 "neighbour redistribution coverage must match the compiled policy");
5694 assert!(reached[1], "no corpus case reached the one-new-leaf split");
5695 assert!(reached[2], "no corpus case reached the three-way cut");
5696 }
5697
5698 /// The comparison above is worthless unless it can FAIL. Feed the same
5699 /// harness two states that genuinely differ -- one byte of one value --
5700 /// and require it to notice.
5701 #[test]
5702 fn the_oracle_notices_a_one_byte_difference() {
5703 let mut a = corpus().remove(2);
5704 let mut b = corpus().remove(2);
5705 b.last.1[0] = b'z';
5706 assert_ne!(a.last.1, b.last.1);
5707 let pages_a = run(&a, Impl::Vec).0;
5708 let pages_b = run(&b, Impl::Vec).0;
5709 assert_ne!(pages_a, pages_b, "the oracle cannot tell two different splits apart");
5710 // ... and identical input through the same implementation must be
5711 // identical, or the harness itself is not deterministic.
5712 a.name = "determinism";
5713 assert_eq!(run(&a, Impl::Vec).0, run(&a, Impl::Vec).0);
5714 }
5715}
5716
5717/// L2.3-2 -- how much the split path allocates, counted rather than argued.
5718///
5719/// The finding this candidate answers: on a Pi, a typed put spends ~46% of its
5720/// CPU in the split path, and a quarter of all cycles in malloc/free. A plain
5721/// leaf split copied every record on the page into its own `Vec<u8>`
5722/// (`p.slot(j).to_vec()`); a three-sibling redistribution did that for three
5723/// pages plus the parent's records. So the number to watch is not a timing, it
5724/// is an ALLOCATION COUNT, and it is measured here with the kernel's
5725/// `cfg(test)` counting allocator.
5726///
5727/// Both figures are printed, so the test doubles as the record of the before.
5728#[cfg(test)]
5729mod split_allocations {
5730 use super::*;
5731 use crate::test_alloc::measured;
5732 use crate::test_support::scratch_pool;
5733
5734 /// Allocations inside ONE steady-state split -- not the first one. The
5735 /// first split of a process warms allocator free lists and (with the
5736 /// feature on) creates the one reusable scratch; a steady-state split is
5737 /// what a load actually spends its time in.
5738 ///
5739 /// The split that gets measured is a REAL one: keys are inserted through
5740 /// the ordinary path until one of them genuinely does not fit its leaf,
5741 /// and only that record is handed to the split directly, so the leaf is as
5742 /// full as it would be in a running store rather than as full as the test
5743 /// happened to leave it.
5744 fn split_allocations(scattered: bool, slotref: bool) -> (usize, usize) {
5745 let (pool, _d) = scratch_pool(192);
5746 let last_leaf = Cell::new(None);
5747 let hits = Cell::new(0);
5748 let tries = Cell::new(0);
5749 let mut t = BTree::create(&pool, 1, &last_leaf, &hits, &tries).unwrap();
5750 let val = vec![b'v'; 120];
5751 let mut i = 0u64;
5752 let mut next = move || {
5753 i += 1;
5754 if scattered { i.wrapping_mul(0x9E37_79B9_7F4A_7C15).to_be_bytes().to_vec() }
5755 else { i.to_be_bytes().to_vec() }
5756 };
5757 for _ in 0..2000 { let k = next(); t.insert(&k, &val).unwrap(); }
5758
5759 /// Insert until a record does not fit its leaf; return that record's key.
5760 fn until_split(t: &mut BTree<'_>, next: &mut impl FnMut() -> Vec<u8>, val: &[u8]) -> Vec<u8> {
5761 loop {
5762 let k = next();
5763 let rec = enc_leaf(&k, val, t.pool.compact_cells());
5764 let (leaf, _) = t.descend(&k).unwrap();
5765 let room = {
5766 let r = t.pool.get(leaf).unwrap();
5767 let p = open_cached(&r, leaf).unwrap();
5768 p.free_space() >= rec.len() + 4
5769 };
5770 if !room { return k; }
5771 t.insert(&k, val).unwrap();
5772 }
5773 }
5774
5775 fn once<'p>(t: &mut BTree<'p>, k: &[u8], val: &[u8], slotref: bool, measure: bool) -> (usize, usize) {
5776 let rec = enc_leaf(k, val, t.pool.compact_cells());
5777 let (w, path) = t.descend_for_write(k).unwrap();
5778 let leaf = w.page_no();
5779 let at = {
5780 let p = PageRef::open_resident(w.bytes(), leaf).unwrap();
5781 validate_records(&p).unwrap();
5782 lower_bound(&p, k).unwrap()
5783 };
5784 let run = |t: &mut BTree<'p>| {
5785 if slotref { t.split_leaf_and_insert_ref(w, path, at, rec, k, None) }
5786 else { t.split_leaf_and_insert_vec(w, path, at, rec, k, None) }.unwrap()
5787 };
5788 if measure {
5789 let (_, count, bytes) = measured(|| run(t));
5790 (count, bytes)
5791 } else {
5792 run(t);
5793 (0, 0)
5794 }
5795 }
5796 // The first split warms; the second is the measurement.
5797 let warm = until_split(&mut t, &mut next, &val);
5798 once(&mut t, &warm, &val, slotref, false);
5799 let hot = until_split(&mut t, &mut next, &val);
5800 once(&mut t, &hot, &val, slotref, true)
5801 }
5802
5803 #[test]
5804 fn a_steady_state_split_allocates_a_bounded_constant() {
5805 for scattered in [false, true] {
5806 let (n_vec, b_vec) = split_allocations(scattered, false);
5807 let (n_ref, b_ref) = split_allocations(scattered, true);
5808 println!(
5809 "{} split: record-per-Vec {n_vec} allocations / {b_vec} bytes; \
5810 SlotRef {n_ref} allocations / {b_ref} bytes",
5811 if scattered { "scattered" } else { "ascending" }
5812 );
5813 // The before. A split of a leaf holding ~30 records of this size
5814 // allocates one Vec per record plus the page images; a
5815 // redistribution does it for a whole window.
5816 assert!(n_vec >= 20, "the record-per-Vec split should allocate per record, saw {n_vec}");
5817 // The after, and the residue is NAMED rather than waved at. A
5818 // redistribution allocates nothing at all (measured: 0). A split
5819 // that actually adds a page allocates exactly one thing: the
5820 // interior record `insert_separator` pushes into the parent
5821 // (`enc_interior`, key + 6 bytes). That belongs to the separator
5822 // promotion, not to moving the leaf's records, and it does not
5823 // grow with the number of records on the page -- which is the
5824 // property under test. `BufferPool::allocate` itself allocated
5825 // nothing here; if its page tables ever grow inside a measured
5826 // split this bound is where it will show.
5827 assert!(
5828 n_ref <= 1,
5829 "the SlotRef split should allocate at most the promoted separator, saw {n_ref}"
5830 );
5831 }
5832 }
5833}