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