noxu_tree/tree.rs
1//! B+tree implementation.
2//!
3//!
4//! Tree implements the B+tree. It provides search, insert, and delete
5//! operations on the tree structure. The tree uses latch-coupling for
6//! concurrent access: when traversing down the tree, the parent latch
7//! is released after the child latch is acquired.
8//!
9//! # Architecture
10//!
11//! The tree has a hierarchical structure:
12//! - Internal Nodes (IN) at levels 2 and above
13//! - Bottom Internal Nodes (BIN) at level 1
14//! - Leaf Nodes (LN) containing actual data
15//!
16//! # Locking Strategy
17//!
18//! - Root latch protects the root pointer itself
19//! - Each node has its own latch for concurrent access
20//! - Search uses latch-coupling: acquire child, release parent
21//! - Modifications may require exclusive latches
22
23use crate::error::TreeError;
24use crate::key::{create_key_prefix, get_key_prefix_length};
25use crate::search_result::SearchResult;
26use noxu_latch::{LatchContext, SharedLatch};
27use noxu_util::{Lsn, NULL_LSN};
28// DST: the tree-node latch. Production (default cfg) is BYTE-IDENTICAL — the
29// literal `parking_lot::RwLock`, zero cost, with `parking_lot` in the dep graph
30// exactly as before. Under `--cfg noxu_shuttle` (dev/test only) it resolves to
31// the parking_lot-shaped shuttle wrapper `noxu_util::dst_sync_pl::RwLock`, so
32// shuttle can schedule the insert / split_child / compress interleavings that
33// let the BIN-split check-then-act race (bug-bin-split-concurrency.md) escape
34// into a benchmark instead of DST. The hand-over-hand *read* descent
35// (`root.read_arc()` → an Arc-owning read guard) is provided under the cfg by
36// `noxu_latch::dst_arc_guard` (a shuttle-only shim that noxu-tree cannot host
37// itself because it is `#![forbid(unsafe_code)]`). Under the default cfg
38// `read_arc()`/`ArcRwLockReadGuard` are parking_lot's own zero-cost inherent
39// API — no shim in the graph.
40#[cfg(noxu_shuttle)]
41use noxu_util::dst_sync_pl::RwLock;
42#[cfg(not(noxu_shuttle))]
43use parking_lot::RwLock;
44
45// The Arc-owning read guard for the hand-over-hand descent. Default =
46// parking_lot's own inherent type (zero-cost, byte-identical). Under shuttle =
47// the noxu-latch DST shim (see its module docs). The `.read_arc()` method is
48// available on `Arc<RwLock<TreeNode>>` in both: inherent on parking_lot,
49// via `noxu_latch::dst_arc_guard::ReadArc` under shuttle.
50#[cfg(not(noxu_shuttle))]
51type NodeArcReadGuard =
52 parking_lot::ArcRwLockReadGuard<parking_lot::RawRwLock, TreeNode>;
53#[cfg(noxu_shuttle)]
54type NodeArcReadGuard = noxu_latch::dst_arc_guard::ArcRwLockReadGuard<TreeNode>;
55#[cfg(noxu_shuttle)]
56use noxu_latch::dst_arc_guard::ReadArc as _;
57use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
58use std::sync::{Arc, Weak};
59
60/// Observer that mirrors JE's `INList` feeding the evictor's `LRUList`s.
61///
62/// The tree owns no eviction policy of its own; instead it notifies a
63/// registered listener whenever an IN/BIN node enters the resident cache, is
64/// accessed, or is removed. The `Evictor` (in `noxu-evictor`) implements this
65/// trait, but the dependency is one-way (`noxu-evictor` → `noxu-tree`), so the
66/// tree refers to the listener only through this trait object — avoiding a
67/// circular crate dependency.
68///
69/// JE reference: `IN.fetchTarget` / split / `rebuildINList` call
70/// `Evictor.addBack`; node access calls `Evictor.moveBack`; node removal
71/// calls `Evictor.remove`.
72pub trait InListListener: Send + Sync {
73 /// A node has just become resident in the cache (JE `Evictor.addBack`).
74 fn note_ins_added(&self, node_id: u64);
75 /// A resident node was accessed (JE `Evictor.moveBack` — LRU touch).
76 fn note_ins_accessed(&self, node_id: u64);
77 /// A node was removed from the cache (JE `Evictor.remove`).
78 fn note_ins_removed(&self, node_id: u64);
79}
80
81// Level and flag constants re-exported here for tree-internal use.
82pub const DBMAP_LEVEL: i32 = 0x20000;
83pub const MAIN_LEVEL: i32 = 0x10000;
84pub const LEVEL_MASK: i32 = 0x0ffff;
85pub const MIN_LEVEL: i32 = -1;
86pub const BIN_LEVEL: i32 = MAIN_LEVEL | 1;
87pub const EXACT_MATCH: i32 = 1 << 16;
88pub const INSERT_SUCCESS: i32 = 1 << 17;
89
90/// Per-slot fixed memory overhead for a BIN entry, in bytes (DBI-23).
91///
92/// This is the heap footprint of one `BinEntry` *struct* as it lives inside
93/// the BIN's `Vec<BinEntry>` buffer — NOT counting the variable-length key and
94/// data bytes, which are separate heap allocations counted on top of this.
95///
96/// Faithful to JE `IN.getEntryInMemorySize` + the per-slot `entryStates` /
97/// LSN-array overhead folded into `IN.computeMemorySize` (IN.java ~4632):
98/// JE measures the slot's fixed cost with `Sizeof` on the JVM; Rust has a
99/// fixed struct layout so `size_of::<BinEntry>()` is exact.
100///
101/// T-2/T-3: the per-slot `key` (`Vec<u8>` header) and `lsn` (`u64`) were
102/// hoisted out of `BinEntry` into the node-level `KeyRep`/`LsnRep`. The
103/// `size_of::<BinEntry>()` therefore shrank; we add back the packed per-slot
104/// LSN-rep cost (`LsnRep::BYTES_PER_LSN_ENTRY`, 4 bytes) so the incremental
105/// live counter still approximates the walked heap (the key bytes are charged
106/// separately as `key.len()` at the call site, matching the compact key rep).
107///
108/// Derived (not hard-coded) so a layout change to `BinEntry` is tracked
109/// automatically — see `bin_stub_conformance` for the drift guard.
110pub const BIN_ENTRY_OVERHEAD: usize =
111 std::mem::size_of::<BinEntry>() + LsnRep::BYTES_PER_LSN_ENTRY;
112
113/// Per-slot fixed memory overhead for an IN entry, in bytes (DBI-23).
114///
115/// Heap footprint of one `InEntry` struct inside the IN's `Vec<InEntry>`
116/// buffer (key bytes counted separately). JE `IN.getEntryInMemorySize` for
117/// an upper IN plus the per-slot state/LSN/target overhead from
118/// `IN.computeMemorySize`.
119pub const IN_ENTRY_OVERHEAD: usize = std::mem::size_of::<InEntry>();
120
121/// Type alias for the key comparator used by sorted-duplicate databases.
122///
123/// The comparator takes two full (uncompressed) keys and returns their
124/// relative ordering. For sorted-dup databases this is `DupKeyData::compare`,
125/// which splits each key into primary + data parts and applies separate
126/// comparators to each. For normal databases this field is `None` and
127/// lexicographic byte comparison is used.
128///
129/// `DatabaseImpl.btreeComparator` / `DatabaseImpl.dupComparator`.
130pub type KeyComparatorFn =
131 Arc<dyn Fn(&[u8], &[u8]) -> std::cmp::Ordering + Send + Sync>;
132
133/// Combined search result carrying slot data and the BIN arc, returned by
134/// [`Tree::search_with_data`].
135///
136/// Avoids the double-descent pattern where `Tree::search` checked key
137/// existence and a second call re-descended to fetch the actual slot bytes.
138/// One descent now serves both purposes (Wave-11-I optimisation).
139pub struct SlotFetch {
140 /// `true` if an exact key match was found and is not expired.
141 pub found: bool,
142 /// Data bytes for the slot (`None` when `found` is `false`).
143 pub data: Option<Vec<u8>>,
144 /// Raw slot LSN as `u64`; zero when `found` is `false`.
145 pub lsn: u64,
146 /// Slot index within the BIN. Set to the actual BIN slot index when
147 /// `found` is `true`; `0` otherwise.
148 ///
149 /// Used by `CursorImpl` to set `current_index` correctly so that
150 /// `retrieve_next` advances to the right slot after a search.
151 pub slot_index: usize,
152 /// Arc to the BIN that the descent reached. Always `Some` when the
153 /// tree has at least one node, regardless of whether `found` is `true`.
154 pub bin_arc: Arc<RwLock<TreeNode>>,
155}
156
157/// The B+tree.
158///
159///
160///
161/// This is the main tree structure that manages the B+tree nodes and
162/// provides operations for search, insert, delete, and tree maintenance.
163pub struct Tree {
164 /// Database ID this tree belongs to.
165 database_id: u64,
166
167 /// Maximum entries per node (from config).
168 max_entries_per_node: usize,
169
170 /// Root of the tree. None if tree is empty.
171 ///
172 /// Wrapped in `RwLock` so that `insert`, `delete`, and other mutating
173 /// operations can take `&self` (interior mutability), enabling concurrent
174 /// access to different BIN nodes without requiring a global `&mut Tree`
175 /// borrow. The root pointer itself is only written during root splits
176 /// and initial creation; all other access is read-only.
177 ///
178 /// `Tree.root` protected by the root latch.
179 root: RwLock<Option<Arc<RwLock<TreeNode>>>>,
180
181 /// Latch protecting the root reference itself.
182 /// Must be held when changing the root pointer.
183 root_latch: SharedLatch,
184
185 /// LSN at which the current root IN/BIN was last logged.
186 ///
187 /// Used by the IN-redo currency check (`recover_root_bin` /
188 /// `recover_root_upper_in`) to decide whether a logged root replaces the
189 /// in-memory one. Updated whenever a new root is installed via
190 /// `set_root_with_lsn` or the IN-redo recover-root path.
191 ///
192 /// JE `RootUpdater.originalLsn` / `ChildReference.getLsn()` for the root.
193 root_log_lsn: RwLock<noxu_util::Lsn>,
194
195 /// Statistics: number of times the root has been split.
196 root_splits: AtomicU64,
197
198 /// Statistics: number of latch upgrades from shared to exclusive.
199 relatches_required: AtomicU64,
200
201 /// Optional custom key comparator for sorted-duplicate databases.
202 ///
203 /// When `Some`, all key comparisons in tree traversal (upper IN routing
204 /// and BIN entry search/insert/delete) use this comparator instead of
205 /// lexicographic byte comparison.
206 ///
207 /// / `dupComparator` stored on the
208 /// database and consulted at every `IN.findEntry()` call.
209 pub key_comparator: Option<KeyComparatorFn>,
210
211 /// Shared memory counter for the evictor / MemoryBudget.
212 ///
213 /// Updated on every BIN entry insert (+key+data+overhead) and delete
214 /// (-key+overhead) so the evictor sees real cache pressure.
215 ///
216 /// `env.getMemoryBudget().updateTreeMemoryUsage(delta)` call
217 /// in the equivalent `IN.updateMemorySize()`. In Noxu the counter is an
218 /// `Arc<AtomicI64>` shared with the `Arbiter` (and later `MemoryBudget`)
219 /// to avoid a circular crate dependency (`noxu-tree` → `noxu-dbi`).
220 pub memory_counter: Option<Arc<AtomicI64>>,
221
222 /// Optional listener fed on node add/access/remove, mirroring JE's
223 /// `INList` feeding the evictor's `LRUList`s.
224 ///
225 /// When `None` (the default — used by unit tests with no environment),
226 /// the notifications are no-ops. `EnvironmentImpl` installs the
227 /// `Evictor` here so production inserts/accesses populate the LRU lists
228 /// the evictor drains.
229 ///
230 /// JE reference: `IN.fetchTarget`/split/`rebuildINList` → `addBack`,
231 /// access → `moveBack`, removal → `remove`.
232 pub in_list_listener: Option<Arc<dyn InListListener>>,
233
234 /// Optional log manager so an evicted root IN can be re-materialized from
235 /// its persisted `root_log_lsn` on the next access (EV-14, piece B).
236 ///
237 /// JE's `Tree` reaches the log via `database.getEnv().getLogManager()`;
238 /// `Tree.getRootINRootAlreadyLatched` calls `root.fetchTarget(...)` which
239 /// reads the root IN back from its `ChildReference` LSN when the in-memory
240 /// target is null (Tree.java:477-516, ChildReference.fetchTarget). Noxu
241 /// has no env back-reference here, so the log manager is installed
242 /// directly (the same one-way wiring as `in_list_listener`). When `None`
243 /// (unit tests with no environment), an evicted root cannot be re-fetched
244 /// — but `evict_root` refuses to evict without a log manager, so the root
245 /// is never made non-resident in that configuration.
246 pub log_manager: Option<Arc<noxu_log::LogManager>>,
247
248 /// Capacity hint for the recovery redo path.
249 ///
250 /// When non-zero, the first BIN created by `redo_insert` (the first-key
251 /// path) pre-allocates its `entries` Vec with this capacity so that
252 /// redo insertions proceed without Vec-resize doublings. The value is
253 /// clamped to `max_entries_per_node` at use.
254 ///
255 /// Set by `hint_redo_capacity` before the redo loop.
256 /// Wave 11-K optimisation (Fix 3).
257 redo_capacity_hint: usize,
258
259 /// Whether key-prefix compression is enabled for this tree's BINs.
260 ///
261 /// JE `DatabaseImpl.getKeyPrefixing()` / `DatabaseConfig.setKeyPrefixing()`.
262 /// When `false`, `IN.computeKeyPrefix` returns `null` in JE — no prefix
263 /// is ever set. Noxu mirrors this: `insert_with_prefix` is skipped in
264 /// favour of `insert_raw`, and `recompute_key_prefix` is not called on
265 /// BIN halves after a split.
266 ///
267 /// Default: `false` (matches JE's `DatabaseConfig.KEY_PREFIXING_DEFAULT`).
268 ///
269 /// Ref: `IN.java computeKeyPrefix` ~line 2456.
270 pub key_prefixing: bool,
271 /// T-5: maximum post-prefix key length (bytes) for the compact key rep
272 /// (`INKeyRep.MaxKeySize`). A node packs all its keys into one fixed-width
273 /// byte array when every post-prefix key is `<=` this length; a longer key
274 /// inflates the node to the `Default` rep. `<= 0` disables the compact
275 /// rep entirely.
276 ///
277 /// Default 16 (`TREE_COMPACT_MAX_KEY_LENGTH` /
278 /// `INKeyRep.MaxKeySize.DEFAULT_MAX_KEY_LENGTH`). Wired from
279 /// `EnvironmentConfig` via `Tree::set_compact_max_key_length`
280 /// (`IN.getCompactMaxKeyLength`, IN.java:4929).
281 pub compact_max_key_length: i32,
282}
283
284/// A node in the tree.
285///
286/// TreeNode wraps an upper IN or a BIN. Each variant carries a lightweight
287/// stub whose fields mirror the persistent IN/BIN structure. The stubs will
288/// be replaced with full InNode/Bin types as the implementation matures; the
289/// API surface here is intentionally minimal.
290#[derive(Debug)]
291pub enum TreeNode {
292 /// Internal Node (IN) - non-leaf node in the tree.
293 Internal(InNodeStub),
294
295 /// Bottom Internal Node (BIN) - leaf-level internal node.
296 Bottom(BinStub),
297}
298
299/// Type alias for a resident child pointer.
300pub type ChildArc = Arc<RwLock<TreeNode>>;
301
302/// T-4: per-node representation of the resident-child-pointer array.
303///
304/// Faithful to JE `INTargetRep` (`INTargetRep.java`), the abstract array of
305/// target pointers to an IN's cached children. These arrays are usually
306/// sparse — most upper INs have NO resident children — so JE never stores a
307/// full per-slot `Node[]` until many children are actually cached:
308///
309/// * `None` — `INTargetRep.None`: a shared singleton, 0 child-pointer
310/// bytes, used when no children are cached (the common case for upper
311/// INs). `get` returns null for every slot.
312/// * `Sparse` — `INTargetRep.Sparse`: a small parallel `(index, target)[]`
313/// for 1..=`MAX_ENTRIES` cached children (JE caps at 4). `get(j)` is a
314/// linear scan of the index array.
315/// * `Default`— `INTargetRep.Default`: the full `Vec<Option<Arc>>`, one
316/// slot per entry, used once more than `MAX_ENTRIES` children are
317/// resident.
318///
319/// A node starts `None` and grows `None → Sparse → Default`. JE does not
320/// shrink back when entries are nulled (it only compacts on IN-stripping) to
321/// avoid transitionary rep churn; we follow the same policy — `set_child` only
322/// inflates, and `compact()` (called on eviction/stripping) collapses an
323/// empty/small `Default`/`Sparse` back toward `None`.
324#[derive(Debug)]
325pub enum TargetRep {
326 /// `INTargetRep.None` — no children cached (shared-singleton semantics).
327 None,
328 /// `INTargetRep.Sparse` — a few cached children, `(slot_index, child)`.
329 /// Invariant: `len() <= SPARSE_MAX_ENTRIES`.
330 Sparse(Vec<(u16, ChildArc)>),
331 /// `INTargetRep.Default` — full parallel array, one slot per entry.
332 Default(Vec<Option<ChildArc>>),
333}
334
335impl TargetRep {
336 /// `INTargetRep.Sparse.MAX_ENTRIES` (INTargetRep.java) — the maximum
337 /// number of cached children the `Sparse` rep holds before inflating to
338 /// `Default`.
339 pub const SPARSE_MAX_ENTRIES: usize = 4;
340
341 /// `INTargetRep.get(idx)` — the cached child for slot `idx`, or `None`.
342 #[inline]
343 pub fn get(&self, idx: usize) -> Option<&ChildArc> {
344 match self {
345 TargetRep::None => None,
346 TargetRep::Sparse(v) => {
347 v.iter().find(|(i, _)| *i as usize == idx).map(|(_, c)| c)
348 }
349 TargetRep::Default(v) => v.get(idx).and_then(|o| o.as_ref()),
350 }
351 }
352
353 /// `INTargetRep.set(idx, node, parent)` — set (or clear, when `node` is
354 /// `None`) the cached child for slot `idx`, mutating the representation
355 /// upward (`None → Sparse → Default`) as needed.
356 pub fn set(&mut self, idx: usize, node: Option<ChildArc>) {
357 match self {
358 TargetRep::None => {
359 // INTargetRep.None.set: clearing stays None; setting mutates
360 // to a Sparse rep and sets there.
361 if let Some(child) = node {
362 *self = TargetRep::Sparse(vec![(idx as u16, child)]);
363 }
364 }
365 TargetRep::Sparse(v) => {
366 // Update existing slot in place.
367 if let Some(pos) =
368 v.iter().position(|(i, _)| *i as usize == idx)
369 {
370 match node {
371 Some(child) => v[pos].1 = child,
372 None => {
373 v.swap_remove(pos);
374 }
375 }
376 return;
377 }
378 // New child: clearing a non-present slot is a no-op.
379 let Some(child) = node else { return };
380 if v.len() < Self::SPARSE_MAX_ENTRIES {
381 v.push((idx as u16, child));
382 return;
383 }
384 // Full — INTargetRep.Sparse.set mutates to Default.
385 let cap = v.iter().map(|(i, _)| *i as usize).max().unwrap_or(0);
386 let cap = cap.max(idx) + 1;
387 let mut def: Vec<Option<ChildArc>> = vec![None; cap];
388 for (i, c) in v.drain(..) {
389 def[i as usize] = Some(c);
390 }
391 def[idx] = Some(child);
392 *self = TargetRep::Default(def);
393 }
394 TargetRep::Default(v) => {
395 if idx >= v.len() {
396 if node.is_none() {
397 return;
398 }
399 v.resize_with(idx + 1, || None);
400 }
401 v[idx] = node;
402 }
403 }
404 }
405
406 /// `INTargetRep.None`-aware take: remove and return the cached child for
407 /// slot `idx`, leaving the slot empty (JE `IN.setTarget(idx, null)` plus
408 /// returning the old target).
409 pub fn take(&mut self, idx: usize) -> Option<ChildArc> {
410 match self {
411 TargetRep::None => None,
412 TargetRep::Sparse(v) => v
413 .iter()
414 .position(|(i, _)| *i as usize == idx)
415 .map(|pos| v.swap_remove(pos).1),
416 TargetRep::Default(v) => v.get_mut(idx).and_then(|o| o.take()),
417 }
418 }
419
420 /// JE `INArrayRep.copy(from, to, n, parent)` adapted to slice ops: shift
421 /// the child mapping when an entry is INSERTED at `idx` (all children at
422 /// slots `>= idx` move up by one). Mirrors how `Vec::insert` shifts the
423 /// parallel `entries` array.
424 pub fn insert_shift(&mut self, idx: usize) {
425 match self {
426 TargetRep::None => {}
427 TargetRep::Sparse(v) => {
428 for (i, _) in v.iter_mut() {
429 if (*i as usize) >= idx {
430 *i += 1;
431 }
432 }
433 }
434 TargetRep::Default(v) => {
435 if idx <= v.len() {
436 v.insert(idx, None);
437 }
438 }
439 }
440 }
441
442 /// JE `INArrayRep.copy` adapted: shift the child mapping when the entry at
443 /// `idx` is REMOVED (all children at slots `> idx` move down by one; the
444 /// child at `idx` itself is dropped). Mirrors `Vec::remove`.
445 pub fn remove_shift(&mut self, idx: usize) {
446 match self {
447 TargetRep::None => {}
448 TargetRep::Sparse(v) => {
449 v.retain(|(i, _)| *i as usize != idx);
450 for (i, _) in v.iter_mut() {
451 if (*i as usize) > idx {
452 *i -= 1;
453 }
454 }
455 }
456 TargetRep::Default(v) => {
457 if idx < v.len() {
458 v.remove(idx);
459 }
460 }
461 }
462 }
463
464 /// `INTargetRep.compact(parent)` — collapse toward the most compact rep:
465 /// an empty rep becomes `None`; a `Default` with `<= MAX_ENTRIES` children
466 /// becomes `Sparse` (or `None`). Called when an IN is stripped/evicted.
467 pub fn compact(&mut self) {
468 let count = self.resident_count();
469 if count == 0 {
470 *self = TargetRep::None;
471 return;
472 }
473 if count <= Self::SPARSE_MAX_ENTRIES
474 && let TargetRep::Default(v) = self
475 {
476 let sparse: Vec<(u16, ChildArc)> = v
477 .iter()
478 .enumerate()
479 .filter_map(|(i, o)| o.as_ref().map(|c| (i as u16, c.clone())))
480 .collect();
481 *self = TargetRep::Sparse(sparse);
482 }
483 }
484
485 /// Number of resident (non-null) children.
486 pub fn resident_count(&self) -> usize {
487 match self {
488 TargetRep::None => 0,
489 TargetRep::Sparse(v) => v.len(),
490 TargetRep::Default(v) => v.iter().filter(|o| o.is_some()).count(),
491 }
492 }
493
494 /// True if no children are cached (`INTargetRep.None` or empty).
495 pub fn is_empty(&self) -> bool {
496 self.resident_count() == 0
497 }
498
499 /// Iterate every resident child (in unspecified order).
500 pub fn iter_children(&self) -> Box<dyn Iterator<Item = ChildArc> + '_> {
501 match self {
502 TargetRep::None => Box::new(std::iter::empty()),
503 TargetRep::Sparse(v) => Box::new(v.iter().map(|(_, c)| c.clone())),
504 TargetRep::Default(v) => {
505 Box::new(v.iter().filter_map(|o| o.clone()))
506 }
507 }
508 }
509
510 /// `INTargetRep.calculateMemorySize()` — heap bytes of the rep itself
511 /// (excluding the children it points at). `None` is 0 (shared singleton),
512 /// matching `INTargetRep.None.calculateMemorySize() == 0`.
513 pub fn memory_size(&self) -> usize {
514 use std::mem::size_of;
515 match self {
516 TargetRep::None => 0,
517 TargetRep::Sparse(v) => v.capacity() * size_of::<(u16, ChildArc)>(),
518 TargetRep::Default(v) => {
519 v.capacity() * size_of::<Option<ChildArc>>()
520 }
521 }
522 }
523}
524
525/// T-3: node-level packed LSN array — `IN.entryLsnByteArray` /
526/// `IN.entryLsnLongArray` (IN.java:251-289, getLsn/setLsnInternal
527/// IN.java:1752-1935).
528///
529/// JE stores one LSN per slot. A naive `Lsn` (u64) costs 8 bytes/slot even
530/// though most LSNs in a node share a file number and have a file offset that
531/// fits in 3 bytes. JE's compact rep is a single `byte[]` with
532/// `BYTES_PER_LSN_ENTRY == 4` bytes per slot:
533///
534/// * `base_file_number` is the lowest file number of any non-NULL LSN in the
535/// node;
536/// * byte 0 of each slot = `file_number - base_file_number` (0..=127,
537/// `Byte.MAX_VALUE`);
538/// * bytes 1..4 = the 3-byte little-endian file offset (max
539/// `MAX_FILE_OFFSET == 0xff_fffe`).
540///
541/// The NULL_LSN blocker (Noxu `NULL_LSN == u64::MAX`) is solved EXACTLY as JE
542/// does it: NULL is NOT stored as the raw u64; the slot's 3 file-offset bytes
543/// are set to `0xff_ffff` (`THREE_BYTE_NEGATIVE_ONE`), a value `MAX_FILE_OFFSET`
544/// can never reach, and `get_lsn` maps it back to `NULL_LSN`.
545///
546/// If a file-number difference exceeds 127 or a file offset exceeds
547/// `MAX_FILE_OFFSET`, the rep mutates to `Long` (one `u64` per slot), matching
548/// JE's `mutateToLongArray` (IN.java:1924). An all-NULL node uses `Empty`
549/// (0 bytes), matching the EMPTY_REP/initial-capacity-free state.
550#[derive(Debug)]
551pub enum LsnRep {
552 /// All slots NULL — 0 heap bytes (the `byteArray == null` initial state).
553 Empty,
554 /// `IN.entryLsnByteArray` — 4 bytes/slot, `base_file_number`-relative.
555 Compact { base_file_number: u32, bytes: Vec<u8> },
556 /// `IN.entryLsnLongArray` — 8 bytes/slot fallback after `mutateToLongArray`.
557 Long(Vec<Lsn>),
558}
559
560impl LsnRep {
561 /// `IN.BYTES_PER_LSN_ENTRY` (IN.java:151).
562 pub const BYTES_PER_LSN_ENTRY: usize = 4;
563 /// `IN.MAX_FILE_OFFSET` (IN.java:152) — max file offset the 3-byte form holds.
564 const MAX_FILE_OFFSET: u32 = 0x00ff_fffe;
565 /// `IN.THREE_BYTE_NEGATIVE_ONE` (IN.java:153) — the NULL sentinel in the
566 /// 3 file-offset bytes.
567 const THREE_BYTE_NEGATIVE_ONE: u32 = 0x00ff_ffff;
568 /// `Byte.MAX_VALUE` — max file-number difference the 1-byte offset holds.
569 const MAX_FILE_NUMBER_OFFSET: u32 = 127;
570
571 /// A rep sized for `n` slots, all NULL. Returns `Empty` (0 bytes); the
572 /// Compact byte array is lazily allocated by the first non-NULL `set_lsn`
573 /// — `base_file_number` is unknown until then (IN.java:1820, the
574 /// `baseFileNumber == -1` first-entry case).
575 #[inline]
576 pub fn new(_n: usize) -> Self {
577 LsnRep::Empty
578 }
579
580 /// Build a rep from a per-slot `Lsn` slice (used by node construction and
581 /// split, where slots arrive together). Equivalent to `new(lsns.len())`
582 /// followed by `set(i, lsns[i])` for each slot.
583 pub fn from_lsns(lsns: &[Lsn]) -> Self {
584 let mut rep = LsnRep::Empty;
585 let n = lsns.len();
586 for (i, &lsn) in lsns.iter().enumerate() {
587 rep.set(i, lsn, n);
588 }
589 rep
590 }
591
592 /// `IN.getLsn(idx)` (IN.java:1752).
593 pub fn get(&self, idx: usize) -> Lsn {
594 match self {
595 LsnRep::Empty => NULL_LSN,
596 LsnRep::Long(v) => v.get(idx).copied().unwrap_or(NULL_LSN),
597 LsnRep::Compact { base_file_number, bytes } => {
598 let off = idx * Self::BYTES_PER_LSN_ENTRY;
599 if off + Self::BYTES_PER_LSN_ENTRY > bytes.len() {
600 return NULL_LSN;
601 }
602 let file_offset = Self::get_3byte(bytes, off + 1);
603 if file_offset == Self::THREE_BYTE_NEGATIVE_ONE {
604 NULL_LSN
605 } else {
606 let file_number = base_file_number + bytes[off] as u32;
607 Lsn::new(file_number, file_offset)
608 }
609 }
610 }
611 }
612
613 /// `IN.setLsnInternal(idx, value)` (IN.java:1801) — set the LSN of slot
614 /// `idx`, mutating Empty→Compact→Long as necessary. `n` is the node's
615 /// slot count (sizes a freshly-allocated Compact array).
616 pub fn set(&mut self, idx: usize, lsn: Lsn, n: usize) {
617 // Empty: first non-NULL value allocates the Compact array; a NULL set
618 // on an Empty rep is a no-op (all slots already read NULL).
619 if let LsnRep::Empty = self {
620 if lsn.is_null() {
621 return;
622 }
623 let cap = n.max(idx + 1);
624 *self = LsnRep::Compact {
625 base_file_number: lsn.file_number(),
626 bytes: vec![0u8; cap * Self::BYTES_PER_LSN_ENTRY],
627 };
628 // Mark every other slot NULL (3-byte offset = 0xffffff).
629 if let LsnRep::Compact { bytes, .. } = self {
630 for s in 0..cap {
631 if s != idx {
632 Self::put_3byte(
633 bytes,
634 s * Self::BYTES_PER_LSN_ENTRY + 1,
635 Self::THREE_BYTE_NEGATIVE_ONE,
636 );
637 }
638 }
639 }
640 self.set(idx, lsn, n);
641 return;
642 }
643
644 if let LsnRep::Long(v) = self {
645 if idx >= v.len() {
646 v.resize(idx + 1, NULL_LSN);
647 }
648 v[idx] = lsn;
649 return;
650 }
651
652 // Compact path.
653 let LsnRep::Compact { base_file_number, bytes } = self else {
654 unreachable!()
655 };
656 let need = (idx + 1) * Self::BYTES_PER_LSN_ENTRY;
657 if need > bytes.len() {
658 let old = bytes.len() / Self::BYTES_PER_LSN_ENTRY;
659 bytes.resize(need, 0);
660 for s in old..(idx + 1) {
661 Self::put_3byte(
662 bytes,
663 s * Self::BYTES_PER_LSN_ENTRY + 1,
664 Self::THREE_BYTE_NEGATIVE_ONE,
665 );
666 }
667 }
668 let off = idx * Self::BYTES_PER_LSN_ENTRY;
669
670 if lsn.is_null() {
671 // IN.java:1812 — file-number offset 0, file offset -1 (0xffffff).
672 bytes[off] = 0;
673 Self::put_3byte(bytes, off + 1, Self::THREE_BYTE_NEGATIVE_ONE);
674 return;
675 }
676
677 let this_file_number = lsn.file_number();
678 let this_file_offset = lsn.file_offset();
679
680 // Whether to fall back to the Long rep.
681 let mutate = this_file_offset > Self::MAX_FILE_OFFSET || {
682 if this_file_number < *base_file_number {
683 // IN.java:1827 — try to re-base downward; bail if any existing
684 // slot would then exceed the 1-byte file-number offset.
685 !Self::adjust_file_numbers(
686 bytes,
687 *base_file_number,
688 this_file_number,
689 )
690 } else {
691 this_file_number - *base_file_number
692 > Self::MAX_FILE_NUMBER_OFFSET
693 }
694 };
695
696 if mutate {
697 // IN.java:1924 mutateToLongArray.
698 let nelts = bytes.len() / Self::BYTES_PER_LSN_ENTRY;
699 let mut longs = vec![NULL_LSN; nelts.max(idx + 1)];
700 for (s, slot) in longs.iter_mut().enumerate().take(nelts) {
701 *slot = self_get_compact(*base_file_number, bytes, s);
702 }
703 longs[idx] = lsn;
704 *self = LsnRep::Long(longs);
705 return;
706 }
707
708 if this_file_number < *base_file_number {
709 *base_file_number = this_file_number;
710 }
711 bytes[off] = (this_file_number - *base_file_number) as u8;
712 Self::put_3byte(bytes, off + 1, this_file_offset);
713 }
714
715 /// `IN.adjustFileNumbers` (IN.java:1855) — re-base to a lower file number,
716 /// rewriting every existing slot's 1-byte offset. Returns false (and
717 /// leaves `bytes` unchanged) if any slot would overflow the 1-byte offset.
718 fn adjust_file_numbers(
719 bytes: &mut [u8],
720 old_base: u32,
721 new_base: u32,
722 ) -> bool {
723 let stride = Self::BYTES_PER_LSN_ENTRY;
724 // First pass: verify none overflow.
725 let mut i = 0;
726 while i < bytes.len() {
727 if Self::get_3byte(bytes, i + 1) != Self::THREE_BYTE_NEGATIVE_ONE {
728 let cur_fn = old_base + bytes[i] as u32;
729 if cur_fn - new_base > Self::MAX_FILE_NUMBER_OFFSET {
730 return false;
731 }
732 }
733 i += stride;
734 }
735 // Second pass: apply.
736 let mut i = 0;
737 while i < bytes.len() {
738 if Self::get_3byte(bytes, i + 1) != Self::THREE_BYTE_NEGATIVE_ONE {
739 let cur_fn = old_base + bytes[i] as u32;
740 bytes[i] = (cur_fn - new_base) as u8;
741 }
742 i += stride;
743 }
744 true
745 }
746
747 /// `INArrayRep.copy` analogue: shift LSNs when an entry is inserted at
748 /// `idx` (slots `>= idx` move up one). Mirrors `targets.insert_shift`.
749 pub fn insert_shift(&mut self, idx: usize, n: usize) {
750 match self {
751 LsnRep::Empty => {}
752 LsnRep::Long(v) => {
753 if idx <= v.len() {
754 v.insert(idx, NULL_LSN);
755 }
756 }
757 LsnRep::Compact { bytes, .. } => {
758 let stride = Self::BYTES_PER_LSN_ENTRY;
759 let cap = (n.max((bytes.len() / stride) + 1)) * stride;
760 bytes.resize(cap, 0);
761 let at = idx * stride;
762 // Shift the tail up by one slot.
763 bytes.copy_within(at..cap - stride, at + stride);
764 // The new slot reads NULL.
765 Self::put_3byte(bytes, at + 1, Self::THREE_BYTE_NEGATIVE_ONE);
766 }
767 }
768 }
769
770 /// `INArrayRep.copy` analogue: shift LSNs when entry `idx` is removed
771 /// (slots `> idx` move down one). Mirrors `targets.remove_shift`.
772 pub fn remove_shift(&mut self, idx: usize) {
773 match self {
774 LsnRep::Empty => {}
775 LsnRep::Long(v) => {
776 if idx < v.len() {
777 v.remove(idx);
778 }
779 }
780 LsnRep::Compact { bytes, .. } => {
781 let stride = Self::BYTES_PER_LSN_ENTRY;
782 let at = idx * stride;
783 if at + stride <= bytes.len() {
784 bytes.copy_within(at + stride.., at);
785 let newlen = bytes.len() - stride;
786 bytes.truncate(newlen);
787 }
788 }
789 }
790 }
791
792 /// `IN.computeLsnOverhead` analogue: heap bytes of the rep itself.
793 pub fn memory_size(&self) -> usize {
794 use std::mem::size_of;
795 match self {
796 LsnRep::Empty => 0,
797 LsnRep::Compact { bytes, .. } => bytes.capacity(),
798 LsnRep::Long(v) => v.capacity() * size_of::<Lsn>(),
799 }
800 }
801
802 fn put_3byte(bytes: &mut [u8], offset: usize, value: u32) {
803 bytes[offset] = (value & 0xFF) as u8;
804 bytes[offset + 1] = ((value >> 8) & 0xFF) as u8;
805 bytes[offset + 2] = ((value >> 16) & 0xFF) as u8;
806 }
807
808 fn get_3byte(bytes: &[u8], offset: usize) -> u32 {
809 (bytes[offset] as u32)
810 | ((bytes[offset + 1] as u32) << 8)
811 | ((bytes[offset + 2] as u32) << 16)
812 }
813}
814
815/// Helper used by `LsnRep::set` during `mutateToLongArray` to read an existing
816/// Compact slot without borrowing `self` (which is mid-mutation).
817fn self_get_compact(base_file_number: u32, bytes: &[u8], idx: usize) -> Lsn {
818 let off = idx * LsnRep::BYTES_PER_LSN_ENTRY;
819 let file_offset = LsnRep::get_3byte(bytes, off + 1);
820 if file_offset == LsnRep::THREE_BYTE_NEGATIVE_ONE {
821 NULL_LSN
822 } else {
823 Lsn::new(base_file_number + bytes[off] as u32, file_offset)
824 }
825}
826
827/// `INKeyRep.MaxKeySize.DEFAULT_MAX_KEY_LENGTH` (INKeyRep.java) and the
828/// `TREE_COMPACT_MAX_KEY_LENGTH` config default.
829#[allow(non_upper_case_globals)]
830pub const INKeyRep_DEFAULT_MAX_KEY_LENGTH: i32 = 16;
831
832/// T-2: node-level key array — `INKeyRep.{Default,MaxKeySize}` (INKeyRep.java).
833///
834/// The per-slot key that used to live in `BinEntry`/`InEntry` as a `Vec<u8>`
835/// (24-byte header + a separate heap allocation per key) is hoisted here as a
836/// node-level rep. When every (post-prefix) key in the node is `<=`
837/// `TREE_COMPACT_MAX_KEY_LENGTH` (default 16) the keys pack into ONE
838/// fixed-width byte buffer (`MaxKeySize`): `slot_width` bytes per slot, with a
839/// parallel `lengths` vector tracking the actual length of each key. A key
840/// longer than the threshold inflates the whole node to the `Default` rep
841/// (one `Vec<u8>` per slot), matching JE's `Default.compact` /
842/// `MaxKeySize.expandToDefaultRep`.
843///
844/// As in JE, this stores the UNPREFIXED suffix (key prefixing strips the
845/// common prefix first), so the compact rep is the smaller post-prefix bytes.
846#[derive(Debug, Clone)]
847pub enum KeyRep {
848 /// `INKeyRep.Default` — one owned key per slot (any length).
849 Default(Vec<Vec<u8>>),
850 /// `INKeyRep.MaxKeySize` — all keys packed into one fixed-width buffer.
851 /// `buf.len() == slot_width * lengths.len()`; slot `i` occupies
852 /// `buf[i*slot_width .. i*slot_width + lengths[i]]`.
853 Compact { buf: Vec<u8>, slot_width: usize, lengths: Vec<u16> },
854}
855
856impl KeyRep {
857 /// An empty `Default` rep.
858 #[inline]
859 pub fn new() -> Self {
860 KeyRep::Default(Vec::new())
861 }
862
863 /// Build a `Default` rep from owned keys (callers may later `compact`).
864 #[inline]
865 pub fn from_keys(keys: Vec<Vec<u8>>) -> Self {
866 KeyRep::Default(keys)
867 }
868
869 /// Number of slots.
870 #[inline]
871 pub fn len(&self) -> usize {
872 match self {
873 KeyRep::Default(v) => v.len(),
874 KeyRep::Compact { lengths, .. } => lengths.len(),
875 }
876 }
877
878 #[inline]
879 pub fn is_empty(&self) -> bool {
880 self.len() == 0
881 }
882
883 /// `INKeyRep.get(idx)` / `getKey` — borrow the (post-prefix) key at slot
884 /// `idx` without allocating.
885 #[inline]
886 pub fn get(&self, idx: usize) -> &[u8] {
887 match self {
888 KeyRep::Default(v) => v[idx].as_slice(),
889 KeyRep::Compact { buf, slot_width, lengths } => {
890 let off = idx * slot_width;
891 &buf[off..off + lengths[idx] as usize]
892 }
893 }
894 }
895
896 /// Set the key at slot `idx`. A key longer than a Compact rep's
897 /// `slot_width` inflates the rep to `Default` first
898 /// (`MaxKeySize.expandToDefaultRep`).
899 pub fn set(&mut self, idx: usize, key: Vec<u8>) {
900 match self {
901 KeyRep::Default(v) => v[idx] = key,
902 KeyRep::Compact { slot_width, .. } if key.len() > *slot_width => {
903 self.inflate_to_default();
904 self.set(idx, key);
905 }
906 KeyRep::Compact { buf, slot_width, lengths } => {
907 let off = idx * *slot_width;
908 buf[off..off + key.len()].copy_from_slice(&key);
909 lengths[idx] = key.len() as u16;
910 }
911 }
912 }
913
914 /// Insert a key at slot `idx`, shifting later slots up (mirrors
915 /// `Vec::insert` + `INArrayRep.copy`).
916 pub fn insert(&mut self, idx: usize, key: Vec<u8>) {
917 match self {
918 KeyRep::Default(v) => v.insert(idx, key),
919 KeyRep::Compact { slot_width, .. } if key.len() > *slot_width => {
920 self.inflate_to_default();
921 self.insert(idx, key);
922 }
923 KeyRep::Compact { buf, slot_width, lengths } => {
924 let sw = *slot_width;
925 let at = idx * sw;
926 buf.splice(at..at, std::iter::repeat_n(0u8, sw));
927 buf[at..at + key.len()].copy_from_slice(&key);
928 lengths.insert(idx, key.len() as u16);
929 }
930 }
931 }
932
933 /// Remove the key at slot `idx`, shifting later slots down.
934 pub fn remove(&mut self, idx: usize) -> Vec<u8> {
935 match self {
936 KeyRep::Default(v) => v.remove(idx),
937 KeyRep::Compact { buf, slot_width, lengths } => {
938 let sw = *slot_width;
939 let len = lengths[idx] as usize;
940 let at = idx * sw;
941 let out = buf[at..at + len].to_vec();
942 buf.drain(at..at + sw);
943 lengths.remove(idx);
944 out
945 }
946 }
947 }
948
949 /// `INKeyRep.MaxKeySize.expandToDefaultRep` — mutate a Compact rep to a
950 /// Default rep (one owned `Vec<u8>` per slot).
951 fn inflate_to_default(&mut self) {
952 if let KeyRep::Compact { .. } = self {
953 let keys: Vec<Vec<u8>> =
954 (0..self.len()).map(|i| self.get(i).to_vec()).collect();
955 *self = KeyRep::Default(keys);
956 }
957 }
958
959 /// `INKeyRep.Default.compact(parent)` (INKeyRep.java) — if every key in a
960 /// `Default` rep fits `compact_max_key_length`, pack them into a
961 /// `MaxKeySize` (`Compact`) rep. `compact_max_key_length <= 0` disables
962 /// compaction. No-op when already Compact.
963 pub fn compact(&mut self, compact_max_key_length: i32) {
964 if compact_max_key_length <= 0 {
965 return;
966 }
967 let KeyRep::Default(keys) = self else {
968 return; // already Compact
969 };
970 if keys.is_empty() {
971 return;
972 }
973 let max_len = keys.iter().map(|k| k.len()).max().unwrap_or(0);
974 if max_len > compact_max_key_length as usize {
975 return; // a key exceeds the threshold — stay Default
976 }
977 let slot_width = max_len.max(1);
978 let mut buf = vec![0u8; slot_width * keys.len()];
979 let mut lengths = Vec::with_capacity(keys.len());
980 for (i, k) in keys.iter().enumerate() {
981 let off = i * slot_width;
982 buf[off..off + k.len()].copy_from_slice(k);
983 lengths.push(k.len() as u16);
984 }
985 *self = KeyRep::Compact { buf, slot_width, lengths };
986 }
987
988 /// True when key-byte memory is accounted for inside this rep (Compact),
989 /// vs per-slot `Vec` allocations (Default).
990 /// `INKeyRep.accountsForKeyByteMemUsage`.
991 #[inline]
992 pub fn is_compact(&self) -> bool {
993 matches!(self, KeyRep::Compact { .. })
994 }
995
996 /// Heap bytes of the rep itself (`INKeyRep.calculateMemorySize` +
997 /// key-byte accounting). For Default this is the `Vec<Vec<u8>>` header
998 /// plus each key's heap allocation; for Compact it is the single buffer
999 /// plus the lengths vector.
1000 pub fn memory_size(&self) -> usize {
1001 use std::mem::size_of;
1002 match self {
1003 KeyRep::Default(v) => {
1004 v.capacity() * size_of::<Vec<u8>>()
1005 + v.iter().map(|k| k.capacity()).sum::<usize>()
1006 }
1007 KeyRep::Compact { buf, lengths, .. } => {
1008 buf.capacity() + lengths.capacity() * size_of::<u16>()
1009 }
1010 }
1011 }
1012}
1013
1014impl Default for KeyRep {
1015 fn default() -> Self {
1016 KeyRep::new()
1017 }
1018}
1019
1020/// Lightweight upper-IN representation used by the tree traversal layer.
1021///
1022/// `IN`: carries the dirty flag (IN_DIRTY_BIT), the LRU
1023/// generation counter, and a weak back-pointer to the parent so that
1024/// dirty state can be propagated upward.
1025#[derive(Debug)]
1026pub struct InNodeStub {
1027 /// Node ID.
1028 pub node_id: u64,
1029 /// Level in tree.
1030 pub level: i32,
1031 /// Child entries (key, lsn).
1032 pub entries: Vec<InEntry>,
1033 /// T-4: per-node resident-child-pointer representation.
1034 ///
1035 /// `IN.entryTargets` (`INTargetRep`). The cached child pointer is no
1036 /// longer a per-`InEntry` `Option<Arc>` (which cost a pointer-sized slot
1037 /// even when no child was resident); it lives here as a compact
1038 /// node-level rep that starts `None` (0 child-pointer bytes — most upper
1039 /// INs have no resident children), grows to `Sparse` for a few cached
1040 /// children, and inflates to `Default` (the full parallel array) once
1041 /// many children are resident. See `INTargetRep.{None,Sparse,Default}`.
1042 pub targets: TargetRep,
1043 /// Dirty flag — set whenever this node is modified.
1044 /// `IN.dirty` (IN_DIRTY_BIT).
1045 pub dirty: bool,
1046 /// LRU generation counter for the evictor.
1047 /// `IN.generation`.
1048 pub generation: u64,
1049 /// Weak back-pointer to parent IN.
1050 /// Enables dirty-propagation and latch-coupling validation.
1051 /// `IN.parent` reference used during splits and logging.
1052 pub parent: Option<Weak<RwLock<TreeNode>>>,
1053 /// T-3: per-node packed LSN array (`IN.entryLsnByteArray`). The per-slot
1054 /// `lsn` (8 bytes) that used to live in `InEntry` is hoisted here as a
1055 /// `base_file_number`-relative 4-byte-per-slot rep, falling back to a
1056 /// `u64`-per-slot `Long` rep only when a node's LSN range exceeds the
1057 /// compact form. Access via `get_lsn(slot)` / `set_lsn(slot, lsn)`.
1058 pub lsn_rep: LsnRep,
1059}
1060
1061/// Entry in an IN node.
1062///
1063/// T-4: the resident-child pointer that used to live here (`Option<Arc>`) was
1064/// hoisted to the node-level `InNodeStub.targets` (`INTargetRep`); access the
1065/// child for slot `i` via `InNodeStub::get_child(i)` / `set_child` / etc.
1066///
1067/// T-3: the per-slot `lsn` (8 bytes) that used to live here was hoisted to the
1068/// node-level `InNodeStub.lsn_rep` (`IN.entryLsnByteArray`); access the LSN for
1069/// slot `i` via `InNodeStub::get_lsn(i)` / `set_lsn(i, lsn)`.
1070#[derive(Debug, Clone)]
1071pub struct InEntry {
1072 /// Key for this entry.
1073 pub key: Vec<u8>,
1074}
1075
1076/// Lightweight BIN representation used by the tree traversal layer.
1077///
1078/// `BIN` (which extends `IN`): carries the dirty flag, LRU
1079/// generation counter, and a weak back-pointer to the parent IN.
1080///
1081/// # Key Prefix Compression
1082///
1083/// BINs support key prefix compression. When
1084/// `key_prefix` is non-empty the `key` field of every `BinEntry` stores only
1085/// the *suffix* — the bytes after stripping the common leading bytes. The
1086/// full key is reconstructed by prepending `key_prefix` to the stored suffix.
1087///
1088/// This is transparent to callers through the `get_full_key` / `find_entry`
1089/// helpers on `BinStub`. The prefix is recomputed after every insert and
1090/// after a split via `recompute_key_prefix`.
1091#[derive(Debug)]
1092pub struct BinStub {
1093 /// Node ID.
1094 pub node_id: u64,
1095 /// Level (always BIN_LEVEL).
1096 pub level: i32,
1097 /// Entries. When `key_prefix` is non-empty the `key` field in each entry
1098 /// is the *suffix* of the full key (leading `key_prefix` bytes stripped).
1099 /// `IN.entryKeys` (suffix-only storage when prefixing is on).
1100 pub entries: Vec<BinEntry>,
1101 /// Common prefix shared by every key in this BIN.
1102 /// Empty slice means no prefix compression is active.
1103 /// `IN.keyPrefix`.
1104 pub key_prefix: Vec<u8>,
1105 /// Dirty flag — set whenever this BIN is modified.
1106 /// `IN.dirty` (IN_DIRTY_BIT).
1107 pub dirty: bool,
1108 /// BIN-delta flag — true when this BIN contains only dirty (delta) slots
1109 /// rather than a complete set of entries.
1110 /// `IN.IN_DELTA_BIT` (the IN_DELTA_BIT flag inside `flags`).
1111 pub is_delta: bool,
1112 /// LSN at which this BIN was last logged as a full (non-delta) BIN.
1113 ///
1114 /// Used by the checkpoint path to construct `BINDeltaLogEntry.prev_full_lsn`
1115 /// and to compare against `prev_delta_lsn` when deciding whether to write
1116 /// a delta or a full BIN.
1117 ///
1118 /// `BIN.lastFullLsn`.
1119 pub last_full_lsn: Lsn,
1120 /// LSN at which this BIN was last logged as a BIN-delta.
1121 ///
1122 /// Written as `prev_delta_lsn` into the next `BINDeltaLogEntry` so the
1123 /// cleaner's utilization tracker can mark the superseded delta obsolete.
1124 /// Reset to `NULL_LSN` whenever a full BIN is written.
1125 ///
1126 /// `BIN.lastDeltaVersion` / `BIN.getLastDeltaLsn()`.
1127 pub last_delta_lsn: Lsn,
1128 /// LRU generation counter for the evictor.
1129 /// `IN.generation`.
1130 pub generation: u64,
1131 /// Weak back-pointer to parent IN.
1132 /// Enables dirty-propagation and latch-coupling validation.
1133 pub parent: Option<Weak<RwLock<TreeNode>>>,
1134 /// If true, `BinEntry.expiration_time` values in this BIN are packed hours
1135 /// since epoch; if false, they are packed seconds since epoch.
1136 ///
1137 /// Default: `true` (hours, matching TTL resolution).
1138 ///
1139 /// `BIN.expirationInHours`.
1140 pub expiration_in_hours: bool,
1141 /// Number of cursors currently positioned on this BIN.
1142 ///
1143 /// The evictor skips BINs with a non-zero cursor count to avoid evicting
1144 /// a node that a cursor is actively traversing. CursorImpl increments
1145 /// this when positioning on a BIN and decrements it on reposition/close.
1146 ///
1147 /// `IN.cursorSet.size()` used by `Evictor.selectIN()`.
1148 pub cursor_count: i32,
1149 /// When true, the NEXT log of this BIN must be a full BIN, not a delta.
1150 ///
1151 /// Set after a dirty slot is removed (a delta would silently lose that
1152 /// removal) and cleared after a full BIN is written. This is the
1153 /// delta-chain bound: it forces a periodic full BIN so a delta never
1154 /// references stale state.
1155 ///
1156 /// `IN.prohibitNextDelta` / `IN.setProhibitNextDelta` (IN.java:5013) /
1157 /// `IN.getProhibitNextDelta`.
1158 pub prohibit_next_delta: bool,
1159 /// T-3: per-node packed LSN array (`IN.entryLsnByteArray`). The per-slot
1160 /// `lsn` (8 bytes) that used to live in `BinEntry` is hoisted here as a
1161 /// `base_file_number`-relative 4-byte-per-slot rep. Access via
1162 /// `get_lsn(slot)` / `set_lsn(slot, lsn)`.
1163 pub lsn_rep: LsnRep,
1164 /// T-2: per-node key array (`INKeyRep.{Default,MaxKeySize}`). The per-slot
1165 /// `key` (`Vec<u8>`, 24-byte header + heap alloc) that used to live in
1166 /// `BinEntry` is hoisted here. Stores the post-prefix SUFFIX (key
1167 /// prefixing strips the common prefix first). Packs into one fixed-width
1168 /// buffer (`Compact`) when every suffix is `<= compact_max_key_length`,
1169 /// else one `Vec<u8>` per slot (`Default`). `keys.len()` is kept in lock
1170 /// step with `entries.len()`. Access via `get_key(slot)` /
1171 /// `get_full_key(slot)`.
1172 pub keys: KeyRep,
1173 /// T-5: the node's compact-key threshold (`IN.getCompactMaxKeyLength`),
1174 /// copied from the owning `Tree` at construction so `apply_new_prefix` can
1175 /// decide whether the suffixes now fit `MaxKeySize`. Default 16.
1176 pub compact_max_key_length: i32,
1177}
1178
1179/// Entry in a BIN node.
1180///
1181/// T-3: the per-slot `lsn` (8 bytes) that used to live here was hoisted to the
1182/// node-level `BinStub.lsn_rep` (`IN.entryLsnByteArray`); access the LSN for
1183/// slot `i` via `BinStub::get_lsn(i)` / `set_lsn(i, lsn)`.
1184#[derive(Debug, Clone)]
1185pub struct BinEntry {
1186 /// Optional embedded data (for small records) or cached LN.
1187 pub data: Option<Vec<u8>>,
1188 /// True when this slot has been marked known-deleted (analogous to the
1189 /// KNOWN_DELETED_BIT in `IN.entryStates`). The slot is eligible for
1190 /// removal by `compress_bin()`.
1191 pub known_deleted: bool,
1192 /// True when this slot has been modified since the last full BIN log write.
1193 ///
1194 /// `IN.entryStates[i] & IN_DIRTY_BIT`. Used by the checkpoint
1195 /// path to decide whether to write a BIN-delta (few dirty slots) or a
1196 /// full BIN (many dirty slots).
1197 pub dirty: bool,
1198 /// Packed expiration time (0 = no expiration).
1199 ///
1200 /// When the owning `BinStub.expiration_in_hours` is true, this value is
1201 /// hours since Unix epoch; otherwise it is seconds since Unix epoch.
1202 ///
1203 /// `IN.entryExpiration`.
1204 pub expiration_time: u32,
1205}
1206
1207impl InNodeStub {
1208 /// `IN.getTarget(idx)` — the resident child cached for slot `idx`, cloned
1209 /// (a strong `Arc`), or `None` if the child is not cached. Routes through
1210 /// the node-level `INTargetRep` (T-4).
1211 #[inline]
1212 pub fn get_child(&self, idx: usize) -> Option<ChildArc> {
1213 self.targets.get(idx).cloned()
1214 }
1215
1216 /// Borrow the resident child for slot `idx` without cloning.
1217 #[inline]
1218 pub fn child_ref(&self, idx: usize) -> Option<&ChildArc> {
1219 self.targets.get(idx)
1220 }
1221
1222 /// True if slot `idx` has no resident (cached) child.
1223 /// `IN.getTarget(idx) == null`.
1224 #[inline]
1225 pub fn child_is_none(&self, idx: usize) -> bool {
1226 self.targets.get(idx).is_none()
1227 }
1228
1229 /// `IN.setTarget(idx, node)` — set (or clear) the cached child for slot
1230 /// `idx`, mutating the `INTargetRep` upward as needed.
1231 #[inline]
1232 pub fn set_child(&mut self, idx: usize, node: Option<ChildArc>) {
1233 self.targets.set(idx, node);
1234 }
1235
1236 /// `IN.detachNode` helper — remove and return the cached child for slot
1237 /// `idx`, leaving the slot's key/LSN intact for re-fetch.
1238 #[inline]
1239 pub fn take_child(&mut self, idx: usize) -> Option<ChildArc> {
1240 self.targets.take(idx)
1241 }
1242
1243 /// `IN.getLsn(idx)` (IN.java:1752) — the LSN of slot `idx` via the
1244 /// node-level packed `LsnRep` (T-3).
1245 #[inline]
1246 pub fn get_lsn(&self, idx: usize) -> Lsn {
1247 self.lsn_rep.get(idx)
1248 }
1249
1250 /// `IN.setLsn(idx, lsn)` (IN.java:1773) — set the LSN of slot `idx` via
1251 /// the node-level packed `LsnRep` (T-3).
1252 #[inline]
1253 pub fn set_lsn(&mut self, idx: usize, lsn: Lsn) {
1254 let n = self.entries.len();
1255 self.lsn_rep.set(idx, lsn, n);
1256 }
1257
1258 /// Insert an entry at `idx`, shifting the child mapping to stay aligned
1259 /// (`INArrayRep.copy`), then set the new slot's cached child. Mirrors the
1260 /// old `entries.insert(idx, InEntry{ child: ..})` in one call.
1261 pub fn insert_entry(
1262 &mut self,
1263 idx: usize,
1264 key: Vec<u8>,
1265 lsn: Lsn,
1266 child: Option<ChildArc>,
1267 ) {
1268 self.entries.insert(idx, InEntry { key });
1269 let n = self.entries.len();
1270 self.lsn_rep.insert_shift(idx, n);
1271 self.lsn_rep.set(idx, lsn, n);
1272 self.targets.insert_shift(idx);
1273 if child.is_some() {
1274 self.targets.set(idx, child);
1275 }
1276 }
1277
1278 /// Remove the entry at `idx`, shifting the child mapping to stay aligned
1279 /// (`INArrayRep.copy`). Returns the removed `InEntry` (key).
1280 pub fn remove_entry(&mut self, idx: usize) -> InEntry {
1281 let e = self.entries.remove(idx);
1282 self.lsn_rep.remove_shift(idx);
1283 self.targets.remove_shift(idx);
1284 e
1285 }
1286
1287 /// All resident children (cloned `Arc`s), in unspecified order.
1288 /// Replaces `entries.iter().filter_map(|e| e.child.clone())`.
1289 pub fn resident_children(&self) -> Vec<ChildArc> {
1290 self.targets.iter_children().collect()
1291 }
1292
1293 /// `(slot_index, child)` of the first resident child, if any.
1294 pub fn first_resident_child(&self) -> Option<(usize, ChildArc)> {
1295 (0..self.entries.len())
1296 .find_map(|i| self.targets.get(i).map(|c| (i, c.clone())))
1297 }
1298}
1299
1300impl BinStub {
1301 /// `IN.getLsn(idx)` (IN.java:1752) — the LSN of slot `idx` via the
1302 /// node-level packed `LsnRep` (T-3).
1303 #[inline]
1304 pub fn get_lsn(&self, idx: usize) -> Lsn {
1305 self.lsn_rep.get(idx)
1306 }
1307
1308 /// `IN.setLsn(idx, lsn)` (IN.java:1773) — set the LSN of slot `idx` via
1309 /// the node-level packed `LsnRep` (T-3).
1310 #[inline]
1311 pub fn set_lsn(&mut self, idx: usize, lsn: Lsn) {
1312 let n = self.entries.len();
1313 self.lsn_rep.set(idx, lsn, n);
1314 }
1315
1316 /// TREE-F1: the single user-facing liveness predicate for a BIN slot.
1317 ///
1318 /// A slot is LIVE for reads/scans iff it is neither `known_deleted` nor
1319 /// TTL-expired. This mirrors the two ways JE makes a slot read as ABSENT:
1320 /// * `IN.findEntry` (IN.java:3197) returns -1 for a `known_deleted`
1321 /// exact match;
1322 /// * `CursorImpl.isProbablyExpired` / `lockAndGetCurrent`
1323 /// (CursorImpl.java:2062-2064) skip `isEntryKnownDeleted` (and
1324 /// expired) slots while stepping.
1325 ///
1326 /// KD slots legitimately exist in live BINs during BIN-delta
1327 /// reconstitution until the compressor reclaims them; the maintenance
1328 /// paths (compressor / recovery undo) iterate them on purpose and do NOT
1329 /// use this predicate.
1330 #[inline]
1331 pub fn slot_is_live(&self, idx: usize) -> bool {
1332 match self.entries.get(idx) {
1333 Some(e) => {
1334 !(e.known_deleted
1335 || (e.expiration_time != 0
1336 && noxu_util::ttl::is_expired(
1337 e.expiration_time,
1338 self.expiration_in_hours,
1339 )))
1340 }
1341 None => false,
1342 }
1343 }
1344
1345 // ========================================================================
1346 // Key prefix compression helpers
1347 // IN.computeKeyPrefix / IN.recalcSuffixes / IN.getKey
1348 // ========================================================================
1349
1350 /// Strips embedded LN data from non-dirty slots, freeing the heap
1351 /// allocations of the per-slot value bytes while keeping the slot keys
1352 /// and LSNs addressable. Used by the evictor's PartialEvict path: a
1353 /// hot BIN is kept in cache so its descent path stays warm, but the LN
1354 /// data is dropped to make room for hotter content. Subsequent reads
1355 /// re-fetch the data from the log via the slot LSN.
1356 ///
1357 /// Skips slots that are still dirty (their data has not been written
1358 /// to the log yet, so dropping the in-memory copy would lose the
1359 /// update). Returns the number of bytes freed (sum of the lengths
1360 /// of the dropped `Vec<u8>` data fields).
1361 ///
1362 /// Returns 0 if the BIN has any open cursors (the cursor may be
1363 /// reading the data right now).
1364 pub fn strip_lns(&mut self) -> usize {
1365 if self.cursor_count > 0 {
1366 return 0;
1367 }
1368 let mut freed = 0usize;
1369 for idx in 0..self.entries.len() {
1370 // JE BIN.evictLNs / LN.isEvictable (LN.java:263 returns true): an
1371 // LN's in-memory value can be stripped whenever it is recoverable
1372 // from the log — i.e. the slot has a valid (logged) LSN — REGARDLESS
1373 // of the dirty bit. The dirty bit governs whether the BIN's
1374 // *structure* needs re-logging at the next checkpoint (BIN-delta vs
1375 // full BIN), NOT whether the LN *value* is durable: a transactional
1376 // commit logs the LN, so the slot's LSN points at the durable copy
1377 // even while the slot is still dirty. Gating the strip on `!dirty`
1378 // (the previous behaviour) meant a freshly-written, not-yet-
1379 // checkpointed record — the common case under a write/recently-read
1380 // workload — could never be stripped, so eviction reclaimed almost
1381 // nothing under pressure (EVICTOR-RECLAIM-1). A slot with a NULL/
1382 // transient LSN (a deferred-write LN never logged) is NOT
1383 // strippable — its only copy is the in-memory value.
1384 if self.get_lsn(idx) == NULL_LSN {
1385 continue;
1386 }
1387 if let Some(data) = self.entries[idx].data.take() {
1388 freed = freed.saturating_add(data.len());
1389 }
1390 }
1391 freed
1392 }
1393
1394 /// Reconstruct the full key for slot `idx` by prepending the BIN's
1395 /// current prefix to the stored suffix.
1396 ///
1397 /// `IN.getKey(int idx)`.
1398 pub fn get_full_key(&self, idx: usize) -> Option<Vec<u8>> {
1399 if idx >= self.keys.len() {
1400 return None;
1401 }
1402 let suffix = self.keys.get(idx); // T-2
1403 if self.key_prefix.is_empty() {
1404 Some(suffix.to_vec())
1405 } else {
1406 let mut full =
1407 Vec::with_capacity(self.key_prefix.len() + suffix.len());
1408 full.extend_from_slice(&self.key_prefix);
1409 full.extend_from_slice(suffix);
1410 Some(full)
1411 }
1412 }
1413
1414 /// Borrow the stored (post-prefix) suffix at slot `idx` (`INKeyRep.get`).
1415 #[inline]
1416 pub fn get_key(&self, idx: usize) -> &[u8] {
1417 self.keys.get(idx)
1418 }
1419
1420 /// T-2: insert a new slot at `idx` keeping the parallel `entries`, `keys`,
1421 /// and `lsn_rep` arrays in lock step. `suffix` is the post-prefix key.
1422 fn insert_slot(
1423 &mut self,
1424 idx: usize,
1425 suffix: Vec<u8>,
1426 lsn: Lsn,
1427 data: Option<Vec<u8>>,
1428 ) {
1429 self.entries.insert(
1430 idx,
1431 BinEntry {
1432 data,
1433 known_deleted: false,
1434 dirty: true,
1435 expiration_time: 0,
1436 },
1437 );
1438 self.keys.insert(idx, suffix); // T-2
1439 let n = self.entries.len();
1440 self.lsn_rep.insert_shift(idx, n); // T-3
1441 self.lsn_rep.set(idx, lsn, n);
1442 }
1443
1444 /// Decompress a stored suffix back to a full key.
1445 ///
1446 /// `IN.getKey` used from outside: prepend `key_prefix` to
1447 /// `suffix`. If `key_prefix` is empty the suffix *is* the full key.
1448 pub fn decompress_key(&self, suffix: &[u8]) -> Vec<u8> {
1449 if self.key_prefix.is_empty() {
1450 suffix.to_vec()
1451 } else {
1452 let mut full =
1453 Vec::with_capacity(self.key_prefix.len() + suffix.len());
1454 full.extend_from_slice(&self.key_prefix);
1455 full.extend_from_slice(suffix);
1456 full
1457 }
1458 }
1459
1460 /// Strip the current prefix from a full key to obtain the stored suffix.
1461 ///
1462 /// `IN.computeKeySuffix(byte[] prefix, byte[] key)`.
1463 ///
1464 /// # Panics
1465 /// Panics (debug only) if `full_key` does not start with `key_prefix`.
1466 pub fn compress_key(&self, full_key: &[u8]) -> Vec<u8> {
1467 let plen = self.key_prefix.len();
1468 if plen == 0 {
1469 full_key.to_vec()
1470 } else {
1471 debug_assert!(
1472 full_key.starts_with(&self.key_prefix),
1473 "compress_key: key does not start with current prefix"
1474 );
1475 full_key[plen..].to_vec()
1476 }
1477 }
1478
1479 /// Compute the longest common prefix of all full keys currently in this
1480 /// BIN, optionally excluding the entry at `exclude_idx` (used during
1481 /// insertions to ignore the slot that is about to be replaced).
1482 ///
1483 /// Returns an empty `Vec` if the BIN has fewer than 2 entries or if the
1484 /// keys share no common leading bytes.
1485 ///
1486 /// `IN.computeKeyPrefix(int excludeIdx)`.
1487 pub fn compute_key_prefix(&self, exclude_idx: Option<usize>) -> Vec<u8> {
1488 // Need at least 2 entries to find a common prefix.
1489 let n = self.keys.len();
1490 if n < 2 {
1491 return Vec::new();
1492 }
1493
1494 // Pick the first non-excluded index as the seed.
1495 let first_idx = match exclude_idx {
1496 Some(0) => 1,
1497 _ => 0,
1498 };
1499
1500 // The current prefix_len is taken from the seed full key.
1501 let seed_full = match self.get_full_key(first_idx) {
1502 Some(k) => k,
1503 None => return Vec::new(),
1504 };
1505 let mut prefix_len = seed_full.len();
1506
1507 // Compare every other non-excluded entry against the running prefix.
1508 // Iterate all entries (byteOrdered disabled in too).
1509 for i in (first_idx + 1)..n {
1510 if let Some(ex) = exclude_idx
1511 && i == ex
1512 {
1513 continue;
1514 }
1515 let full_key = match self.get_full_key(i) {
1516 Some(k) => k,
1517 None => continue,
1518 };
1519 let new_len =
1520 get_key_prefix_length(&seed_full[..prefix_len], &full_key);
1521 if new_len < prefix_len {
1522 prefix_len = new_len;
1523 }
1524 if prefix_len == 0 {
1525 return Vec::new();
1526 }
1527 }
1528
1529 seed_full[..prefix_len].to_vec()
1530 }
1531
1532 /// Recompute the key prefix from scratch and re-encode every stored suffix.
1533 ///
1534 /// Call this after bulk inserts, splits, or merges.
1535 ///
1536 /// `IN.recalcKeyPrefix()` → `IN.recalcSuffixes(newPrefix, …)`.
1537 pub fn recompute_key_prefix(&mut self) {
1538 let new_prefix = self.compute_key_prefix(None);
1539 self.apply_new_prefix(new_prefix);
1540 }
1541
1542 /// Apply `new_prefix` as the BIN's key prefix, re-encoding all stored
1543 /// suffixes from the old prefix into the new one.
1544 ///
1545 /// This is the Rust.
1546 fn apply_new_prefix(&mut self, new_prefix: Vec<u8>) {
1547 // Reconstruct all full keys (using old prefix), then re-encode with
1548 // the new prefix.
1549 let full_keys: Vec<Vec<u8>> = (0..self.keys.len())
1550 .map(|i| self.get_full_key(i).unwrap_or_default())
1551 .collect();
1552
1553 self.key_prefix = new_prefix;
1554
1555 // T-2: re-encode every suffix into the key rep, then re-attempt
1556 // compaction (a smaller prefix may make all suffixes fit MaxKeySize).
1557 for (i, full_key) in full_keys.into_iter().enumerate() {
1558 let suffix = self.compress_key(&full_key);
1559 self.keys.set(i, suffix);
1560 }
1561 self.keys.compact(self.compact_max_key_length);
1562 }
1563
1564 /// Binary-search this BIN for `full_key` (a full, uncompressed key).
1565 ///
1566 /// The stored suffixes are compared after stripping the current prefix
1567 /// from `full_key`, so the search is done entirely in suffix-space — no
1568 /// heap allocation needed in the happy path.
1569 ///
1570 /// Returns `(idx, exact)` where:
1571 /// - `idx` is the slot index (or insertion point when `exact == false`).
1572 /// - `exact` is `true` when an exact match was found.
1573 ///
1574 /// `IN.findEntry(key, indicateIfDuplicate, exact)`.
1575 pub fn find_entry_compressed(&self, full_key: &[u8]) -> (usize, bool) {
1576 let plen = self.key_prefix.len();
1577 // Check that the key shares the current prefix; if not it cannot be
1578 // present and we return the appropriate insertion point.
1579 if plen > 0
1580 && (full_key.len() < plen
1581 || &full_key[..plen] != self.key_prefix.as_slice())
1582 {
1583 // The key does not share the current prefix.
1584 // Determine insertion point using full-key comparison.
1585 let pos = self.key_partition_point(|s| {
1586 self.decompress_key(s).as_slice() < full_key
1587 });
1588 return (pos, false);
1589 }
1590 let suffix = &full_key[plen..];
1591 // T-2: binary search over the node-level key rep (suffix space).
1592 match self.key_binary_search(suffix) {
1593 Ok(idx) => (idx, true),
1594 Err(idx) => (idx, false),
1595 }
1596 }
1597
1598 /// Binary search the key rep for `suffix` (suffix space, unsigned bytes).
1599 /// Mirrors `Vec::binary_search_by(|e| e.key.cmp(suffix))` over the
1600 /// node-level `KeyRep` (T-2).
1601 #[inline]
1602 fn key_binary_search(&self, suffix: &[u8]) -> Result<usize, usize> {
1603 let mut lo = 0usize;
1604 let mut hi = self.keys.len();
1605 while lo < hi {
1606 let mid = lo + (hi - lo) / 2;
1607 match self.keys.get(mid).cmp(suffix) {
1608 std::cmp::Ordering::Less => lo = mid + 1,
1609 std::cmp::Ordering::Greater => hi = mid,
1610 std::cmp::Ordering::Equal => return Ok(mid),
1611 }
1612 }
1613 Err(lo)
1614 }
1615
1616 /// `slice::partition_point` over the node-level key rep suffixes (T-2):
1617 /// the index of the first slot for which `pred(suffix)` is false.
1618 #[inline]
1619 fn key_partition_point(
1620 &self,
1621 mut pred: impl FnMut(&[u8]) -> bool,
1622 ) -> usize {
1623 let mut lo = 0usize;
1624 let mut hi = self.keys.len();
1625 while lo < hi {
1626 let mid = lo + (hi - lo) / 2;
1627 if pred(self.keys.get(mid)) {
1628 lo = mid + 1;
1629 } else {
1630 hi = mid;
1631 }
1632 }
1633 lo
1634 }
1635
1636 /// Insert or update a full (uncompressed) key in this BIN.
1637 ///
1638 /// After insertion the key prefix is recomputed; if the prefix changes all
1639 /// stored suffixes are re-encoded.
1640 ///
1641 /// Returns `(slot_index, is_new_insert)`.
1642 ///
1643 /// `IN.setKey` / BIN insert path.
1644 pub fn insert_with_prefix(
1645 &mut self,
1646 full_key: Vec<u8>,
1647 lsn: Lsn,
1648 data: Option<Vec<u8>>,
1649 ) -> (usize, bool) {
1650 // Is the current prefix still compatible with this key?
1651 let plen = self.key_prefix.len();
1652 let new_len = if plen > 0 {
1653 get_key_prefix_length(&self.key_prefix, &full_key)
1654 } else {
1655 0
1656 };
1657
1658 // If the new key shrinks the prefix we must re-encode everything first.
1659 if plen > 0 && new_len < plen {
1660 // Compute new prefix considering the incoming key and
1661 // all existing full keys. We pass `None` for exclude_idx because
1662 // the slot for this key does not yet exist.
1663 let mut candidate = self.compute_key_prefix(None);
1664 // Also constrain by the new key itself.
1665 if !candidate.is_empty() {
1666 let cl = get_key_prefix_length(&candidate, &full_key);
1667 candidate.truncate(cl);
1668 } else {
1669 // No existing prefix; try to build one from the new key
1670 // against the existing full keys.
1671 if !self.entries.is_empty()
1672 && let Some(first_full) = self.get_full_key(0)
1673 {
1674 candidate = create_key_prefix(&first_full, &full_key)
1675 .unwrap_or_default();
1676 for i in 1..self.entries.len() {
1677 if candidate.is_empty() {
1678 break;
1679 }
1680 if let Some(fk) = self.get_full_key(i) {
1681 let l = get_key_prefix_length(&candidate, &fk);
1682 candidate.truncate(l);
1683 }
1684 }
1685 }
1686 }
1687 self.apply_new_prefix(candidate);
1688 }
1689
1690 // Compress the new key under the (possibly updated) prefix.
1691 let suffix = self.compress_key(&full_key);
1692
1693 match self.key_binary_search(&suffix) {
1694 Ok(idx) => {
1695 // Key exists — update in place.
1696 self.set_lsn(idx, lsn); // T-3
1697 self.entries[idx].data = data;
1698 // Mark slot dirty: this slot changed since the last full BIN log.
1699 // `IN.setDirtyEntry(idx)`.
1700 self.entries[idx].dirty = true;
1701 (idx, false)
1702 }
1703 Err(idx) => {
1704 // New key — insert in sorted position.
1705 // New slots start dirty: they have never been logged in any BIN.
1706 // `IN.setDirtyEntry(idx)` called after `insertEntry`.
1707 self.insert_slot(idx, suffix, lsn, data);
1708 // After insertion, if there is no prefix yet, try to establish one.
1709 if self.key_prefix.is_empty() && self.entries.len() >= 2 {
1710 self.recompute_key_prefix();
1711 }
1712 (idx, true)
1713 }
1714 }
1715 }
1716
1717 /// Slice-based variant of [`BinStub::insert_with_prefix`] for the recovery redo path.
1718 ///
1719 /// Accepts `key` and `data` as `&[u8]` slices instead of owned `Vec<u8>`,
1720 /// eliminating the intermediate `Vec<u8>` that `redo_ln` would otherwise
1721 /// allocate before crossing the BIN boundary. The compressed suffix and
1722 /// the data bytes are each copied into the `BinEntry` exactly once.
1723 ///
1724 /// Semantics are identical to `insert_with_prefix`:
1725 /// - Updates the slot in place when the key already exists.
1726 /// - Inserts a new sorted entry when absent, recomputing the key prefix.
1727 ///
1728 /// Wave 11-K optimisation (Fix 1).
1729 pub fn insert_with_prefix_slice(
1730 &mut self,
1731 full_key: &[u8],
1732 lsn: Lsn,
1733 data: Option<&[u8]>,
1734 ) -> (usize, bool) {
1735 let plen = self.key_prefix.len();
1736 let new_len = if plen > 0 {
1737 get_key_prefix_length(&self.key_prefix, full_key)
1738 } else {
1739 0
1740 };
1741
1742 if plen > 0 && new_len < plen {
1743 let mut candidate = self.compute_key_prefix(None);
1744 if !candidate.is_empty() {
1745 let cl = get_key_prefix_length(&candidate, full_key);
1746 candidate.truncate(cl);
1747 } else {
1748 if !self.entries.is_empty()
1749 && let Some(first_full) = self.get_full_key(0)
1750 {
1751 candidate = create_key_prefix(&first_full, full_key)
1752 .unwrap_or_default();
1753 for i in 1..self.entries.len() {
1754 if candidate.is_empty() {
1755 break;
1756 }
1757 if let Some(fk) = self.get_full_key(i) {
1758 let l = get_key_prefix_length(&candidate, &fk);
1759 candidate.truncate(l);
1760 }
1761 }
1762 }
1763 }
1764 self.apply_new_prefix(candidate);
1765 }
1766
1767 let suffix = self.compress_key(full_key);
1768
1769 match self.key_binary_search(&suffix) {
1770 Ok(idx) => {
1771 self.set_lsn(idx, lsn); // T-3
1772 self.entries[idx].data = data.map(|d| d.to_vec());
1773 self.entries[idx].dirty = true;
1774 (idx, false)
1775 }
1776 Err(idx) => {
1777 self.insert_slot(idx, suffix, lsn, data.map(|d| d.to_vec()));
1778 if self.key_prefix.is_empty() && self.entries.len() >= 2 {
1779 self.recompute_key_prefix();
1780 }
1781 (idx, true)
1782 }
1783 }
1784 }
1785
1786 /// Returns the number of slots that are marked dirty.
1787 ///
1788 /// `BIN.getNumDirtyEntries()`.
1789 pub fn dirty_count(&self) -> usize {
1790 self.entries.iter().filter(|e| e.dirty).count()
1791 }
1792
1793 /// Decide whether to log this BIN as a delta (true) or a full BIN (false).
1794 ///
1795 /// Faithful port of JE `BIN.shouldLogDelta()` (BIN.java:1892). The
1796 /// decision is COUNT-based (number of would-be delta slots vs a percent of
1797 /// `nEntries`), NOT a dirty-fraction-vs-hardcoded-0.25 heuristic:
1798 ///
1799 /// ```text
1800 /// if (isBINDelta()) { return true; } // already a delta
1801 /// if (isDeltaProhibited()) return false; // prohibit / no prior full
1802 /// numDeltas = getNDeltas();
1803 /// if (numDeltas <= 0) return false; // empty delta is invalid
1804 /// deltaLimit = (getNEntries() * binDeltaPercent) / 100; // INTEGER math
1805 /// return numDeltas <= deltaLimit;
1806 /// ```
1807 ///
1808 /// `numDeltas` (JE `getNDeltas`) is the count of slots that would appear in
1809 /// the delta — i.e. the dirty slots since the last full BIN — which here is
1810 /// `dirty_count()`. `binDeltaPercent` is the CONFIGURABLE `TREE_BIN_DELTA`
1811 /// param (JE `DatabaseImpl.getBinDeltaPercent()`, default 25), threaded in
1812 /// by the checkpointer — NOT a hardcoded constant.
1813 ///
1814 /// `isDeltaProhibited()` (BIN.java:1867) is
1815 /// `getProhibitNextDelta() || isDeferredWriteMode() || lastFullLsn == NULL`.
1816 /// Deferred-write mode is not modelled in the runtime stub; the other two
1817 /// terms are.
1818 ///
1819 /// JE ref: `BIN.shouldLogDelta` (BIN.java:1892), `BIN.isDeltaProhibited`
1820 /// (BIN.java:1867).
1821 pub fn should_log_delta(&self, bin_delta_percent: i32) -> bool {
1822 // Already a delta: re-log as a delta. JE asserts !prohibitNextDelta
1823 // and lastFullLsn != NULL here.
1824 if self.is_delta {
1825 return self.last_full_lsn != NULL_LSN && !self.prohibit_next_delta;
1826 }
1827
1828 // isDeltaProhibited(): cheapest checks first.
1829 if self.prohibit_next_delta || self.last_full_lsn == NULL_LSN {
1830 return false;
1831 }
1832
1833 // numDeltas = getNDeltas(): the dirty slots that would be in the delta.
1834 let num_deltas = self.dirty_count() as i32;
1835
1836 // A delta with zero items is not valid.
1837 if num_deltas <= 0 {
1838 return false;
1839 }
1840
1841 // Configured BinDeltaPercent limit — INTEGER math, exactly as JE.
1842 let delta_limit = (self.entries.len() as i32 * bin_delta_percent) / 100;
1843 num_deltas <= delta_limit
1844 }
1845
1846 /// Comparator-aware binary search: finds `full_key` using `cmp`.
1847 ///
1848 /// Unlike `find_entry_compressed` (which uses suffix-based lexicographic
1849 /// comparison), this decompresses each entry's key to its full form and
1850 /// applies the provided comparator — required for sorted-dup databases
1851 /// where lexicographic suffix comparison would give wrong results when
1852 /// different-length primary keys are in the same BIN.
1853 ///
1854 /// Returns `(idx, exact)`. Does NOT do prefix compression.
1855 ///
1856 /// `IN.findEntry` with btreeComparator active.
1857 pub fn find_entry_cmp(
1858 &self,
1859 full_key: &[u8],
1860 cmp: &dyn Fn(&[u8], &[u8]) -> std::cmp::Ordering,
1861 ) -> (usize, bool) {
1862 // Hot path: avoid per-comparison Vec<u8> allocation.
1863 // When key_prefix is empty the stored suffix IS the full key, so we
1864 // pass the suffix slice directly. When prefix is non-empty we build a
1865 // temporary concatenation only once per comparison using a small
1866 // stack-local Vec that is dropped immediately after the call — this
1867 // still allocates but is limited to O(key_len) bytes per call and
1868 // avoids retaining any heap state between comparisons.
1869 if self.key_prefix.is_empty() {
1870 match self.key_binary_search_by(|s| cmp(s, full_key)) {
1871 Ok(idx) => (idx, true),
1872 Err(idx) => (idx, false),
1873 }
1874 } else {
1875 let prefix = self.key_prefix.as_slice();
1876 match self.key_binary_search_by(|s| {
1877 let mut fk = Vec::with_capacity(prefix.len() + s.len());
1878 fk.extend_from_slice(prefix);
1879 fk.extend_from_slice(s);
1880 cmp(&fk, full_key)
1881 }) {
1882 Ok(idx) => (idx, true),
1883 Err(idx) => (idx, false),
1884 }
1885 }
1886 }
1887
1888 /// Comparator-driven binary search over the node-level key rep (T-2).
1889 /// `cmp(stored_suffix)` returns how the stored slot compares to the
1890 /// search key.
1891 #[inline]
1892 fn key_binary_search_by(
1893 &self,
1894 mut cmp: impl FnMut(&[u8]) -> std::cmp::Ordering,
1895 ) -> Result<usize, usize> {
1896 let mut lo = 0usize;
1897 let mut hi = self.keys.len();
1898 while lo < hi {
1899 let mid = lo + (hi - lo) / 2;
1900 match cmp(self.keys.get(mid)) {
1901 std::cmp::Ordering::Less => lo = mid + 1,
1902 std::cmp::Ordering::Greater => hi = mid,
1903 std::cmp::Ordering::Equal => return Ok(mid),
1904 }
1905 }
1906 Err(lo)
1907 }
1908
1909 /// Returns the LSN of the slot matching `full_key`, if one exists.
1910 ///
1911 /// Used by the recovery LN-redo apply to enforce JE's currency check
1912 /// (`RecoveryManager.redo()` line ~2512): a logged LN is applied only
1913 /// when `logrecLsn > treeLsn`. Returns `None` when the key is absent
1914 /// (always apply). Uses the same lookup variant the matching insert
1915 /// path uses so the comparison is over the right slot.
1916 pub fn redo_slot_lsn(
1917 &self,
1918 full_key: &[u8],
1919 cmp: Option<&dyn Fn(&[u8], &[u8]) -> std::cmp::Ordering>,
1920 key_prefixing: bool,
1921 ) -> Option<Lsn> {
1922 let (idx, found) = match cmp {
1923 Some(c) => self.find_entry_cmp(full_key, c),
1924 None if key_prefixing => self.find_entry_compressed(full_key),
1925 None => {
1926 // insert_raw path: full keys stored verbatim.
1927 match self.key_binary_search(full_key) {
1928 Ok(idx) => (idx, true),
1929 Err(idx) => (idx, false),
1930 }
1931 }
1932 };
1933 if found { Some(self.get_lsn(idx)) } else { None }
1934 }
1935
1936 /// Raw insert (no prefix compression) for databases with
1937 /// `key_prefixing = false`.
1938 ///
1939 /// JE `IN.computeKeyPrefix` returns `null` when
1940 /// `databaseImpl.getKeyPrefixing()` is `false`, so no prefix is ever
1941 /// set on those BINs. Noxu was previously ignoring the flag and always
1942 /// calling `insert_with_prefix`; this method provides the faithful path.
1943 ///
1944 /// The key is stored verbatim (no suffix stripping). An existing
1945 /// `key_prefix` on the BIN is left untouched; callers must ensure it is
1946 /// empty (split_child already guarantees this for new BINs when
1947 /// `key_prefixing = false`).
1948 ///
1949 /// Returns `(slot_index, is_new_insert)`.
1950 ///
1951 /// Ref: `IN.java computeKeyPrefix` ~line 2456,
1952 /// `DatabaseConfig.setKeyPrefixing` / `DatabaseImpl.getKeyPrefixing`.
1953 pub fn insert_raw(
1954 &mut self,
1955 full_key: Vec<u8>,
1956 lsn: Lsn,
1957 data: Option<Vec<u8>>,
1958 ) -> (usize, bool) {
1959 // Binary search on the stored (full) keys.
1960 // When key_prefix is empty entries store full keys directly; for
1961 // key_prefixing=false DBs the prefix is always empty.
1962 match self.key_binary_search(full_key.as_slice()) {
1963 Ok(idx) => {
1964 self.set_lsn(idx, lsn); // T-3
1965 self.entries[idx].data = data;
1966 self.entries[idx].dirty = true;
1967 (idx, false)
1968 }
1969 Err(idx) => {
1970 self.insert_slot(idx, full_key, lsn, data);
1971 (idx, true)
1972 }
1973 }
1974 }
1975
1976 /// Comparator-aware insert: inserts `full_key` into the BIN using `cmp`.
1977 ///
1978 /// Prefix compression is DISABLED: the key is stored as-is. This is
1979 /// intentional for sorted-dup databases where the custom comparator
1980 /// requires full-key access at every comparison.
1981 ///
1982 /// Returns `(slot_index, is_new_insert)`.
1983 ///
1984 pub fn insert_cmp(
1985 &mut self,
1986 full_key: Vec<u8>,
1987 lsn: Lsn,
1988 data: Option<Vec<u8>>,
1989 cmp: &dyn Fn(&[u8], &[u8]) -> std::cmp::Ordering,
1990 ) -> (usize, bool) {
1991 if self.key_prefix.is_empty() {
1992 match self.key_binary_search_by(|s| cmp(s, &full_key)) {
1993 Ok(idx) => {
1994 self.set_lsn(idx, lsn); // T-3
1995 self.entries[idx].data = data;
1996 self.entries[idx].dirty = true;
1997 (idx, false)
1998 }
1999 Err(idx) => {
2000 self.insert_slot(idx, full_key, lsn, data);
2001 (idx, true)
2002 }
2003 }
2004 } else {
2005 let prefix = self.key_prefix.clone();
2006 match self.key_binary_search_by(|s| {
2007 let mut fk = Vec::with_capacity(prefix.len() + s.len());
2008 fk.extend_from_slice(&prefix);
2009 fk.extend_from_slice(s);
2010 cmp(&fk, &full_key)
2011 }) {
2012 Ok(idx) => {
2013 // Key exists — update in place.
2014 self.set_lsn(idx, lsn); // T-3
2015 self.entries[idx].data = data;
2016 self.entries[idx].dirty = true;
2017 (idx, false)
2018 }
2019 Err(idx) => {
2020 // New key — insert at sorted position (no prefix compression).
2021 self.insert_slot(idx, full_key, lsn, data);
2022 (idx, true)
2023 }
2024 }
2025 }
2026 }
2027
2028 /// Comparator-aware delete: removes `full_key` from the BIN using `cmp`.
2029 ///
2030 /// Returns `true` if the entry was found and removed.
2031 pub fn delete_cmp(
2032 &mut self,
2033 full_key: &[u8],
2034 cmp: &dyn Fn(&[u8], &[u8]) -> std::cmp::Ordering,
2035 ) -> bool {
2036 let result = if self.key_prefix.is_empty() {
2037 self.key_binary_search_by(|s| cmp(s, full_key))
2038 } else {
2039 let prefix = self.key_prefix.clone();
2040 self.key_binary_search_by(|s| {
2041 let mut fk = Vec::with_capacity(prefix.len() + s.len());
2042 fk.extend_from_slice(&prefix);
2043 fk.extend_from_slice(s);
2044 cmp(&fk, full_key)
2045 })
2046 };
2047 match result {
2048 Ok(idx) => {
2049 self.entries.remove(idx);
2050 self.keys.remove(idx); // T-2
2051 self.lsn_rep.remove_shift(idx); // T-3
2052 self.dirty = true;
2053 true
2054 }
2055 Err(_) => false,
2056 }
2057 }
2058
2059 /// Serialise ALL entries (full BIN write).
2060 ///
2061 /// Format (per slot): key_len(u32BE) | key | lsn(u64BE) |
2062 /// has_data(u8) | data_len(u32BE) | data | known_deleted(u8)
2063 ///
2064 /// Prepended by: node_id(u64BE) | num_entries(u32BE).
2065 ///
2066 /// `BIN.writeToLog()` (non-delta path).
2067 pub fn serialize_full(&self) -> Vec<u8> {
2068 let mut buf = Vec::new();
2069 buf.extend_from_slice(&self.node_id.to_be_bytes());
2070 buf.extend_from_slice(&(self.entries.len() as u32).to_be_bytes());
2071 for i in 0..self.entries.len() {
2072 let full_key = self.get_full_key(i).unwrap_or_default();
2073 buf.extend_from_slice(&(full_key.len() as u32).to_be_bytes());
2074 buf.extend_from_slice(&full_key);
2075 let lsn = self.get_lsn(i); // T-3
2076 let e = &self.entries[i];
2077 buf.extend_from_slice(&lsn.as_u64().to_be_bytes());
2078 if let Some(d) = &e.data {
2079 buf.push(1u8);
2080 buf.extend_from_slice(&(d.len() as u32).to_be_bytes());
2081 buf.extend_from_slice(d);
2082 } else {
2083 buf.push(0u8);
2084 }
2085 buf.push(e.known_deleted as u8);
2086 }
2087 buf
2088 }
2089
2090 /// Serialise only dirty slots (BIN-delta write).
2091 ///
2092 /// Format (per dirty slot): slot_idx(u32BE) | key_len(u32BE) | key |
2093 /// lsn(u64BE) | has_data(u8) | data_len(u32BE) | data | known_deleted(u8)
2094 ///
2095 /// Prepended by: node_id(u64BE) | num_dirty(u32BE).
2096 ///
2097 /// `BIN.writeToLog()` (delta path).
2098 pub fn serialize_delta(&self) -> Vec<u8> {
2099 let dirty: Vec<usize> = (0..self.entries.len())
2100 .filter(|&i| self.entries[i].dirty)
2101 .collect();
2102 let mut buf = Vec::new();
2103 buf.extend_from_slice(&self.node_id.to_be_bytes());
2104 buf.extend_from_slice(&(dirty.len() as u32).to_be_bytes());
2105 for idx in dirty {
2106 buf.extend_from_slice(&(idx as u32).to_be_bytes());
2107 let full_key = self.get_full_key(idx).unwrap_or_default();
2108 buf.extend_from_slice(&(full_key.len() as u32).to_be_bytes());
2109 buf.extend_from_slice(&full_key);
2110 let lsn = self.get_lsn(idx); // T-3
2111 let e = &self.entries[idx];
2112 buf.extend_from_slice(&lsn.as_u64().to_be_bytes());
2113 if let Some(d) = &e.data {
2114 buf.push(1u8);
2115 buf.extend_from_slice(&(d.len() as u32).to_be_bytes());
2116 buf.extend_from_slice(d);
2117 } else {
2118 buf.push(0u8);
2119 }
2120 buf.push(e.known_deleted as u8);
2121 }
2122 buf
2123 }
2124
2125 /// Deserialise a full BIN from the bytes produced by `serialize_full()`.
2126 ///
2127 /// Returns a `BinStub` with all entries populated and all slots marked
2128 /// clean (they are already on disk at `last_full_lsn`). Returns `None`
2129 /// if the byte slice is malformed.
2130 ///
2131 /// `INLogEntry.readEntry()` / `IN.readFromLog()` (non-delta).
2132 pub fn deserialize_full(bytes: &[u8]) -> Option<BinStub> {
2133 if bytes.len() < 12 {
2134 return None;
2135 }
2136 let node_id = u64::from_be_bytes(bytes[0..8].try_into().ok()?);
2137 let num_entries =
2138 u32::from_be_bytes(bytes[8..12].try_into().ok()?) as usize;
2139 let mut pos = 12usize;
2140 let mut entries = Vec::with_capacity(num_entries);
2141 let mut lsns: Vec<Lsn> = Vec::with_capacity(num_entries);
2142 let mut keys: Vec<Vec<u8>> = Vec::with_capacity(num_entries); // T-2
2143 for _ in 0..num_entries {
2144 // key_len(u32BE) | key | lsn(u64BE) | has_data(u8) [| data_len(u32BE) | data] | known_deleted(u8)
2145 if pos + 4 > bytes.len() {
2146 return None;
2147 }
2148 let key_len =
2149 u32::from_be_bytes(bytes[pos..pos + 4].try_into().ok()?)
2150 as usize;
2151 pos += 4;
2152 if pos + key_len > bytes.len() {
2153 return None;
2154 }
2155 let key = bytes[pos..pos + key_len].to_vec();
2156 pos += key_len;
2157 if pos + 8 > bytes.len() {
2158 return None;
2159 }
2160 let lsn = Lsn::from_u64(u64::from_be_bytes(
2161 bytes[pos..pos + 8].try_into().ok()?,
2162 ));
2163 pos += 8;
2164 if pos + 1 > bytes.len() {
2165 return None;
2166 }
2167 let has_data = bytes[pos] != 0;
2168 pos += 1;
2169 let data = if has_data {
2170 if pos + 4 > bytes.len() {
2171 return None;
2172 }
2173 let data_len =
2174 u32::from_be_bytes(bytes[pos..pos + 4].try_into().ok()?)
2175 as usize;
2176 pos += 4;
2177 if pos + data_len > bytes.len() {
2178 return None;
2179 }
2180 let d = bytes[pos..pos + data_len].to_vec();
2181 pos += data_len;
2182 Some(d)
2183 } else {
2184 None
2185 };
2186 if pos + 1 > bytes.len() {
2187 return None;
2188 }
2189 let known_deleted = bytes[pos] != 0;
2190 pos += 1;
2191 entries.push(BinEntry {
2192 data,
2193 known_deleted,
2194 dirty: false, // freshly loaded from log — clean
2195 expiration_time: 0,
2196 });
2197 keys.push(key); // T-2 (full keys; recompute_key_prefix compresses)
2198 lsns.push(lsn); // T-3
2199 }
2200 // Keys stored in the serialized format are full (uncompressed) keys.
2201 // Re-establish the key prefix after loading so that memory use and
2202 // search performance match an in-memory BIN.
2203 // `IN.readFromLog()` → key prefix is part of the wire
2204 // format in the; in Noxu we store full keys and recompute on load.
2205 let mut bin = BinStub {
2206 node_id,
2207 level: BIN_LEVEL,
2208 entries,
2209 key_prefix: Vec::new(),
2210 dirty: false,
2211 is_delta: false,
2212 last_full_lsn: NULL_LSN, // caller sets this to the logged LSN
2213 last_delta_lsn: NULL_LSN,
2214 generation: 0,
2215 parent: None,
2216 expiration_in_hours: true,
2217 cursor_count: 0,
2218 prohibit_next_delta: false,
2219 lsn_rep: LsnRep::from_lsns(&lsns), // T-3
2220 keys: KeyRep::from_keys(keys), // T-2 (full keys, no prefix yet)
2221 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
2222 };
2223 // Recompute key prefix from the full keys just loaded.
2224 // `IN.recalcKeyPrefix()` called after materializing from log.
2225 if bin.entries.len() >= 2 {
2226 bin.recompute_key_prefix();
2227 } else {
2228 // Even a single-slot BIN should attempt compaction.
2229 bin.keys.compact(bin.compact_max_key_length);
2230 }
2231 Some(bin)
2232 }
2233
2234 /// Deserialise a BIN delta from the bytes produced by `serialize_delta()`.
2235 ///
2236 /// **DO NOT USE for BIN reconstruction.** This helper writes full
2237 /// (uncompressed) keys directly into slots without recomputing the BIN
2238 /// key prefix, so on a prefix-compressed BIN it corrupts the slot keys and
2239 /// breaks the sorted-suffix invariant. It is NOT wired into any live path.
2240 /// The correct delta-reconstruction path is
2241 /// `mutate_to_full_bin` → `apply_delta_to_bin` → `insert_with_prefix`,
2242 /// which recomputes the prefix. This function is retained only for the
2243 /// raw byte-format round-trip and must not be used to reconstitute a BIN.
2244 /// Tracked for removal — see the v3.x review synthesis (storage C-2).
2245 ///
2246 /// Returns `None` if `delta_bytes` is malformed.
2247 pub fn apply_delta(base: &mut BinStub, delta_bytes: &[u8]) -> Option<()> {
2248 if delta_bytes.len() < 12 {
2249 return None;
2250 }
2251 // node_id(u64BE) — must match base
2252 let _node_id = u64::from_be_bytes(delta_bytes[0..8].try_into().ok()?);
2253 let num_dirty =
2254 u32::from_be_bytes(delta_bytes[8..12].try_into().ok()?) as usize;
2255 let mut pos = 12usize;
2256 for _ in 0..num_dirty {
2257 // slot_idx(u32BE) | key_len(u32BE) | key | lsn(u64BE) | has_data(u8) [| data_len | data] | known_deleted(u8)
2258 if pos + 4 > delta_bytes.len() {
2259 return None;
2260 }
2261 let slot_idx =
2262 u32::from_be_bytes(delta_bytes[pos..pos + 4].try_into().ok()?)
2263 as usize;
2264 pos += 4;
2265 if pos + 4 > delta_bytes.len() {
2266 return None;
2267 }
2268 let key_len =
2269 u32::from_be_bytes(delta_bytes[pos..pos + 4].try_into().ok()?)
2270 as usize;
2271 pos += 4;
2272 if pos + key_len > delta_bytes.len() {
2273 return None;
2274 }
2275 let key = delta_bytes[pos..pos + key_len].to_vec();
2276 pos += key_len;
2277 if pos + 8 > delta_bytes.len() {
2278 return None;
2279 }
2280 let lsn = Lsn::from_u64(u64::from_be_bytes(
2281 delta_bytes[pos..pos + 8].try_into().ok()?,
2282 ));
2283 pos += 8;
2284 if pos + 1 > delta_bytes.len() {
2285 return None;
2286 }
2287 let has_data = delta_bytes[pos] != 0;
2288 pos += 1;
2289 let data = if has_data {
2290 if pos + 4 > delta_bytes.len() {
2291 return None;
2292 }
2293 let data_len = u32::from_be_bytes(
2294 delta_bytes[pos..pos + 4].try_into().ok()?,
2295 ) as usize;
2296 pos += 4;
2297 if pos + data_len > delta_bytes.len() {
2298 return None;
2299 }
2300 let d = delta_bytes[pos..pos + data_len].to_vec();
2301 pos += data_len;
2302 Some(d)
2303 } else {
2304 None
2305 };
2306 if pos + 1 > delta_bytes.len() {
2307 return None;
2308 }
2309 let known_deleted = delta_bytes[pos] != 0;
2310 pos += 1;
2311
2312 // Apply to base: update existing slot or insert new one.
2313 if slot_idx < base.entries.len() {
2314 base.keys.set(slot_idx, key); // T-2
2315 base.set_lsn(slot_idx, lsn); // T-3
2316 base.entries[slot_idx].data = data;
2317 base.entries[slot_idx].known_deleted = known_deleted;
2318 base.entries[slot_idx].dirty = false;
2319 } else {
2320 // Slot index beyond current length — append.
2321 base.entries.push(BinEntry {
2322 data,
2323 known_deleted,
2324 dirty: false,
2325 expiration_time: 0,
2326 });
2327 let n = base.entries.len();
2328 base.keys.insert(n - 1, key); // T-2
2329 base.lsn_rep.set(n - 1, lsn, n); // T-3
2330 }
2331 }
2332 Some(())
2333 }
2334
2335 /// Clear per-slot dirty flags and record `logged_at` as the LSN at which
2336 /// this BIN was last fully logged.
2337 ///
2338 /// Called by the checkpoint path after a successful full-BIN log write.
2339 /// `BIN.afterLog()` / `BIN.setLastFullLsn()`.
2340 pub fn clear_dirty_after_full_log(&mut self, logged_at: Lsn) {
2341 for e in &mut self.entries {
2342 e.dirty = false;
2343 }
2344 self.last_full_lsn = logged_at;
2345 self.dirty = false;
2346 // A full BIN captures all current state, so the delta-chain bound is
2347 // cleared: the next log may once again be a delta.
2348 // JE `IN.afterLog` clears the prohibit flag after a full log
2349 // (IN.java:5557 `bin.setProhibitNextDelta(false)`).
2350 self.prohibit_next_delta = false;
2351 }
2352
2353 /// Clear per-slot dirty flags after a successful delta log write.
2354 ///
2355 /// `last_full_lsn` is NOT updated — the full LSN only changes after a
2356 /// full BIN write.
2357 /// `BIN.afterLog()` (delta path).
2358 pub fn clear_dirty_after_delta_log(&mut self) {
2359 for e in &mut self.entries {
2360 e.dirty = false;
2361 }
2362 self.dirty = false;
2363 }
2364}
2365
2366impl TreeNode {
2367 /// Returns true if this is a BIN (bottom internal node).
2368 pub fn is_bin(&self) -> bool {
2369 matches!(self, TreeNode::Bottom(_))
2370 }
2371
2372 /// Returns the level of this node.
2373 pub fn level(&self) -> i32 {
2374 match self {
2375 TreeNode::Internal(n) => n.level,
2376 TreeNode::Bottom(b) => b.level,
2377 }
2378 }
2379
2380 /// Returns the node id of this node.
2381 pub fn node_id(&self) -> u64 {
2382 match self {
2383 TreeNode::Internal(n) => n.node_id,
2384 TreeNode::Bottom(b) => b.node_id,
2385 }
2386 }
2387
2388 /// Faithful in-memory heap footprint of this node, in bytes.
2389 ///
2390 /// JE `IN.getBudgetedMemorySize()` (IN.java) returns the running
2391 /// `inMemorySize` that `MemoryBudget` tracks for the node: the fixed
2392 /// IN/BIN struct overhead plus, per slot, the fixed entry overhead and the
2393 /// variable key (and embedded-LN data for BINs) bytes. This is the single
2394 /// source of truth for both the live tree accounting and the evictor's
2395 /// detach credit (EV-13) — keeping it on `TreeNode` avoids the formula
2396 /// drifting between `noxu-tree` and `noxu-evictor`.
2397 ///
2398 /// Rust has a fixed struct layout (unlike JE's `Sizeof`-measured JVM
2399 /// constants) so `size_of` is exact for the fixed overheads; the variable
2400 /// part mirrors JE's per-slot `entryKeys`/embedded-data accounting.
2401 pub fn budgeted_memory_size(&self) -> u64 {
2402 use std::mem::size_of;
2403 match self {
2404 TreeNode::Bottom(b) => {
2405 (size_of::<BinStub>()
2406 + b.entries.len() * size_of::<BinEntry>()
2407 + b.key_prefix.len()
2408 + b.keys.memory_size() // T-2: node-level key rep bytes
2409 + b.lsn_rep.memory_size() // T-3: node-level LSN rep bytes
2410 + b.entries
2411 .iter()
2412 .map(|e| {
2413 e.data.as_ref().map(|d| d.len()).unwrap_or(0)
2414 })
2415 .sum::<usize>()) as u64
2416 }
2417 TreeNode::Internal(n) => {
2418 (size_of::<InNodeStub>()
2419 + n.entries.len() * size_of::<InEntry>()
2420 + n.targets.memory_size()
2421 + n.entries.iter().map(|e| e.key.len()).sum::<usize>())
2422 as u64
2423 }
2424 }
2425 }
2426
2427 /// Binary search for a key in this node.
2428 ///
2429 /// For BIN nodes the search is prefix-aware: if the BIN has a key prefix,
2430 /// `key` (a full, uncompressed key) is compared against stored suffixes
2431 /// after stripping the prefix.
2432 /// `IN.findEntry(key, indicateIfDuplicate, exact)`.
2433 ///
2434 /// Returns index with EXACT_MATCH flag set if exact match found.
2435 /// If exact is false, returns insertion point.
2436 pub fn find_entry(&self, key: &[u8], _indicator: bool, exact: bool) -> i32 {
2437 match self {
2438 TreeNode::Internal(n) => {
2439 let result = n
2440 .entries
2441 .binary_search_by(|entry| entry.key.as_slice().cmp(key));
2442 match result {
2443 Ok(idx) => (idx as i32) | EXACT_MATCH,
2444 Err(idx) => {
2445 if exact {
2446 -1
2447 } else {
2448 // Floor (not insertion point): the child slot to
2449 // descend into is the largest entry ≤ key. Slot 0
2450 // is the leftmost child, so a key below every
2451 // separator floors to 0. (St-H5: previously
2452 // returned the insertion point `idx`, which routes
2453 // one child too far right.)
2454 (idx as i32 - 1).max(0)
2455 }
2456 }
2457 }
2458 }
2459 TreeNode::Bottom(b) => {
2460 // Use prefix-aware search: the stored key is a suffix when
2461 // key_prefix is non-empty.
2462 let (idx, found) = b.find_entry_compressed(key);
2463 if found {
2464 (idx as i32) | EXACT_MATCH
2465 } else if exact {
2466 -1
2467 } else {
2468 idx as i32
2469 }
2470 }
2471 }
2472 }
2473
2474 /// Gets the number of entries in this node.
2475 pub fn get_n_entries(&self) -> usize {
2476 match self {
2477 TreeNode::Internal(n) => n.entries.len(),
2478 TreeNode::Bottom(b) => b.entries.len(),
2479 }
2480 }
2481
2482 // ========================================================================
2483 // Dirty flag
2484 // ========================================================================
2485
2486 /// Returns true if this node has been modified since last checkpoint.
2487 ///
2488 /// `IN.getDirty()`.
2489 pub fn is_dirty(&self) -> bool {
2490 match self {
2491 TreeNode::Internal(n) => n.dirty,
2492 TreeNode::Bottom(b) => b.dirty,
2493 }
2494 }
2495
2496 /// Sets or clears the dirty flag on this node.
2497 ///
2498 /// `IN.setDirty(boolean dirty)`.
2499 pub fn set_dirty(&mut self, dirty: bool) {
2500 match self {
2501 TreeNode::Internal(n) => n.dirty = dirty,
2502 TreeNode::Bottom(b) => b.dirty = dirty,
2503 }
2504 }
2505
2506 // ========================================================================
2507 // LRU generation
2508 // ========================================================================
2509
2510 /// Returns the LRU generation counter.
2511 ///
2512 /// `IN.getGeneration()`.
2513 pub fn get_generation(&self) -> u64 {
2514 match self {
2515 TreeNode::Internal(n) => n.generation,
2516 TreeNode::Bottom(b) => b.generation,
2517 }
2518 }
2519
2520 /// Sets the LRU generation counter.
2521 ///
2522 /// `IN.setGeneration(long gen)`.
2523 pub fn set_generation(&mut self, r#gen: u64) {
2524 match self {
2525 TreeNode::Internal(n) => n.generation = r#gen,
2526 TreeNode::Bottom(b) => b.generation = r#gen,
2527 }
2528 }
2529
2530 // ========================================================================
2531 // Parent pointer
2532 // ========================================================================
2533
2534 /// Returns a clone of the weak parent pointer, if any.
2535 pub fn get_parent(&self) -> Option<Weak<RwLock<TreeNode>>> {
2536 match self {
2537 TreeNode::Internal(n) => n.parent.clone(),
2538 TreeNode::Bottom(b) => b.parent.clone(),
2539 }
2540 }
2541
2542 /// Sets the weak parent pointer on this node.
2543 pub fn set_parent(&mut self, parent: Option<Weak<RwLock<TreeNode>>>) {
2544 match self {
2545 TreeNode::Internal(n) => n.parent = parent,
2546 TreeNode::Bottom(b) => b.parent = parent,
2547 }
2548 }
2549
2550 // ========================================================================
2551 // Log serialization
2552 // ========================================================================
2553
2554 /// Estimates the serialized byte size of this node for log/checkpoint use.
2555 ///
2556 /// `IN.getLogSize()` — Noxu-native serialization format.
2557 ///
2558 /// Format (big-endian):
2559 /// - node_id : 8 bytes
2560 /// - level : 4 bytes
2561 /// - n_entries : 4 bytes
2562 /// - dirty : 1 byte
2563 /// - For each entry:
2564 /// - key_len : 2 bytes
2565 /// - key : key_len bytes
2566 /// - lsn : 8 bytes
2567 pub fn log_size(&self) -> usize {
2568 // Fixed header: node_id(8) + level(4) + n_entries(4) + dirty(1)
2569 let mut size: usize = 8 + 4 + 4 + 1;
2570 match self {
2571 TreeNode::Internal(n) => {
2572 for entry in &n.entries {
2573 size += 2 + entry.key.len() + 8; // key_len + key + lsn
2574 }
2575 }
2576 TreeNode::Bottom(b) => {
2577 for i in 0..b.entries.len() {
2578 size += 2 + b.get_key(i).len() + 8; // key_len + key + lsn
2579 }
2580 }
2581 }
2582 size
2583 }
2584
2585 /// Serializes this node to bytes for log writing.
2586 ///
2587 /// `IN.writeToLog(ByteBuffer logBuffer)` — Noxu-native
2588 /// format matching `log_size()`.
2589 pub fn write_to_bytes(&self) -> Vec<u8> {
2590 let mut buf = Vec::with_capacity(self.log_size());
2591 match self {
2592 TreeNode::Internal(n) => {
2593 buf.extend_from_slice(&n.node_id.to_be_bytes());
2594 buf.extend_from_slice(&n.level.to_be_bytes());
2595 buf.extend_from_slice(&(n.entries.len() as u32).to_be_bytes());
2596 buf.push(n.dirty as u8);
2597 for (i, entry) in n.entries.iter().enumerate() {
2598 buf.extend_from_slice(
2599 &(entry.key.len() as u16).to_be_bytes(),
2600 );
2601 buf.extend_from_slice(&entry.key);
2602 buf.extend_from_slice(&n.get_lsn(i).as_u64().to_be_bytes());
2603 }
2604 }
2605 TreeNode::Bottom(b) => {
2606 buf.extend_from_slice(&b.node_id.to_be_bytes());
2607 buf.extend_from_slice(&b.level.to_be_bytes());
2608 buf.extend_from_slice(&(b.entries.len() as u32).to_be_bytes());
2609 buf.push(b.dirty as u8);
2610 for i in 0..b.entries.len() {
2611 let key = b.get_key(i);
2612 buf.extend_from_slice(&(key.len() as u16).to_be_bytes());
2613 buf.extend_from_slice(key);
2614 buf.extend_from_slice(&b.get_lsn(i).as_u64().to_be_bytes());
2615 }
2616 }
2617 }
2618 buf
2619 }
2620}
2621
2622/// Internal helper used during splits to carry entries of either node kind.
2623///
2624/// `BinStub` and `InNodeStub` store different entry types, so we need a
2625/// common wrapper to pass split slices around without code duplication.
2626enum SplitEntries {
2627 /// Upper-IN entries plus the parallel resident-child pointers (one per
2628 /// entry; `None` when the child is not cached) and the parallel per-slot
2629 /// LSNs (T-3: LSNs travel with their slots on a split, just like JE
2630 /// `IN.split` copies `entryLsnByteArray`/`entryLsnLongArray`).
2631 Internal(Vec<InEntry>, Vec<Option<ChildArc>>, Vec<Lsn>),
2632 /// BIN entries (metadata only) plus the parallel per-slot LSNs and the
2633 /// parallel FULL keys (T-2: keys live in the node-level `KeyRep`, not in
2634 /// `BinEntry`, so they travel as a separate `Vec<Vec<u8>>` of full keys
2635 /// through the split — the new BINs recompute their prefix from these).
2636 Bottom(Vec<BinEntry>, Vec<Lsn>, Vec<Vec<u8>>),
2637}
2638
2639impl SplitEntries {
2640 /// Returns the number of entries.
2641 fn len(&self) -> usize {
2642 match self {
2643 SplitEntries::Internal(v, _, _) => v.len(),
2644 SplitEntries::Bottom(v, _, _) => v.len(),
2645 }
2646 }
2647
2648 /// Returns the key at `index` as a slice.
2649 fn get_key(&self, index: usize) -> &[u8] {
2650 match self {
2651 SplitEntries::Internal(v, _, _) => v[index].key.as_slice(),
2652 SplitEntries::Bottom(_, _, k) => k[index].as_slice(),
2653 }
2654 }
2655
2656 /// Returns a sub-range `[lo, hi)` as a new `SplitEntries`.
2657 fn slice(&self, lo: usize, hi: usize) -> Self {
2658 match self {
2659 SplitEntries::Internal(v, c, l) => SplitEntries::Internal(
2660 v[lo..hi].to_vec(),
2661 c[lo..hi].to_vec(),
2662 l[lo..hi].to_vec(),
2663 ),
2664 SplitEntries::Bottom(v, l, k) => SplitEntries::Bottom(
2665 v[lo..hi].to_vec(),
2666 l[lo..hi].to_vec(),
2667 k[lo..hi].to_vec(),
2668 ),
2669 }
2670 }
2671}
2672
2673/// Tri-state outcome from one attempt at
2674/// `Tree::get_adjacent_bin_attempt`.
2675///
2676/// Distinguishes "the tree genuinely has no BIN in the requested
2677/// direction" (→ propagate as end-of-iteration) from "the path we
2678/// captured was invalidated by a concurrent split" (→ caller
2679/// retries from root). This split is necessary because the cursor
2680/// translates a `None` from `get_adjacent_bin` into
2681/// `OperationStatus::NotFound`, which is indistinguishable from a
2682/// real end-of-tree.
2683#[derive(Debug)]
2684enum AdjacentBinOutcome {
2685 /// A BIN was found in the requested direction. T-3: each slot carries its
2686 /// `Lsn` alongside the `BinEntry` (the LSN lives in the node's packed
2687 /// `LsnRep`, not in `BinEntry`, so the scan snapshot pairs them).
2688 Found(Vec<(BinEntry, Lsn, Vec<u8>)>),
2689 /// The tree genuinely has no BIN in the requested direction.
2690 NoAdjacent,
2691 /// A concurrent split invalidated our captured path; the
2692 /// caller should retry from root.
2693 SplitRaceRetry,
2694}
2695
2696/// Split hint for the `splitSpecial` heuristic.
2697///
2698/// JE `Tree.forceSplit` tracks `allLeftSideDescent` / `allRightSideDescent`
2699/// (true if **every** routing decision during the top-down descent followed
2700/// the leftmost / rightmost child). At split time, when one of those flags
2701/// is set, `IN.splitSpecial` forces the split index to 1 (left side) or
2702/// `nEntries - 1` (right side) instead of `nEntries / 2`.
2703///
2704/// Effect: for sequential-append workloads the left BIN stays near-full
2705/// after every split (only one entry migrates to the new sibling), cutting
2706/// the split count roughly in half and reducing write amplification.
2707///
2708/// Ref: `IN.java splitSpecial` ~line 4129, `Tree.java forceSplit` ~line 1907.
2709#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2710enum SplitHint {
2711 /// Normal midpoint split (`n_entries / 2`).
2712 Normal,
2713 /// Key was at position 0 on every level of descent.
2714 /// → `split_index = 1` so left node keeps all but the first entry.
2715 AllLeft,
2716 /// Key was at the rightmost position on every level of descent.
2717 /// → `split_index = n_entries - 1` so left node keeps almost everything.
2718 AllRight,
2719}
2720
2721impl Tree {
2722 /// Creates a new empty tree.
2723 ///
2724 /// Constructor.
2725 pub fn new(database_id: u64, max_entries_per_node: usize) -> Self {
2726 Tree {
2727 database_id,
2728 max_entries_per_node,
2729 root: RwLock::new(None),
2730 root_latch: SharedLatch::new(LatchContext::new("TreeRoot"), false),
2731 root_log_lsn: RwLock::new(noxu_util::NULL_LSN),
2732 root_splits: AtomicU64::new(0),
2733 relatches_required: AtomicU64::new(0),
2734 key_comparator: None,
2735 memory_counter: None,
2736 in_list_listener: None,
2737 log_manager: None,
2738 redo_capacity_hint: 0,
2739 key_prefixing: false, // JE default: KEY_PREFIXING_DEFAULT = false
2740 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH, // T-5
2741 }
2742 }
2743
2744 /// Installs a shared memory counter for evictor / MemoryBudget feedback.
2745 ///
2746 /// → `env.getMemoryBudget().updateTreeMemoryUsage(delta)`
2747 ///. The counter is updated on every BIN entry insert/delete.
2748 pub fn set_memory_counter(&mut self, counter: Arc<AtomicI64>) {
2749 self.memory_counter = Some(counter);
2750 }
2751
2752 /// Installs the [`InListListener`] (the evictor) so node add/access/remove
2753 /// feed the LRU lists. JE: `INList` registration that feeds
2754 /// `Evictor.addBack`/`moveBack`/`remove`.
2755 pub fn set_in_list_listener(&mut self, listener: Arc<dyn InListListener>) {
2756 self.in_list_listener = Some(listener);
2757 }
2758
2759 /// Installs the [`noxu_log::LogManager`] so an evicted root IN can be
2760 /// re-materialized from its persisted LSN on the next access (EV-14).
2761 ///
2762 /// JE: the tree reaches the log through `database.getEnv().getLogManager()`
2763 /// for `ChildReference.fetchTarget`. Noxu installs it directly.
2764 pub fn set_log_manager(&mut self, lm: Arc<noxu_log::LogManager>) {
2765 self.log_manager = Some(lm);
2766 }
2767
2768 /// Drops this tree's `Arc<LogManager>` reference (EV-14 teardown).
2769 ///
2770 /// The env's `Drop` calls this on every tree it owns so the
2771 /// `Tree -> Arc<LogManager> -> Arc<FileManager>` chain cannot keep the
2772 /// FileManager (and its on-disk exclusive lock) alive past environment
2773 /// close. After this the tree can no longer re-fetch an evicted root
2774 /// from the log — which is correct, because the environment is shutting
2775 /// down and the tree is about to be dropped.
2776 pub fn clear_log_manager(&mut self) {
2777 self.log_manager = None;
2778 }
2779
2780 /// T-5: set the compact-key threshold (`TREE_COMPACT_MAX_KEY_LENGTH` /
2781 /// `IN.getCompactMaxKeyLength`). New BINs created by this tree inherit it;
2782 /// `<= 0` disables the compact key rep. Default 16.
2783 pub fn set_compact_max_key_length(&mut self, len: i32) {
2784 self.compact_max_key_length = len;
2785 }
2786
2787 /// Notify the listener that a node became resident (JE `Evictor.addBack`).
2788 #[inline]
2789 fn note_added(&self, node_id: u64) {
2790 if let Some(l) = &self.in_list_listener {
2791 l.note_ins_added(node_id);
2792 }
2793 }
2794
2795 /// Notify the listener that a resident node was accessed
2796 /// (JE `Evictor.moveBack` — LRU touch).
2797 #[inline]
2798 fn note_accessed(&self, node_id: u64) {
2799 if let Some(l) = &self.in_list_listener {
2800 l.note_ins_accessed(node_id);
2801 }
2802 }
2803
2804 /// Notify the listener that a node was removed (JE `Evictor.remove`).
2805 #[inline]
2806 fn note_removed(&self, node_id: u64) {
2807 if let Some(l) = &self.in_list_listener {
2808 l.note_ins_removed(node_id);
2809 }
2810 }
2811
2812 /// Creates a new empty tree with a custom key comparator.
2813 ///
2814 /// Used for sorted-duplicate databases where keys are two-part
2815 /// composite keys that require a custom ordering function.
2816 ///
2817 /// Constructor with `btreeComparator` parameter.
2818 pub fn new_with_comparator(
2819 database_id: u64,
2820 max_entries_per_node: usize,
2821 comparator: KeyComparatorFn,
2822 ) -> Self {
2823 Tree {
2824 database_id,
2825 max_entries_per_node,
2826 root: RwLock::new(None),
2827 root_latch: SharedLatch::new(LatchContext::new("TreeRoot"), false),
2828 root_log_lsn: RwLock::new(noxu_util::NULL_LSN),
2829 root_splits: AtomicU64::new(0),
2830 relatches_required: AtomicU64::new(0),
2831 key_comparator: Some(comparator),
2832 memory_counter: None,
2833 in_list_listener: None,
2834 log_manager: None,
2835 redo_capacity_hint: 0,
2836 key_prefixing: false,
2837 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH, // T-5
2838 }
2839 }
2840
2841 /// Sets the key-prefixing flag.
2842 ///
2843 /// When `true`, BIN key-prefix compression is enabled: shared leading
2844 /// bytes are factored out of each slot's key. When `false` (the
2845 /// default), keys are stored verbatim — matching JE
2846 /// `DatabaseConfig.setKeyPrefixing(false)` / `IN.computeKeyPrefix`
2847 /// returning `null`.
2848 ///
2849 /// Ref: `IN.java computeKeyPrefix` ~line 2456.
2850 pub fn set_key_prefixing(&mut self, enabled: bool) {
2851 self.key_prefixing = enabled;
2852 }
2853
2854 /// Sets the key comparator, replacing any existing one.
2855 pub fn set_comparator(&mut self, comparator: KeyComparatorFn) {
2856 self.key_comparator = Some(comparator);
2857 }
2858
2859 /// Store a capacity hint used by `redo_insert` when it creates the first
2860 /// BIN for this tree (the first-key path).
2861 ///
2862 /// The first BIN's `entries` Vec is pre-allocated with
2863 /// `capacity.min(max_entries_per_node)` slots, eliminating the
2864 /// Vec-resize doubling cycle (1 → 2 → 4 → … → cap) that would
2865 /// otherwise occur during the redo loop.
2866 ///
2867 /// Call once before the redo loop. Has no effect on `insert` (the
2868 /// normal, non-recovery path).
2869 ///
2870 /// Wave 11-K optimisation (Fix 3).
2871 pub fn hint_redo_capacity(&mut self, capacity: usize) {
2872 self.redo_capacity_hint = capacity;
2873 }
2874
2875 /// Returns the current redo capacity hint (0 = no hint set).
2876 pub fn get_redo_capacity_hint(&self) -> usize {
2877 self.redo_capacity_hint
2878 }
2879
2880 /// Takes the key comparator out of this tree (leaving None).
2881 pub fn take_comparator(&mut self) -> Option<KeyComparatorFn> {
2882 self.key_comparator.take()
2883 }
2884
2885 /// Returns a reference to the key comparator, if configured.
2886 ///
2887 /// Used by `CursorImpl::find_bin_for_key` (R4 fix) so the cursor's own
2888 /// IN-level descent uses the same comparator-aware floor slot as the
2889 /// tree's own search paths. Mirrors JE `DatabaseImpl.getKeyComparator()`.
2890 pub fn get_comparator(&self) -> Option<&KeyComparatorFn> {
2891 self.key_comparator.as_ref()
2892 }
2893
2894 /// Returns the key comparator if set, or performs lexicographic comparison.
2895 #[inline]
2896 fn key_cmp(&self, a: &[u8], b: &[u8]) -> std::cmp::Ordering {
2897 match &self.key_comparator {
2898 Some(cmp) => cmp(a, b),
2899 None => a.cmp(b),
2900 }
2901 }
2902
2903 /// Floor child slot index for descending an internal node: the largest
2904 /// slot whose key is ≤ `key`. Slot 0 carries a virtual −∞ key (always
2905 /// qualifies); `entries[1..]` are sorted ascending, so this binary-searches
2906 /// the partition point instead of an O(n) linear walk (St-H4). Uses
2907 /// `key_cmp` so a configured custom comparator is honoured on every descent
2908 /// path. Returns 0 for an empty/single-slot node.
2909 fn upper_in_floor_index(&self, entries: &[InEntry], key: &[u8]) -> usize {
2910 if entries.len() <= 1 {
2911 return 0;
2912 }
2913 entries[1..].partition_point(|e| {
2914 self.key_cmp(e.key.as_slice(), key) != std::cmp::Ordering::Greater
2915 })
2916 }
2917
2918 /// Returns true if the tree has no root (is empty).
2919 pub fn is_empty(&self) -> bool {
2920 self.root.read().is_none()
2921 }
2922
2923 /// Sets the root of the tree.
2924 ///
2925 /// Must hold root_latch exclusively before calling.
2926 pub fn set_root(&self, node: TreeNode) {
2927 *self.root.write() = Some(Arc::new(RwLock::new(node)));
2928 }
2929
2930 /// Returns the root Arc, if any.
2931 ///
2932 /// Returns a cloned `Arc` rather than a reference so the caller does not
2933 /// hold the inner `RwLock` guard.
2934 ///
2935 /// EV-14: when the in-memory root has been evicted (`evict_root`) but a
2936 /// persisted version exists (`root_log_lsn` set), this re-materializes it
2937 /// from the log before returning — the faithful equivalent of JE
2938 /// `Tree.getRootIN` always calling `root.fetchTarget(...)`. Returns
2939 /// `None` only for a genuinely empty tree (no resident root and no
2940 /// persisted root LSN).
2941 pub fn get_root(&self) -> Option<Arc<RwLock<TreeNode>>> {
2942 if let Some(r) = self.root.read().clone() {
2943 return Some(r);
2944 }
2945 // Root not resident: re-fetch it from `root_log_lsn` if one exists
2946 // (a no-op returning None when the tree was never populated).
2947 self.fetch_root_from_log()
2948 }
2949
2950 /// Returns the database ID.
2951 pub fn get_database_id(&self) -> u64 {
2952 self.database_id
2953 }
2954
2955 /// Count the total number of live (non-deleted) entries across all BINs.
2956 ///
2957 /// Used by `DatabaseImpl::set_recovered_tree()` to initialise the
2958 /// per-database `entry_count` AtomicU64 after recovery replays the log.
2959 pub fn count_entries(&self) -> u64 {
2960 let mut total = 0u64;
2961 if let Some(root) = self.get_root() {
2962 Self::count_entries_recursive(&root, &mut total);
2963 }
2964 total
2965 }
2966
2967 /// DBI-14: collect every live `(full_key, data, lsn)` triple in physical
2968 /// (left-to-right) order. Used by `resort_under_comparator` to rebuild a
2969 /// tree whose slots were laid out in byte order (e.g. by recovery redo,
2970 /// which has no access to the application comparator) under the real
2971 /// configured comparator.
2972 fn collect_all_entries(&self) -> Vec<(Vec<u8>, Vec<u8>, Lsn)> {
2973 let mut out = Vec::new();
2974 if let Some(root) = self.get_root() {
2975 Self::collect_all_entries_recursive(&root, &mut out);
2976 }
2977 out
2978 }
2979
2980 fn collect_all_entries_recursive(
2981 node_arc: &Arc<RwLock<TreeNode>>,
2982 out: &mut Vec<(Vec<u8>, Vec<u8>, Lsn)>,
2983 ) {
2984 let guard = node_arc.read();
2985 match &*guard {
2986 TreeNode::Bottom(b) => {
2987 for i in 0..b.entries.len() {
2988 if b.entries[i].known_deleted {
2989 continue;
2990 }
2991 if let Some(fk) = b.get_full_key(i) {
2992 let data =
2993 b.entries[i].data.clone().unwrap_or_default();
2994 out.push((fk, data, b.get_lsn(i)));
2995 }
2996 }
2997 }
2998 TreeNode::Internal(n) => {
2999 let children: Vec<Arc<RwLock<TreeNode>>> =
3000 n.resident_children();
3001 drop(guard);
3002 for child in &children {
3003 Self::collect_all_entries_recursive(child, out);
3004 }
3005 }
3006 }
3007 }
3008
3009 /// DBI-14: rebuild this tree so that its on-disk byte-ordered slot layout
3010 /// is re-sorted under the currently-configured key comparator.
3011 ///
3012 /// Recovery redo (`redo_insert`) has no access to the application's
3013 /// comparator function — only the persisted identity — so it lays keys
3014 /// out in unsigned-byte order. After `set_recovered_tree` attaches the
3015 /// real comparator, the slots must be re-sorted, or comparator-driven
3016 /// searches would binary-search a tree ordered by the wrong relation.
3017 ///
3018 /// No-op when no comparator is configured (byte order already matches the
3019 /// recovered layout) or when the tree is empty. Mirrors the effect of
3020 /// JE reconstructing the comparator at open and the tree always having
3021 /// been built under it.
3022 pub fn resort_under_comparator(&self) {
3023 if self.key_comparator.is_none() {
3024 return;
3025 }
3026 let entries = self.collect_all_entries();
3027 if entries.is_empty() {
3028 return;
3029 }
3030 // Drop the current root; re-insert every entry through the normal
3031 // comparator-aware insert path so the new layout obeys the comparator.
3032 *self.root.write() = None;
3033 *self.root_log_lsn.write() = noxu_util::NULL_LSN;
3034 for (key, data, lsn) in entries {
3035 // Best-effort: a failed re-insert would be a tree-structure bug;
3036 // surface it loudly in debug builds.
3037 let r = self.insert(key, data, lsn);
3038 debug_assert!(
3039 r.is_ok(),
3040 "resort_under_comparator: re-insert failed: {r:?}"
3041 );
3042 }
3043 }
3044
3045 fn count_entries_recursive(
3046 node_arc: &Arc<RwLock<TreeNode>>,
3047 total: &mut u64,
3048 ) {
3049 let guard = node_arc.read();
3050 match &*guard {
3051 TreeNode::Bottom(b) => {
3052 // Count only live (non-known_deleted) entries.
3053 *total += b.entries.iter().filter(|e| !e.known_deleted).count()
3054 as u64;
3055 }
3056 TreeNode::Internal(n) => {
3057 let children: Vec<Arc<RwLock<TreeNode>>> =
3058 n.resident_children();
3059 drop(guard);
3060 for child in children {
3061 Self::count_entries_recursive(&child, total);
3062 }
3063 }
3064 }
3065 }
3066
3067 /// Sum the real in-memory heap footprint of every resident node in the
3068 /// tree (DBI-23 oracle / reconciliation), in bytes.
3069 ///
3070 /// Walks all resident IN/BIN nodes and adds each node's
3071 /// `budgeted_memory_size` (JE `IN.getBudgetedMemorySize`). This is the
3072 /// authoritative "real heap" figure the incrementally-maintained
3073 /// `memory_counter` is meant to approximate; an engine can call it to
3074 /// reconcile counter drift, and the DBI-23 test uses it as the oracle the
3075 /// live counter must stay within tolerance of.
3076 pub fn total_budgeted_memory(&self) -> u64 {
3077 let mut total = 0u64;
3078 if let Some(root) = self.get_root() {
3079 Self::total_budgeted_memory_recursive(&root, &mut total);
3080 }
3081 total
3082 }
3083
3084 fn total_budgeted_memory_recursive(
3085 node_arc: &Arc<RwLock<TreeNode>>,
3086 total: &mut u64,
3087 ) {
3088 let guard = node_arc.read();
3089 *total += guard.budgeted_memory_size();
3090 if let TreeNode::Internal(n) = &*guard {
3091 let children: Vec<Arc<RwLock<TreeNode>>> = n.resident_children();
3092 drop(guard);
3093 for child in children {
3094 Self::total_budgeted_memory_recursive(&child, total);
3095 }
3096 }
3097 }
3098
3099 /// Search for a BIN that should contain the given key.
3100 ///
3101 /// This is the core tree traversal operation. It walks from root to BIN
3102 /// using latch-coupling (acquire child latch, then release parent latch).
3103 ///
3104 /// . Descends the tree until a BIN is
3105 /// reached, following the child pointer at the slot whose key is the
3106 /// largest key <= the search key (the "LTE" rule). Slot 0 in every upper
3107 /// IN carries a virtual key (-infinity) so any search key routes through
3108 /// it when all real keys are larger.
3109 ///
3110 /// Returns a SearchResult indicating where the key is or should be.
3111 /// Returns None if tree is empty.
3112 pub fn search(&self, key: &[u8]) -> Option<SearchResult> {
3113 let root = self.get_root()?;
3114
3115 // Hand-over-hand latch coupling for the descent. At each level we
3116 // hold a `parking_lot::ArcRwLockReadGuard` on the current node;
3117 // before dropping it, we acquire the child's read guard via
3118 // `Arc::read_arc`. This keeps a continuous chain of read locks
3119 // along the descent path so that no concurrent `split_child(parent,
3120 // …)` can run on a node we are about to enter — `split_child` takes
3121 // `parent.write()` to install the new sibling, and that write
3122 // blocks while we hold `parent.read()`. Without this, the prior
3123 // pattern (capture child Arc, drop parent guard, then take child
3124 // read lock) left a window in which a split could relocate the
3125 // child entries: a search for a key that should have ended up in
3126 // the new sibling would instead reach the (now left-half) child
3127 // and return a false `NotFound`.
3128 //
3129 // `read_arc()` returns `ArcRwLockReadGuard<RawRwLock, TreeNode>`
3130 // — a guard that owns its own Arc reference, so it has no
3131 // borrow lifetime and can be held across loop iterations and
3132 // assignment.
3133 let mut guard: NodeArcReadGuard = root.read_arc();
3134
3135 loop {
3136 if guard.is_bin() {
3137 // JE: IN.fetchTarget / CursorImpl access moves the reached
3138 // BIN toward the hot end of the evictor's LRU list
3139 // (Evictor.moveBack). A freshly split BIN that has not yet
3140 // been registered is added here (moveBack is add-if-absent).
3141 if let TreeNode::Bottom(bin) = &*guard {
3142 self.note_accessed(bin.node_id);
3143 }
3144 // Reached a BIN: final key lookup within the same guard.
3145 // Use indicate_if_duplicate=true so an exact match sets
3146 // EXACT_MATCH in the return value. Guard against -1 (not
3147 // found): -1i32 has all bits set, so the naive
3148 // `index & EXACT_MATCH != 0` check would incorrectly report
3149 // an exact match for a missing key.
3150 let (found, raw_idx) = match &*guard {
3151 TreeNode::Bottom(bin) => match &self.key_comparator {
3152 Some(cmp) => {
3153 let (idx, exact) =
3154 bin.find_entry_cmp(key, cmp.as_ref());
3155 (exact, idx as i32)
3156 }
3157 None => {
3158 let index = guard.find_entry(key, true, true);
3159 let exact =
3160 index >= 0 && (index & EXACT_MATCH != 0);
3161 (exact, index & 0xFFFF)
3162 }
3163 },
3164 _ => {
3165 let index = guard.find_entry(key, true, true);
3166 let exact = index >= 0 && (index & EXACT_MATCH != 0);
3167 (exact, index & 0xFFFF)
3168 }
3169 };
3170 // CursorImpl.isProbablyExpired(): if an exact match
3171 // was found, check whether the entry's TTL has already elapsed.
3172 // If it has, treat the slot as not found so callers skip it.
3173 //
3174 // TREE-F1: also treat a known_deleted slot as ABSENT on an
3175 // exact lookup, mirroring the tail of IN.findEntry
3176 // (IN.java:3197): `if (ret >= 0 && exact &&
3177 // isEntryKnownDeleted(ret & 0xffff)) return -1;`. KD slots
3178 // legitimately exist in live BINs during BIN-delta
3179 // reconstitution until the compressor reclaims them.
3180 let found = if found {
3181 if let TreeNode::Bottom(bin) = &*guard {
3182 let idx = (raw_idx & 0x7FFF) as usize;
3183 bin.slot_is_live(idx)
3184 } else {
3185 found
3186 }
3187 } else {
3188 found
3189 };
3190 return Some(SearchResult::with_values(found, raw_idx, false));
3191 }
3192
3193 // Upper IN: find the child slot with the largest key <= search
3194 // key, and capture the child Arc WHILE HOLDING the guard.
3195 // Slot 0 has a virtual key that compares as -infinity.
3196 let parent_arc = NodeArcReadGuard::rwlock(&guard).clone();
3197 let next_arc = match &*guard {
3198 TreeNode::Internal(n) => {
3199 if n.entries.is_empty() {
3200 return None;
3201 }
3202 // Walk forward as long as entry.key <= key, starting
3203 // from slot 0 (which always qualifies because its key
3204 // is the virtual -infinity key).
3205 let idx = self.upper_in_floor_index(&n.entries, key);
3206 match n.get_child(idx) {
3207 // Resident child: keep the hand-over-hand fast path.
3208 Some(c) => {
3209 let next_guard = c.read_arc();
3210 drop(guard);
3211 guard = next_guard;
3212 continue;
3213 }
3214 // EV-14/EV-13: child evicted — re-fetch it from its
3215 // slot LSN (JE ChildReference.fetchTarget). Must
3216 // drop the parent read guard to upgrade to a write
3217 // latch inside child_at_or_fetch.
3218 None => idx,
3219 }
3220 }
3221 TreeNode::Bottom(_) => {
3222 unreachable!("is_bin() returned false above")
3223 }
3224 };
3225 drop(guard);
3226 let child = self.child_at_or_fetch(&parent_arc, next_arc)?;
3227 guard = child.read_arc();
3228 }
3229 }
3230
3231 /// Combined search-and-fetch: descend once to the BIN and return the
3232 /// slot's data together with a reference to the BIN arc.
3233 ///
3234 /// Replaces the previous three-descent sequence on the `Database::get`
3235 /// hot path:
3236 /// 1. `Tree::search` — existence check only.
3237 /// 2. `CursorImpl::get_data_from_tree` — re-descended to fetch data.
3238 /// 3. `CursorImpl::find_bin_for_key` — re-descended for BIN pinning.
3239 ///
3240 /// One descent now does all three jobs. At the BIN level it uses the
3241 /// existing binary-search helper `find_entry_compressed` instead of the
3242 /// O(n) `iter().find()` used by `get_data_from_tree`.
3243 ///
3244 /// Returns `None` only when the tree is empty. Otherwise returns
3245 /// `Some(SlotFetch)` — callers must inspect `SlotFetch::found` to
3246 /// determine whether the key was present. The BIN read-guard is released
3247 /// before this method returns so callers may safely call `lock_ln`
3248 /// (which may block) without holding any tree latch.
3249 ///
3250 /// Wave-11-I — see the 2026 review.
3251 pub fn search_with_data(&self, key: &[u8]) -> Option<SlotFetch> {
3252 let root = self.get_root()?;
3253 let mut guard: NodeArcReadGuard = root.read_arc();
3254
3255 loop {
3256 if guard.is_bin() {
3257 // Capture the BIN Arc before inspecting entries.
3258 let bin_arc = NodeArcReadGuard::rwlock(&guard).clone();
3259
3260 let (found, data, lsn, slot_index) = match &*guard {
3261 TreeNode::Bottom(bin) => {
3262 let (idx, exact) = match &self.key_comparator {
3263 Some(cmp) => bin.find_entry_cmp(key, cmp.as_ref()),
3264 None => bin.find_entry_compressed(key),
3265 };
3266 if exact {
3267 // TREE-F1: a slot is reported as found only when
3268 // live (not known_deleted, not TTL-expired) — the
3269 // same predicate used by Tree::search and the
3270 // cursor scan. Mirrors IN.findEntry (IN.java:3197)
3271 // and CursorImpl.isProbablyExpired.
3272 if bin.slot_is_live(idx) {
3273 let lsn = bin.get_lsn(idx); // T-3
3274 let e = &bin.entries[idx];
3275 (true, e.data.clone(), lsn.as_u64(), idx)
3276 } else {
3277 (false, None, 0u64, 0)
3278 }
3279 } else {
3280 (false, None, 0u64, 0)
3281 }
3282 }
3283 _ => (false, None, 0u64, 0),
3284 };
3285 // Release the BIN read guard before returning so the caller
3286 // can call lock_ln (which may block) without holding a latch.
3287 drop(guard);
3288 return Some(SlotFetch {
3289 found,
3290 data,
3291 lsn,
3292 slot_index,
3293 bin_arc,
3294 });
3295 }
3296
3297 // Upper IN: same hand-over-hand descent as `Tree::search`.
3298 let parent_arc = NodeArcReadGuard::rwlock(&guard).clone();
3299 let next_idx = match &*guard {
3300 TreeNode::Internal(n) => {
3301 if n.entries.is_empty() {
3302 return None;
3303 }
3304 // Slot 0 = virtual −∞; walk forward while entry.key ≤ key.
3305 let idx = self.upper_in_floor_index(&n.entries, key);
3306 match n.get_child(idx) {
3307 Some(c) => {
3308 let next_guard = c.read_arc();
3309 drop(guard);
3310 guard = next_guard;
3311 continue;
3312 }
3313 // EV-14/EV-13: re-fetch an evicted child from its LSN.
3314 None => idx,
3315 }
3316 }
3317 TreeNode::Bottom(_) => {
3318 unreachable!("is_bin() returned false above")
3319 }
3320 };
3321 drop(guard);
3322 let child = self.child_at_or_fetch(&parent_arc, next_idx)?;
3323 guard = child.read_arc();
3324 }
3325 }
3326
3327 /// Sets the expiration time (in absolute hours since Unix epoch) for an
3328 /// existing key's BIN slot.
3329 ///
3330 /// Returns `true` if the key was found and updated, `false` otherwise.
3331 ///
3332 /// Used by `Database::put_with_options()` to apply per-record TTL.
3333 /// `IN.entryExpiration` / `BIN.expirationInHours` path.
3334 pub fn update_key_expiration(
3335 &self,
3336 key: &[u8],
3337 expiration_hours: u32,
3338 ) -> bool {
3339 let root = match self.get_root() {
3340 Some(r) => r,
3341 None => return false,
3342 };
3343 // Hand-over-hand latch coupling for the descent. At the BIN we
3344 // need a write lock; we drop our read lock first and take the
3345 // write lock under the protection of the *outer* parent's read
3346 // lock (held by the previous loop iteration's guard). For the
3347 // first iteration there is no outer parent, but no `split_child`
3348 // can run on the root itself in that single-level case because
3349 // root splits go through `split_root_if_needed` which holds
3350 // `self.root.write()`. So the worst case is that the root is
3351 // promoted from a single BIN to a level-2 IN between our read
3352 // detect and our write — handled by the `is_bin` re-check
3353 // inside the write lock.
3354 //
3355 // We retry the descent up to a small bound to absorb the rare
3356 // case where a concurrent split moved this key into the new
3357 // sibling between the read-chain release and the write-lock
3358 // acquisition. Without the retry, the sole caller
3359 // (`Database::put_with_options`) would silently lose the TTL
3360 // for the affected key. Three attempts is generous: each
3361 // retry only races a single split and splits are infrequent.
3362 for _ in 0..3 {
3363 let mut guard: NodeArcReadGuard = root.read_arc();
3364 let bin_arc;
3365 loop {
3366 if guard.is_bin() {
3367 bin_arc = NodeArcReadGuard::rwlock(&guard).clone();
3368 drop(guard);
3369 break;
3370 }
3371 let next_arc = match &*guard {
3372 TreeNode::Internal(n) => {
3373 if n.entries.is_empty() {
3374 return false;
3375 }
3376 let idx = self.upper_in_floor_index(&n.entries, key);
3377 match n.get_child(idx) {
3378 Some(c) => c,
3379 None => return false,
3380 }
3381 }
3382 TreeNode::Bottom(_) => unreachable!(),
3383 };
3384 let next_guard = next_arc.read_arc();
3385 drop(guard);
3386 guard = next_guard;
3387 }
3388
3389 // Now take the write lock on the BIN we descended to.
3390 let mut wguard = bin_arc.write();
3391 if let TreeNode::Bottom(bin) = &mut *wguard {
3392 let slot = if let Some(cmp) = &self.key_comparator {
3393 let (idx, exact) = bin.find_entry_cmp(key, cmp.as_ref());
3394 if exact { Some(idx) } else { None }
3395 } else {
3396 let (idx, exact) = bin.find_entry_compressed(key);
3397 if exact { Some(idx) } else { None }
3398 };
3399 if let Some(slot_idx) = slot
3400 && let Some(entry) = bin.entries.get_mut(slot_idx)
3401 {
3402 entry.expiration_time = expiration_hours;
3403 bin.expiration_in_hours = true;
3404 bin.dirty = true;
3405 return true;
3406 }
3407 }
3408 // Key not in this BIN — either it was never present or a
3409 // concurrent split moved it. Retry the descent; at most a
3410 // few iterations are needed to follow the key into its new
3411 // BIN.
3412 }
3413 false
3414 }
3415
3416 /// Returns the key and data of the first BIN entry at or after `key`.
3417 ///
3418 /// Descends with the tree's key comparator (same path as `search()`), then
3419 /// within the BIN finds the first slot whose stored key >= `key` using the
3420 /// comparator. Returns `None` if every entry in the tree is < `key`.
3421 ///
3422 /// Used by sorted-duplicate cursor `search(Set)` to position at the first
3423 /// (key, data) pair whose two-part key >= `lower_bound(primary_key)`.
3424 ///
3425 /// → BIN scan path.
3426 pub fn first_entry_at_or_after(
3427 &self,
3428 key: &[u8],
3429 ) -> Option<(Vec<u8>, Vec<u8>, u64)> {
3430 // Hand-over-hand latch coupling — see Tree::search for the
3431 // detailed rationale on why this closes a reader-vs-splitter
3432 // race window.
3433 let mut guard: NodeArcReadGuard = self.get_root()?.read_arc();
3434
3435 loop {
3436 if guard.is_bin() {
3437 let result = match &*guard {
3438 TreeNode::Bottom(bin) => {
3439 let (mut idx, _exact) = match &self.key_comparator {
3440 Some(cmp) => bin.find_entry_cmp(key, cmp.as_ref()),
3441 None => bin.find_entry_compressed(key),
3442 };
3443 // TREE-F1: skip non-live slots (known_deleted /
3444 // TTL-expired) at/after the floor index, mirroring the
3445 // cursor getNext skip (CursorImpl.java:2062-2064).
3446 while idx < bin.entries.len() && !bin.slot_is_live(idx)
3447 {
3448 idx += 1;
3449 }
3450 if idx < bin.entries.len() {
3451 let full_key =
3452 bin.get_full_key(idx).unwrap_or_default();
3453 let data = bin.entries[idx]
3454 .data
3455 .clone()
3456 .unwrap_or_default();
3457 let lsn = bin.get_lsn(idx).as_u64(); // T-3
3458 Some((full_key, data, lsn))
3459 } else {
3460 None
3461 }
3462 }
3463 _ => None,
3464 };
3465 return result;
3466 }
3467
3468 // Upper IN: same descent as search().
3469 let parent_arc = NodeArcReadGuard::rwlock(&guard).clone();
3470 let next_idx = match &*guard {
3471 TreeNode::Internal(n) => {
3472 if n.entries.is_empty() {
3473 return None;
3474 }
3475 let idx = self.upper_in_floor_index(&n.entries, key);
3476 match n.get_child(idx) {
3477 Some(c) => {
3478 let next_guard = c.read_arc();
3479 drop(guard);
3480 guard = next_guard;
3481 continue;
3482 }
3483 None => idx, // EV-14/EV-13: re-fetch below.
3484 }
3485 }
3486 TreeNode::Bottom(_) => unreachable!(),
3487 };
3488 drop(guard);
3489 let child = self.child_at_or_fetch(&parent_arc, next_idx)?;
3490 guard = child.read_arc();
3491 }
3492 }
3493
3494 /// Like [`Tree::first_entry_at_or_after`] but also returns the BIN node
3495 /// (so callers may pin it) and the entry's slot index inside that
3496 /// BIN.
3497 ///
3498 /// Wave 11-N (Bug 2): `CursorImpl::search_dup` previously stored
3499 /// `current_index = 0` after a sorted-dup `Search`, which broke the
3500 /// fast-path of `retrieve_next` (and the slow path's
3501 /// `next_index = current_index + 1` arithmetic) for any primary
3502 /// that was not the first slot of its BIN. This helper hands back
3503 /// the real index so the cursor can be positioned correctly.
3504 ///
3505 /// CC-2 fix: uses the same `read_arc()` hand-over-hand latch coupling
3506 /// as every other descent method (`search`, `first_entry_at_or_after`,
3507 /// `get_first_node`, `get_adjacent_bin_attempt`). The original
3508 /// implementation did `arc.read().is_bin()` (lock acquired and released)
3509 /// then a SECOND `arc.read()` on the next line — a gap in which a
3510 /// concurrent split can promote the node (BIN→upper IN) or move the
3511 /// sought key to a new sibling, yielding a false "not found" for an
3512 /// existing key. Mirrors JE `Tree.searchSubTree` / `Tree.search`
3513 /// which hold the latch across the `is_bin()` test and the subsequent
3514 /// entry lookup.
3515 pub fn first_entry_at_or_after_with_index(
3516 &self,
3517 key: &[u8],
3518 ) -> Option<(
3519 Vec<u8>,
3520 Vec<u8>,
3521 usize,
3522 u64,
3523 std::sync::Arc<crate::NodeRwLock<TreeNode>>,
3524 )> {
3525 // Hand-over-hand latch coupling — identical strategy to
3526 // first_entry_at_or_after; the guard is held continuously across
3527 // is_bin() and the subsequent entry lookup so no split can
3528 // restructure the path between the two observations.
3529 let mut guard: NodeArcReadGuard = self.get_root()?.read_arc();
3530 loop {
3531 if guard.is_bin() {
3532 if let TreeNode::Bottom(bin) = &*guard {
3533 let (idx, _exact) = match &self.key_comparator {
3534 Some(cmp) => bin.find_entry_cmp(key, cmp.as_ref()),
3535 None => bin.find_entry_compressed(key),
3536 };
3537 // TREE-F1: skip non-live slots (known_deleted /
3538 // TTL-expired) at/after the floor index
3539 // (CursorImpl.java:2062-2064).
3540 let mut idx = idx;
3541 while idx < bin.entries.len() && !bin.slot_is_live(idx) {
3542 idx += 1;
3543 }
3544 if idx < bin.entries.len() {
3545 let full_key =
3546 bin.get_full_key(idx).unwrap_or_default();
3547 let data =
3548 bin.entries[idx].data.clone().unwrap_or_default();
3549 let lsn = bin.get_lsn(idx).as_u64(); // T-3
3550 // Obtain the Arc for the BIN node the guard came from.
3551 // `ArcRwLockReadGuard::rwlock()` returns the backing Arc.
3552 let bin_arc = NodeArcReadGuard::rwlock(&guard).clone();
3553 return Some((full_key, data, idx, lsn, bin_arc));
3554 } else {
3555 return None;
3556 }
3557 }
3558 return None;
3559 }
3560
3561 // Upper IN: descend as in first_entry_at_or_after / search.
3562 let parent_arc = NodeArcReadGuard::rwlock(&guard).clone();
3563 let next_idx = match &*guard {
3564 TreeNode::Internal(n) => {
3565 if n.entries.is_empty() {
3566 return None;
3567 }
3568 let idx = self.upper_in_floor_index(&n.entries, key);
3569 match n.get_child(idx) {
3570 Some(c) => {
3571 let next_guard = c.read_arc();
3572 drop(guard);
3573 guard = next_guard;
3574 continue;
3575 }
3576 None => idx, // EV-14/EV-13: re-fetch below.
3577 }
3578 }
3579 TreeNode::Bottom(_) => unreachable!(),
3580 };
3581 drop(guard);
3582 let child = self.child_at_or_fetch(&parent_arc, next_idx)?;
3583 guard = child.read_arc();
3584 }
3585 }
3586
3587 /// Insert a key/data pair into the tree.
3588 ///
3589 /// . Handles the root-is-null case by
3590 /// creating a two-level tree (upper IN + BIN) per initialisation path,
3591 /// then delegates to `insert_recursive` which performs preemptive splitting
3592 /// as it descends.
3593 ///
3594 /// Returns Ok(true) if this was a new insert, Ok(false) if it was an update.
3595 pub fn insert(
3596 &self,
3597 key: Vec<u8>,
3598 data: Vec<u8>,
3599 lsn: Lsn,
3600 ) -> Result<bool, TreeError> {
3601 // Save sizes before potentially moving key/data — needed for memory tracking.
3602 let key_len = key.len();
3603 let data_len = data.len();
3604
3605 // First-key path. We MUST hold the write lock while testing
3606 // root.is_none() and replacing the root, otherwise N threads can all
3607 // observe an empty tree, each build a fresh single-entry root, and
3608 // the last writer's `*self.root.write() = Some(...)` silently
3609 // discards the others' inserts. (Reproducer:
3610 // xa_protocol_test::test_concurrent_independent_xids — 8 threads
3611 // each inserting their own key into an empty tree lost ~30% of
3612 // inserts before this lock change.)
3613 {
3614 let mut root_guard = self.root.write();
3615 if root_guard.is_none() {
3616 let bin_node_id = generate_node_id();
3617 let root_node_id = generate_node_id();
3618 let bin = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
3619 node_id: bin_node_id,
3620 level: BIN_LEVEL,
3621 entries: vec![BinEntry {
3622 data: Some(data),
3623 known_deleted: false,
3624 dirty: false,
3625 expiration_time: 0,
3626 }],
3627 key_prefix: Vec::new(), // single entry — no common prefix yet
3628 dirty: true,
3629 is_delta: false,
3630 last_full_lsn: NULL_LSN,
3631 last_delta_lsn: NULL_LSN,
3632 generation: 0,
3633 parent: None, // set below after root_in is created
3634 // St-H6: use true to match the engine-wide invariant that
3635 // every BIN which may hold TTL entries uses hours granularity
3636 // (JE BIN.java default; matches tree.rs:980 and read_from_log).
3637 expiration_in_hours: true,
3638 cursor_count: 0,
3639 prohibit_next_delta: false,
3640 lsn_rep: LsnRep::from_lsns(&[lsn]),
3641 keys: KeyRep::from_keys(vec![key]), // T-2
3642 compact_max_key_length: self.compact_max_key_length,
3643 })));
3644
3645 // Upper IN at level 2; slot 0 uses an empty key (virtual root key).
3646 let root_arc =
3647 Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
3648 node_id: root_node_id,
3649 level: MAIN_LEVEL | 2,
3650 entries: vec![InEntry {
3651 key: vec![], // virtual key for slot 0 in upper IN
3652 }],
3653 // T-4: the single resident child at slot 0.
3654 targets: TargetRep::Sparse(vec![(0, bin.clone())]),
3655 dirty: true,
3656 generation: 0,
3657 parent: None,
3658 lsn_rep: LsnRep::from_lsns(&[lsn]),
3659 })));
3660
3661 // Wire the BIN's parent pointer back to the root IN.
3662 {
3663 let mut g = bin.write();
3664 g.set_parent(Some(Arc::downgrade(&root_arc)));
3665 }
3666
3667 *root_guard = Some(root_arc);
3668
3669 // JE: IN.fetchTarget / initial tree build registers the new
3670 // resident nodes with the evictor (Evictor.addBack).
3671 self.note_added(root_node_id);
3672 self.note_added(bin_node_id);
3673
3674 // Count the first entry.
3675 if let Some(counter) = &self.memory_counter {
3676 let delta =
3677 (key_len + data_len + BIN_ENTRY_OVERHEAD) as i64;
3678 counter.fetch_add(delta, Ordering::Relaxed);
3679 }
3680 return Ok(true);
3681 }
3682 // Another thread initialized the root while we were waiting for
3683 // the write lock; fall through and insert into the existing tree.
3684 }
3685
3686 // Check whether the root itself needs to be split before descending.
3687 // Tree.searchSplitsAllowed(): if rootIN.needsSplitting()
3688 // call splitRoot first.
3689 self.split_root_if_needed(lsn)?;
3690
3691 // Recursively insert, splitting children proactively as we descend
3692 // (forceSplit / searchSplitsAllowed pattern).
3693 let root_arc = self.get_root().unwrap();
3694 let result = Self::insert_recursive(
3695 &root_arc,
3696 key,
3697 data,
3698 lsn,
3699 self.max_entries_per_node,
3700 self.key_comparator.as_ref(),
3701 self.key_prefixing,
3702 self.in_list_listener.as_ref(),
3703 )?;
3704
3705 // Update the memory counter for new inserts.
3706 // IN.updateMemorySize(delta) → MemoryBudget.updateTreeMemoryUsage(delta).
3707 // LN_OVERHEAD = 48 bytes (approximate fixed overhead per entry).
3708 if result && let Some(counter) = &self.memory_counter {
3709 let delta = (key_len + data_len + BIN_ENTRY_OVERHEAD) as i64;
3710 counter.fetch_add(delta, Ordering::Relaxed);
3711 }
3712
3713 Ok(result)
3714 }
3715
3716 /// Recovery-redo variant of [`Tree::insert`] that accepts `&[u8]` slices.
3717 ///
3718 /// Eliminates the two intermediate `Vec<u8>` allocations that the normal
3719 /// insert path requires at the `redo_ln` call site (one for the key, one
3720 /// for the data). The compressed key suffix and the data bytes are each
3721 /// materialised into their `BinEntry` slots exactly once.
3722 ///
3723 /// Semantics are identical to `insert`:
3724 /// - Updates the existing slot when the key is already present.
3725 /// - Inserts a new sorted entry when the key is absent.
3726 /// - Triggers the same root-split and proactive-split logic.
3727 ///
3728 /// `data` should be the raw value bytes, or an empty slice for a
3729 /// deletion (which should not normally arrive here during redo, but is
3730 /// handled gracefully).
3731 ///
3732 /// Wave 11-K optimisation (Fix 1).
3733 pub fn redo_insert(
3734 &self,
3735 key: &[u8],
3736 data: &[u8],
3737 lsn: Lsn,
3738 ) -> Result<bool, TreeError> {
3739 let key_len = key.len();
3740 let data_len = data.len();
3741 let data_opt: Option<&[u8]> =
3742 if data.is_empty() { None } else { Some(data) };
3743
3744 // First-key path: initialise a two-level tree from scratch.
3745 {
3746 let mut root_guard = self.root.write();
3747 if root_guard.is_none() {
3748 // Pre-allocate the BIN's entries Vec using the redo capacity
3749 // hint (Fix 3). Without the hint the first BIN starts at
3750 // capacity 1 and doubles on each insert; with the hint it
3751 // starts at min(hint, max_entries) entries, eliminating
3752 // ~log2(max_entries) Vec-resize doublings.
3753 let initial_cap = if self.redo_capacity_hint > 0 {
3754 self.redo_capacity_hint.min(self.max_entries_per_node)
3755 } else {
3756 1
3757 };
3758 let mut initial_entries = Vec::with_capacity(initial_cap);
3759 initial_entries.push(BinEntry {
3760 data: data_opt.map(|d| d.to_vec()),
3761 known_deleted: false,
3762 dirty: false,
3763 expiration_time: 0,
3764 });
3765 let bin = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
3766 node_id: generate_node_id(),
3767 level: BIN_LEVEL,
3768 entries: initial_entries,
3769 key_prefix: Vec::new(),
3770 dirty: true,
3771 is_delta: false,
3772 last_full_lsn: NULL_LSN,
3773 last_delta_lsn: NULL_LSN,
3774 generation: 0,
3775 parent: None,
3776 // St-H6: use true to match the engine-wide hours-only
3777 // invariant (JE BIN.java default; matches tree.rs:980).
3778 expiration_in_hours: true,
3779 cursor_count: 0,
3780 prohibit_next_delta: false,
3781 lsn_rep: LsnRep::from_lsns(&[lsn]),
3782 keys: KeyRep::from_keys(vec![key.to_vec()]), // T-2
3783 compact_max_key_length: self.compact_max_key_length,
3784 })));
3785
3786 let root_arc =
3787 Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
3788 node_id: generate_node_id(),
3789 level: MAIN_LEVEL | 2,
3790 entries: vec![InEntry { key: vec![] }],
3791 // T-4: the single resident child at slot 0.
3792 targets: TargetRep::Sparse(vec![(0, bin.clone())]),
3793 dirty: true,
3794 generation: 0,
3795 parent: None,
3796 lsn_rep: LsnRep::from_lsns(&[lsn]),
3797 })));
3798
3799 {
3800 let mut g = bin.write();
3801 g.set_parent(Some(Arc::downgrade(&root_arc)));
3802 }
3803
3804 *root_guard = Some(root_arc);
3805
3806 if let Some(counter) = &self.memory_counter {
3807 let delta =
3808 (key_len + data_len + BIN_ENTRY_OVERHEAD) as i64;
3809 counter.fetch_add(delta, Ordering::Relaxed);
3810 }
3811 return Ok(true);
3812 }
3813 }
3814
3815 self.split_root_if_needed(lsn)?;
3816
3817 let root_arc = self.get_root().unwrap();
3818 let result = Self::redo_insert_recursive(
3819 &root_arc,
3820 key,
3821 data_opt,
3822 lsn,
3823 self.max_entries_per_node,
3824 self.key_comparator.as_ref(),
3825 self.key_prefixing,
3826 )?;
3827
3828 if result && let Some(counter) = &self.memory_counter {
3829 let delta = (key_len + data_len + BIN_ENTRY_OVERHEAD) as i64;
3830 counter.fetch_add(delta, Ordering::Relaxed);
3831 }
3832
3833 Ok(result)
3834 }
3835
3836 /// Splits the root node if it is full (needsSplitting).
3837 ///
3838 ///
3839 /// ```text
3840 /// 1. Save oldRoot (the current root IN or BIN).
3841 /// 2. Create newRoot at oldRoot.level + 1.
3842 /// 3. Insert oldRoot into newRoot at slot 0 with a virtual (empty) key.
3843 /// 4. Call split_node on oldRoot, passing newRoot as parent.
3844 /// 5. Replace tree root with newRoot.
3845 /// ```
3846 fn split_root_if_needed(&self, lsn: Lsn) -> Result<(), TreeError> {
3847 // Hold `self.root.write()` across the needs_split check and the
3848 // root promotion, mirroring the first-key path fix and matching
3849 // the broader insert/split serialisation discipline.
3850 //
3851 // With the previous read-then-write pattern, two concurrent
3852 // splitters could each observe needs_split == true, then take()
3853 // and install in turn, with the second wrapping the first's
3854 // already-promoted root in its own new IN. Each level wraps the
3855 // previous, producing a chain of one-child internal nodes. No
3856 // data is lost (every entry is still reachable) but the tree
3857 // becomes unnecessarily deep, and the imbalance can compound
3858 // under heavy concurrent insertion.
3859 let mut root_guard = self.root.write();
3860 let needs_split = match root_guard.as_ref() {
3861 Some(arc) => {
3862 let g = arc.read();
3863 g.get_n_entries() >= self.max_entries_per_node
3864 }
3865 None => false,
3866 };
3867 if !needs_split {
3868 return Ok(());
3869 }
3870
3871 // Create a fresh new root one level above the current root.
3872 let old_root_arc = root_guard.take().expect("checked Some above");
3873 let old_root_level = {
3874 let g = old_root_arc.read();
3875 g.level()
3876 };
3877
3878 // newRoot = new IN(level = oldRoot.level + 1) with slot 0 = oldRoot.
3879 // The key at slot 0 is the virtual key (empty slice) following the
3880 // convention that entry-zero in an upper IN compares as -infinity.
3881 let new_root_arc =
3882 Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
3883 node_id: generate_node_id(),
3884 level: old_root_level + 1,
3885 entries: vec![InEntry { key: vec![] }],
3886 // T-4: slot 0's resident child is the old root.
3887 targets: TargetRep::Sparse(vec![(0, old_root_arc.clone())]),
3888 dirty: true,
3889 generation: 0,
3890 parent: None,
3891 lsn_rep: LsnRep::from_lsns(&[lsn]),
3892 })));
3893
3894 // Update the old root's parent pointer to the new root.
3895 {
3896 let mut g = old_root_arc.write();
3897 g.set_parent(Some(Arc::downgrade(&new_root_arc)));
3898 }
3899
3900 // Install the new root before calling split_child so split_child
3901 // (which itself takes parent.write()) can run unencumbered.
3902 *root_guard = Some(new_root_arc.clone());
3903 drop(root_guard);
3904
3905 // Now split the old root (which is now child at slot 0 in new_root).
3906 Self::split_child(
3907 &new_root_arc,
3908 0, // child is at slot 0
3909 self.max_entries_per_node,
3910 lsn,
3911 SplitHint::Normal,
3912 &[], // no insertion key at root-init time
3913 self.key_comparator.as_ref(),
3914 self.key_prefixing,
3915 self.in_list_listener.as_ref(),
3916 )?;
3917
3918 // EVICTOR-RECLAIM-1: register the freshly-promoted root IN with the
3919 // evictor's LRU (JE Tree.splitRoot adds the new root to the INList).
3920 // split_child above already registers the new sibling.
3921 let new_root_id = match &*new_root_arc.read() {
3922 TreeNode::Internal(n) => n.node_id,
3923 TreeNode::Bottom(b) => b.node_id,
3924 };
3925 self.note_added(new_root_id);
3926
3927 self.root_splits.fetch_add(1, Ordering::Relaxed);
3928 Ok(())
3929 }
3930
3931 /// Splits the child at `child_index` in `parent`.
3932 ///
3933 /// . This implementation always keeps the **left** half in the
3934 /// existing child node (`child_arc`) and puts the right half in the new
3935 /// sibling, regardless of where the `identifierKey` falls. JE's
3936 /// `IN.splitInternal` (`idKeyIndex` logic ~line 4172) can place either
3937 /// half in the existing node; Noxu's preemptive-split discipline ensures
3938 /// the parent always has a free slot at split time (the split is done on
3939 /// the way *down*, before the parent fills up), so the safe simplification
3940 /// of always using the left half is correct here — no routing information
3941 /// is lost. This comment replaces the previous incorrect claim that
3942 /// `idKeyIndex` drove the choice.
3943 ///
3944 /// Note: does not emit a split log entry; split nodes are marked dirty
3945 /// and flushed at the next checkpoint (flush_dirty_bins/upper_ins).
3946 ///
3947 /// ```text
3948 /// 1. splitIndex = child.nEntries / 2 (or 1 / n-1 for splitSpecial)
3949 /// 2. Create newSibling at the same level.
3950 /// 3. Move entries [splitIndex..nEntries) to newSibling.
3951 /// 4. Update parent slot childIndex -> child (left half),
3952 /// insert newSibling with newIdKey after childIndex.
3953 /// ```
3954 fn split_child(
3955 parent: &Arc<RwLock<TreeNode>>,
3956 child_index: usize,
3957 max_entries: usize,
3958 lsn: Lsn,
3959 hint: SplitHint,
3960 insert_key: &[u8],
3961 key_comparator: Option<&KeyComparatorFn>,
3962 key_prefixing: bool,
3963 listener: Option<&Arc<dyn InListListener>>,
3964 ) -> Result<(), TreeError> {
3965 // The split is performed under `parent.write()` for the entire
3966 // duration. This is a deliberate choice for correctness:
3967 //
3968 // - Without it, between dropping `child.write()` (after installing
3969 // the left half) and acquiring `parent.write()` (to install the
3970 // sibling), a concurrent descender can pick `child_arc` from the
3971 // parent (still pointing at it), descend, take `child.write()`
3972 // and insert a key. Whether the descender's key belongs in the
3973 // left half (now in `child`) or the right half (which will be
3974 // in the new sibling) is determined by the parent's split key —
3975 // but the parent doesn't know about the split key yet, so the
3976 // descender's routing decision is based on stale data. If the
3977 // descender's key falls in the right half, it lands in `child`
3978 // (left half) where a future search will not find it: the
3979 // future search descends from the root, the parent now has the
3980 // sibling installed, the search routes the key to the sibling,
3981 // the sibling does not contain the key — silently lost.
3982 //
3983 // - Holding `parent.write()` throughout serialises split_child
3984 // against every descender that wants `parent.read()`. A
3985 // descender already holding `parent.read()` (latch coupling
3986 // from above) keeps split_child waiting at this lock until it
3987 // has finished its own work. Combined, the split + sibling
3988 // install is atomic with respect to descents.
3989 //
3990 // - Splits are infrequent compared to inserts (~ once per
3991 // max_entries new keys) so the extra serialisation here does
3992 // not dominate.
3993 //
3994 // Reproducer that exercises this race:
3995 // crates/noxu-db/tests/concurrent_commits_stress.rs.
3996 let mut parent_write_guard = parent.write();
3997
3998 // Extract the child Arc from the parent slot.
3999 let child_arc = match &*parent_write_guard {
4000 TreeNode::Internal(p) => {
4001 p.get_child(child_index).ok_or(TreeError::SplitRequired)?
4002 }
4003 TreeNode::Bottom(_) => return Err(TreeError::SplitRequired),
4004 };
4005
4006 // Gather all entries from the child plus split metadata, AND
4007 // perform the in-place left-half install, all under a single
4008 // write lock on the child. See the earlier comment on the race
4009 // this avoids inside split_child.
4010 let mut child_guard = child_arc.write();
4011
4012 // Re-validate that the child still needs splitting, now that we hold
4013 // its write lock. This closes a check-then-act race: the caller
4014 // (`insert_recursive_inner`) tested `child.get_n_entries() >=
4015 // max_entries` under a PARENT READ lock, then dropped that read lock
4016 // (required — the split needs `parent.write()`) before calling
4017 // `split_child`. Read locks do not exclude each other, so two
4018 // descenders can both pass the fullness check on the same child, both
4019 // drop the parent read lock, and both call `split_child`. They
4020 // serialise here on `parent.write()`: the first splits the child
4021 // (leaving it with only its left half), and by the time the second
4022 // acquires this child write lock the child is no longer full — or is
4023 // empty, if a concurrent INCompressor merge cleared it
4024 // (`compress_node`'s `lb.entries.clear()`). Without this re-check the
4025 // second caller would build a `SplitEntries` from that stale child and
4026 // panic in `SplitEntries::get_key(split_index)` on an empty entries
4027 // vec (tree.rs SplitEntries::get_key `v[index]`, observed as
4028 // "index out of bounds: len is 0" under the 96-thread saturation
4029 // benchmark; see .agent/archived-audits/bench/
4030 // bug-bin-split-concurrency.md).
4031 //
4032 // JE performs the identical re-validation: `IN.split` re-checks
4033 // `needsSplitting()` *after* latching the node it will split, so the
4034 // fullness test and the split are atomic w.r.t. the node latch (see
4035 // `IN.split` / `IN.needsSplitting` in IN.java; `Tree.forceSplit`
4036 // latch-couples down and `IN.split` re-tests before mutating). Here
4037 // the child write guard plays the role of that node latch.
4038 //
4039 // A no-op split returns `Ok(())` — the SAME success variant a real
4040 // split returns — because the caller re-descends unconditionally
4041 // after `split_child` (`return Self::insert_recursive_inner(...)`),
4042 // where it re-reads the (now-current) topology and re-checks
4043 // `child_full`. So a benign "already split" outcome simply leads to a
4044 // correct re-descent and the insert proceeds. This does NOT widen any
4045 // lock or hold `parent.write()` across the caller's read-check, so it
4046 // does not re-introduce the descent over-serialisation fixed in 7.2.1.
4047 if child_guard.get_n_entries() < max_entries {
4048 return Ok(());
4049 }
4050
4051 let child_level = child_guard.level();
4052 // St-H6: capture the splitting BIN's expiration_in_hours flag BEFORE
4053 // drop(child_guard) so the right-half sibling inherits it.
4054 // JE: BIN.java::setExpiration calls setExpirationInHours(hours) to
4055 // propagate the flag on split/clone; the Rust split was hardcoding
4056 // false instead of inheriting — this caused hours-granularity TTL
4057 // entries in the right sibling to be read with in_hours=false, making
4058 // the hours-since-epoch value compare as seconds-since-epoch (far in
4059 // the past) and every right-sibling TTL record appear expired.
4060 let bin_expiration_in_hours: bool = match &*child_guard {
4061 TreeNode::Bottom(b) => b.expiration_in_hours,
4062 // Internal nodes do not carry per-entry TTL; default to true
4063 // (the engine-wide invariant for any BIN that may hold TTL data).
4064 TreeNode::Internal(_) => true,
4065 };
4066 // T-2/T-5: the compact-key threshold the new sibling BIN inherits.
4067 // (Only consumed when the child is a BIN; an upper-IN split produces
4068 // upper-IN siblings, which have no compact key rep.)
4069 let bin_compact_max_key_length: i32 = match &*child_guard {
4070 TreeNode::Bottom(b) => b.compact_max_key_length,
4071 TreeNode::Internal(_) => INKeyRep_DEFAULT_MAX_KEY_LENGTH,
4072 };
4073 let (all_entries, bin_old_prefix) = match &*child_guard {
4074 TreeNode::Internal(n) => {
4075 // T-4: capture the parallel resident-child array alongside the
4076 // entries so children travel with their slots through the
4077 // split (JE `IN.split` copies `entryTargets`).
4078 let children: Vec<Option<ChildArc>> =
4079 (0..n.entries.len()).map(|i| n.get_child(i)).collect();
4080 // T-3: capture the parallel per-slot LSNs so they travel with
4081 // their slots (JE `IN.split` copies `entryLsnByteArray`).
4082 let lsns: Vec<Lsn> =
4083 (0..n.entries.len()).map(|i| n.get_lsn(i)).collect();
4084 (
4085 SplitEntries::Internal(n.entries.clone(), children, lsns),
4086 Vec::new(),
4087 )
4088 }
4089 TreeNode::Bottom(b) => {
4090 // Decompress to full keys.
4091 let full: Vec<BinEntry> = (0..b.entries.len())
4092 .map(|i| BinEntry {
4093 data: b.entries[i].data.clone(),
4094 known_deleted: b.entries[i].known_deleted,
4095 dirty: b.entries[i].dirty,
4096 expiration_time: b.entries[i].expiration_time,
4097 })
4098 .collect();
4099 let lsns: Vec<Lsn> =
4100 (0..b.entries.len()).map(|i| b.get_lsn(i)).collect();
4101 // T-2: carry FULL keys through the split; the new BINs
4102 // recompute their own prefix from them.
4103 let full_keys: Vec<Vec<u8>> = (0..b.entries.len())
4104 .map(|i| b.get_full_key(i).unwrap_or_default())
4105 .collect();
4106 (
4107 SplitEntries::Bottom(full, lsns, full_keys),
4108 b.key_prefix.clone(),
4109 )
4110 }
4111 };
4112
4113 // Determine split point — JE `IN.splitSpecial` / `IN.splitInternal`.
4114 //
4115 // Normal midpoint: `n_entries / 2`.
4116 // AllLeft: insertion key is at position 0 on every descend level.
4117 // → split_index = 1 (left half keeps n-1 entries; new right sibling
4118 // gets only the former-first slot, then the insertion fills it).
4119 // This matches JE: `if (leftSide && index == 0) splitInternal(…, 1)`.
4120 // AllRight: insertion key is at the last position on every level.
4121 // → split_index = n_entries - 1 (left half keeps all but one entry).
4122 // JE: `else if (!leftSide && index == nEntries-1) splitInternal(…, nEntries-1)`.
4123 //
4124 // Ref: `IN.java` splitSpecial ~line 4129, splitInternal ~line 4159.
4125 let n_entries = all_entries.len();
4126 let split_index = if n_entries >= 2 {
4127 // Find where insert_key falls in the child.
4128 let insert_idx = {
4129 let mut idx = 0usize;
4130 for i in 1..n_entries {
4131 let ord = match key_comparator {
4132 Some(cmp) => cmp(all_entries.get_key(i), insert_key),
4133 None => all_entries.get_key(i).cmp(insert_key),
4134 };
4135 if ord != std::cmp::Ordering::Greater {
4136 idx = i;
4137 } else {
4138 break;
4139 }
4140 }
4141 idx
4142 };
4143 match hint {
4144 SplitHint::AllLeft if insert_idx == 0 => 1,
4145 SplitHint::AllRight if insert_idx == n_entries - 1 => {
4146 n_entries - 1
4147 }
4148 _ => n_entries / 2,
4149 }
4150 } else {
4151 n_entries / 2
4152 };
4153
4154 // newIdKey — the full key of the first entry of the right half.
4155 // For BIN: entries are already full keys after decompression above.
4156 // For IN: entries carry full keys directly.
4157 let new_id_key = all_entries.get_key(split_index).to_vec();
4158 // Suppress unused-variable warning when no BIN is involved.
4159 let _ = &bin_old_prefix;
4160
4161 // Divide into left and right halves.
4162 let left_entries = all_entries.slice(0, split_index);
4163 let right_entries = all_entries.slice(split_index, n_entries);
4164
4165 // Install the left half into `child_arc` (still under the same
4166 // write lock) and mark the node dirty.
4167 match (&mut *child_guard, &left_entries) {
4168 (TreeNode::Internal(n), SplitEntries::Internal(le, lc, ll)) => {
4169 n.entries = le.clone();
4170 // T-4: reinstall the (now-shorter) left child array.
4171 n.targets = TargetRep::None;
4172 for (i, c) in lc.iter().enumerate() {
4173 if let Some(child) = c {
4174 n.set_child(i, Some(child.clone()));
4175 }
4176 }
4177 // T-3: reinstall the (now-shorter) left LSN array.
4178 n.lsn_rep = LsnRep::from_lsns(ll);
4179 }
4180 (TreeNode::Bottom(b), SplitEntries::Bottom(le, ll, lk)) => {
4181 // Reset prefix; keys arrive as FULL keys (no prefix yet).
4182 b.key_prefix = Vec::new();
4183 // Pre-allocate at max_entries capacity so the left half
4184 // does not need to reallocate on the next insert (Fix 3).
4185 let mut left = Vec::with_capacity(max_entries);
4186 left.extend_from_slice(le);
4187 b.entries = left;
4188 // T-3: reinstall the left LSN array.
4189 b.lsn_rep = LsnRep::from_lsns(ll);
4190 // T-2: reinstall the left key rep from the full keys (Default;
4191 // recompute_key_prefix below compresses + compacts).
4192 b.keys = KeyRep::from_keys(lk.clone());
4193 // Recompute prefix on each half after split (only when
4194 // key_prefixing is enabled for this database).
4195 // JE: IN.computeKeyPrefix returns null when
4196 // databaseImpl.getKeyPrefixing() is false.
4197 // Ref: IN.java computeKeyPrefix ~line 2456.
4198 if key_prefixing && b.entries.len() >= 2 {
4199 b.recompute_key_prefix();
4200 } else {
4201 b.keys.compact(b.compact_max_key_length); // T-2
4202 }
4203 }
4204 _ => return Err(TreeError::SplitRequired),
4205 }
4206 child_guard.set_dirty(true);
4207 drop(child_guard);
4208
4209 // Create the new right-half sibling.
4210 // Parent pointer will be wired in when it is inserted into the parent.
4211 let new_sibling = match right_entries {
4212 SplitEntries::Internal(re, rc, rl) => {
4213 let mut rin = InNodeStub {
4214 node_id: generate_node_id(),
4215 level: child_level,
4216 entries: re,
4217 targets: TargetRep::None,
4218 dirty: true,
4219 generation: 0,
4220 parent: None, // set below
4221 // T-3: the right half's per-slot LSNs.
4222 lsn_rep: LsnRep::from_lsns(&rl),
4223 };
4224 // T-4: install the right half's resident children.
4225 for (i, c) in rc.into_iter().enumerate() {
4226 if c.is_some() {
4227 rin.set_child(i, c);
4228 }
4229 }
4230 Arc::new(RwLock::new(TreeNode::Internal(rin)))
4231 }
4232 SplitEntries::Bottom(re, rl, rk) => {
4233 // Entries arrive as FULL keys; build BinStub with no prefix
4234 // then recompute key prefix for the new sibling.
4235 // Pre-allocate at max_entries capacity so the right half
4236 // does not need to reallocate on the next insert (Fix 3).
4237 let mut right = Vec::with_capacity(max_entries);
4238 right.extend(re);
4239 let mut sibling_bin = BinStub {
4240 node_id: generate_node_id(),
4241 level: child_level,
4242 entries: right,
4243 key_prefix: Vec::new(),
4244 dirty: true,
4245 is_delta: false,
4246 last_full_lsn: NULL_LSN,
4247 last_delta_lsn: NULL_LSN,
4248 generation: 0,
4249 parent: None, // set below
4250 // St-H6 fix: inherit the splitting BIN's flag so that
4251 // is_expired() uses the correct granularity for entries
4252 // that were already in the BIN before the split.
4253 // JE reference: BIN.java::split() propagates
4254 // expirationInHours via setExpirationInHours(hours).
4255 expiration_in_hours: bin_expiration_in_hours,
4256 cursor_count: 0,
4257 prohibit_next_delta: false,
4258 // T-3: the right half's per-slot LSNs.
4259 lsn_rep: LsnRep::from_lsns(&rl),
4260 // T-2: full keys (Default); recompute/compact below.
4261 keys: KeyRep::from_keys(rk),
4262 compact_max_key_length: bin_compact_max_key_length,
4263 };
4264 // St-H6 debug guard: the sibling must carry the same flag as
4265 // the splitting BIN so that in_hours-resolution entries are
4266 // never silently expired by a mismatched false flag.
4267 debug_assert_eq!(
4268 sibling_bin.expiration_in_hours, bin_expiration_in_hours,
4269 "St-H6 invariant: sibling BIN expiration_in_hours must \
4270 match the splitting BIN (got {}, expected {})",
4271 sibling_bin.expiration_in_hours, bin_expiration_in_hours
4272 );
4273
4274 if key_prefixing && sibling_bin.entries.len() >= 2 {
4275 sibling_bin.recompute_key_prefix();
4276 } else {
4277 sibling_bin.keys.compact(bin_compact_max_key_length); // T-2
4278 }
4279 Arc::new(RwLock::new(TreeNode::Bottom(sibling_bin)))
4280 }
4281 };
4282
4283 // Note: the child (left half) was marked dirty earlier under the
4284 // same write lock that installed left_entries; no need to re-take
4285 // the write lock here.
4286
4287 // Insert the new sibling into the parent after child_index.
4288 // We already hold `parent.write()` (taken at the top of the
4289 // function); operate on it directly rather than re-acquiring.
4290 match &mut *parent_write_guard {
4291 TreeNode::Internal(p) => {
4292 let insert_pos = child_index + 1;
4293 // T-4: insert the parent slot and set its cached child via the
4294 // node-level INTargetRep (shifting existing children).
4295 p.insert_entry(
4296 insert_pos,
4297 new_id_key,
4298 lsn,
4299 Some(new_sibling.clone()),
4300 );
4301 // Parent is dirty because it gained a new entry.
4302 p.dirty = true;
4303 }
4304 TreeNode::Bottom(_) => return Err(TreeError::SplitRequired),
4305 }
4306
4307 // Wire the new sibling's parent pointer to the parent node
4308 // before releasing parent_write_guard, so a future descent that
4309 // takes parent.read() and finds the sibling immediately sees a
4310 // fully-wired parent pointer.
4311 {
4312 let mut g = new_sibling.write();
4313 g.set_parent(Some(Arc::downgrade(parent)));
4314 }
4315 // T-4: when an upper IN split, the children that moved into the new
4316 // sibling must have their parent back-pointers re-wired to the
4317 // sibling (JE re-parents moved targets in IN.split).
4318 {
4319 let sg = new_sibling.read();
4320 if let TreeNode::Internal(sn) = &*sg {
4321 let moved = sn.resident_children();
4322 drop(sg);
4323 for child in moved {
4324 let mut cg = child.write();
4325 cg.set_parent(Some(Arc::downgrade(&new_sibling)));
4326 }
4327 }
4328 }
4329 drop(parent_write_guard);
4330
4331 // EVICTOR-RECLAIM-1: register the freshly-split sibling with the
4332 // evictor's LRU (JE IN.splitInternal calls inList.add(newSibling)).
4333 // Without this, split-created BINs/INs are invisible to the evictor:
4334 // the policy lists never receive them, every evict_batch phase quota
4335 // is 0, and eviction reclaims nothing under pressure even though the
4336 // nodes are fully resident. Only the very first root+BIN (the
4337 // first-key path) and re-fetched nodes were ever registered.
4338 if let Some(l) = listener {
4339 let sibling_id = match &*new_sibling.read() {
4340 TreeNode::Internal(n) => n.node_id,
4341 TreeNode::Bottom(b) => b.node_id,
4342 };
4343 l.note_ins_added(sibling_id);
4344 }
4345
4346 Ok(())
4347 }
4348
4349 /// Recursive insert with preemptive splitting.
4350 ///
4351 /// Top-down traversal in `Tree.forceSplit` +
4352 /// `Tree.searchSplitsAllowed`:
4353 ///
4354 /// 1. At an upper IN: find which child slot covers `key`, split the child
4355 /// proactively if it is full (so we always have room to insert the split
4356 /// key into the parent), then recurse into the appropriate child.
4357 /// 2. At a BIN: insert the key/data directly.
4358 ///
4359 /// This implements the "preemptive splitting" strategy from the: we split
4360 /// children on the way down so we never need to walk back up.
4361 fn insert_recursive(
4362 node_arc: &Arc<RwLock<TreeNode>>,
4363 key: Vec<u8>,
4364 data: Vec<u8>,
4365 lsn: Lsn,
4366 max_entries: usize,
4367 key_comparator: Option<&KeyComparatorFn>,
4368 key_prefixing: bool,
4369 listener: Option<&Arc<dyn InListListener>>,
4370 ) -> Result<bool, TreeError> {
4371 Self::insert_recursive_inner(
4372 node_arc,
4373 key,
4374 data,
4375 lsn,
4376 max_entries,
4377 key_comparator,
4378 key_prefixing,
4379 true, // all_left_so_far
4380 true, // all_right_so_far
4381 listener,
4382 )
4383 }
4384
4385 /// Inner recursive helper that threads `allLeftSideDescent` /
4386 /// `allRightSideDescent` from `Tree.forceSplit` (JE ~line 1912).
4387 ///
4388 /// Both flags start `true` at the root and are cleared as soon as the
4389 /// descent takes a non-leftmost / non-rightmost child slot. At split
4390 /// time they are forwarded to `split_child` which uses them to pick the
4391 /// `splitSpecial` split index (JE `IN.splitSpecial` ~line 4129).
4392 #[allow(clippy::too_many_arguments)]
4393 fn insert_recursive_inner(
4394 node_arc: &Arc<RwLock<TreeNode>>,
4395 key: Vec<u8>,
4396 data: Vec<u8>,
4397 lsn: Lsn,
4398 max_entries: usize,
4399 key_comparator: Option<&KeyComparatorFn>,
4400 key_prefixing: bool,
4401 all_left_so_far: bool,
4402 all_right_so_far: bool,
4403 listener: Option<&Arc<dyn InListListener>>,
4404 ) -> Result<bool, TreeError> {
4405 // Determine if this is a BIN (leaf level).
4406 //
4407 // We hold a read lock on `node_arc` (the parent of any descent we
4408 // do below) for the duration of this call, releasing it just
4409 // before returning. That achieves *latch coupling*: a concurrent
4410 // `split_child(parent, …)` that wants to reorganise our subtree
4411 // ultimately needs `parent.write()` to install the new sibling,
4412 // and that write blocks until our read lock is dropped. Without
4413 // this, the descender-vs-splitter race goes:
4414 //
4415 // T_X: at root, picks child_arc (BIN), drops root read lock.
4416 // T_Y: at root, runs split_child(root, …): takes child_arc.write(),
4417 // installs left half [E1..E5], creates sibling [E6..E10],
4418 // takes root.write() and inserts the sibling.
4419 // T_X: now takes child_arc.write() and inserts a key whose
4420 // sort order falls in the right half. The key lands in
4421 // child_arc (left half) but a future search descending
4422 // from the root routes that key to the new sibling and
4423 // does not find it — silently lost.
4424 //
4425 // Reproducer: noxu-db/tests/concurrent_commits_stress.rs
4426 // (32 threads × 100 keys, ~1–6 lost writes per run before this fix;
4427 // occasionally hundreds when an entire BIN is orphaned).
4428 let parent_guard = node_arc.read();
4429 let is_bin = parent_guard.is_bin();
4430
4431 if is_bin {
4432 // BIN: drop the read lock and take the write lock; this is
4433 // safe because the *outer* call frame still holds a read
4434 // lock on this BIN's parent (or this is the root, in which
4435 // case the first-key path has already initialised it). A
4436 // concurrent split_child(parent, …) cannot run while the
4437 // outer parent.read() is held, so the BIN cannot be
4438 // restructured between dropping our read lock and acquiring
4439 // our write lock.
4440 drop(parent_guard);
4441 let mut guard = node_arc.write();
4442 match &mut *guard {
4443 TreeNode::Bottom(bin) => {
4444 let is_new = if let Some(cmp) = key_comparator {
4445 // Comparator-based insert: no prefix compression.
4446 let (_idx, new) =
4447 bin.insert_cmp(key, lsn, Some(data), cmp.as_ref());
4448 new
4449 } else if key_prefixing {
4450 // insert_with_prefix handles prefix recomputation when
4451 // the new key shrinks the existing prefix, and also
4452 // initialises the prefix when 2 entries are present for
4453 // the first time.
4454 let (_idx, new) =
4455 bin.insert_with_prefix(key, lsn, Some(data));
4456 new
4457 } else {
4458 // key_prefixing disabled: store full key, no prefix.
4459 // JE: IN.computeKeyPrefix returns null when
4460 // databaseImpl.getKeyPrefixing() is false.
4461 // Ref: IN.java computeKeyPrefix ~line 2456.
4462 let (_idx, new) = bin.insert_raw(key, lsn, Some(data));
4463 new
4464 };
4465 // Mark dirty after any modification.
4466 bin.dirty = true;
4467 Ok(is_new)
4468 }
4469 TreeNode::Internal(_) => Err(TreeError::SplitRequired),
4470 }
4471 } else {
4472 // Upper IN: find the child slot that covers key.
4473 // Index = parent.findEntry(key, false, false)
4474 // Entry zero in an upper IN has a virtual key (-infinity), so
4475 // any real key is routed to at least slot 0.
4476 let (child_index, n_entries_at_level, child_arc) =
4477 match &*parent_guard {
4478 TreeNode::Internal(n) => {
4479 // Binary search for the largest key <= search key.
4480 // Slot 0 always matches (virtual key = -infinity).
4481 let mut idx = 0usize;
4482 for (i, entry) in n.entries.iter().enumerate() {
4483 if i == 0 {
4484 idx = 0;
4485 } else {
4486 let ord = match key_comparator {
4487 Some(cmp) => cmp(
4488 entry.key.as_slice(),
4489 key.as_slice(),
4490 ),
4491 None => {
4492 entry.key.as_slice().cmp(key.as_slice())
4493 }
4494 };
4495 if ord != std::cmp::Ordering::Greater {
4496 idx = i;
4497 } else {
4498 break;
4499 }
4500 }
4501 }
4502 let child =
4503 n.get_child(idx).ok_or(TreeError::SplitRequired)?;
4504 (idx, n.entries.len(), child)
4505 }
4506 TreeNode::Bottom(_) => {
4507 return Err(TreeError::SplitRequired);
4508 }
4509 };
4510
4511 // Update the descent-side flags (JE `Tree.forceSplit` ~1959).
4512 // `allLeftSideDescent` ← still true only if we chose slot 0.
4513 // `allRightSideDescent` ← still true only if we chose the last slot.
4514 let all_left = all_left_so_far && child_index == 0;
4515 let all_right = all_right_so_far
4516 && child_index == n_entries_at_level.saturating_sub(1);
4517
4518 // Proactively split the child if it is full.
4519 // If (child.needsSplitting()) child.split(parent, ...)
4520 let child_full = {
4521 let g = child_arc.read();
4522 g.get_n_entries() >= max_entries
4523 };
4524
4525 if child_full {
4526 // Build the splitSpecial hint from the accumulated flags.
4527 // JE `Tree.forceSplit` ~line 2010:
4528 // if (allLeftSideDescent || allRightSideDescent)
4529 // child.splitSpecial(parent, index, grandParent,
4530 // maxTreeEntriesPerNode, key, allLeftSideDescent)
4531 let hint = match (all_left, all_right) {
4532 (true, _) => SplitHint::AllLeft,
4533 (_, true) => SplitHint::AllRight,
4534 _ => SplitHint::Normal,
4535 };
4536 // split_child(parent, …) needs parent.write(); we must
4537 // drop our parent read lock before calling it.
4538 drop(parent_guard);
4539 Self::split_child(
4540 node_arc,
4541 child_index,
4542 max_entries,
4543 lsn,
4544 hint,
4545 &key,
4546 key_comparator,
4547 key_prefixing,
4548 listener,
4549 )?;
4550
4551 // After the split, re-find which child now covers key.
4552 // Re-enter at the top of the inner function; carry the
4553 // flags (the new topology doesn't invalidate them — we
4554 // still know the overall descent direction).
4555 return Self::insert_recursive_inner(
4556 node_arc,
4557 key,
4558 data,
4559 lsn,
4560 max_entries,
4561 key_comparator,
4562 key_prefixing,
4563 all_left_so_far,
4564 all_right_so_far,
4565 listener,
4566 );
4567 }
4568
4569 // Descend into the child while still holding parent_guard.
4570 // The recursive call will hold child.read() before this
4571 // returns, then drop it; combined with our parent_guard,
4572 // the latch coupling chain is preserved on the way down and
4573 // unwound on the way back up.
4574 let r = Self::insert_recursive_inner(
4575 &child_arc,
4576 key,
4577 data,
4578 lsn,
4579 max_entries,
4580 key_comparator,
4581 key_prefixing,
4582 all_left,
4583 all_right,
4584 listener,
4585 );
4586 drop(parent_guard);
4587 r
4588 }
4589 }
4590
4591 /// Slice-based variant of [`Tree::insert_recursive`] for the recovery redo path.
4592 ///
4593 /// Accepts `key: &[u8]` and `data: Option<&[u8]>` instead of owned
4594 /// `Vec<u8>` values. At the BIN leaf, calls
4595 /// [`BinStub::insert_with_prefix_slice`] which copies bytes into the
4596 /// `BinEntry` exactly once.
4597 ///
4598 /// For the comparator path (custom key comparator), falls back to
4599 /// `insert_cmp` with a one-time `to_vec()` conversion — that path is
4600 /// rare in practice (sorted-dup databases only) and is not on the
4601 /// W11 hot path.
4602 ///
4603 /// Wave 11-K optimisation (Fix 1).
4604 fn redo_insert_recursive(
4605 node_arc: &Arc<RwLock<TreeNode>>,
4606 key: &[u8],
4607 data: Option<&[u8]>,
4608 lsn: Lsn,
4609 max_entries: usize,
4610 key_comparator: Option<&KeyComparatorFn>,
4611 key_prefixing: bool,
4612 ) -> Result<bool, TreeError> {
4613 Self::redo_insert_recursive_inner(
4614 node_arc,
4615 key,
4616 data,
4617 lsn,
4618 max_entries,
4619 key_comparator,
4620 key_prefixing,
4621 true,
4622 true,
4623 )
4624 }
4625
4626 #[allow(clippy::too_many_arguments)]
4627 fn redo_insert_recursive_inner(
4628 node_arc: &Arc<RwLock<TreeNode>>,
4629 key: &[u8],
4630 data: Option<&[u8]>,
4631 lsn: Lsn,
4632 max_entries: usize,
4633 key_comparator: Option<&KeyComparatorFn>,
4634 key_prefixing: bool,
4635 all_left_so_far: bool,
4636 all_right_so_far: bool,
4637 ) -> Result<bool, TreeError> {
4638 let parent_guard = node_arc.read();
4639 let is_bin = parent_guard.is_bin();
4640
4641 if is_bin {
4642 drop(parent_guard);
4643 let mut guard = node_arc.write();
4644 match &mut *guard {
4645 TreeNode::Bottom(bin) => {
4646 // REC-F2: JE redo currency check
4647 // (RecoveryManager.redo() line ~2512/2544). A logged LN
4648 // is applied only when logrecLsn > treeLsn. If the slot
4649 // already holds an equal-or-newer LSN, skip the overwrite
4650 // so an out-of-order (older-LSN) redo cannot revert
4651 // committed data or reset the slot LSN backward. This
4652 // makes redo genuinely idempotent regardless of
4653 // redo/undo phase order. Deletes never reach this path
4654 // (redo_ln routes Delete through tree.delete), so the JE
4655 // "lsnCmp == 0 && isDeletion -> set KD" sub-case does not
4656 // apply here.
4657 let cmp_ref = key_comparator.map(|c| {
4658 c.as_ref()
4659 as &dyn Fn(&[u8], &[u8]) -> std::cmp::Ordering
4660 });
4661 if let Some(slot_lsn) =
4662 bin.redo_slot_lsn(key, cmp_ref, key_prefixing)
4663 && lsn <= slot_lsn
4664 {
4665 // Tree already holds an equal-or-newer version.
4666 return Ok(false);
4667 }
4668 let is_new = if let Some(cmp) = key_comparator {
4669 // Comparator path: fall back to owned-Vec variant.
4670 let (_idx, new) = bin.insert_cmp(
4671 key.to_vec(),
4672 lsn,
4673 data.map(|d| d.to_vec()),
4674 cmp.as_ref(),
4675 );
4676 new
4677 } else if key_prefixing {
4678 let (_idx, new) =
4679 bin.insert_with_prefix_slice(key, lsn, data);
4680 new
4681 } else {
4682 // key_prefixing disabled: store full key verbatim.
4683 // Ref: IN.java computeKeyPrefix ~line 2456.
4684 let (_idx, new) = bin.insert_raw(
4685 key.to_vec(),
4686 lsn,
4687 data.map(|d| d.to_vec()),
4688 );
4689 new
4690 };
4691 bin.dirty = true;
4692 Ok(is_new)
4693 }
4694 TreeNode::Internal(_) => Err(TreeError::SplitRequired),
4695 }
4696 } else {
4697 let (child_index, n_entries_at_level, child_arc) =
4698 match &*parent_guard {
4699 TreeNode::Internal(n) => {
4700 let mut idx = 0usize;
4701 for (i, entry) in n.entries.iter().enumerate() {
4702 if i == 0 {
4703 idx = 0;
4704 } else {
4705 let ord = match key_comparator {
4706 Some(cmp) => cmp(entry.key.as_slice(), key),
4707 None => entry.key.as_slice().cmp(key),
4708 };
4709 if ord != std::cmp::Ordering::Greater {
4710 idx = i;
4711 } else {
4712 break;
4713 }
4714 }
4715 }
4716 let child =
4717 n.get_child(idx).ok_or(TreeError::SplitRequired)?;
4718 (idx, n.entries.len(), child)
4719 }
4720 TreeNode::Bottom(_) => {
4721 return Err(TreeError::SplitRequired);
4722 }
4723 };
4724
4725 let all_left = all_left_so_far && child_index == 0;
4726 let all_right = all_right_so_far
4727 && child_index == n_entries_at_level.saturating_sub(1);
4728
4729 let child_full = {
4730 let g = child_arc.read();
4731 g.get_n_entries() >= max_entries
4732 };
4733
4734 if child_full {
4735 let hint = match (all_left, all_right) {
4736 (true, _) => SplitHint::AllLeft,
4737 (_, true) => SplitHint::AllRight,
4738 _ => SplitHint::Normal,
4739 };
4740 drop(parent_guard);
4741 Self::split_child(
4742 node_arc,
4743 child_index,
4744 max_entries,
4745 lsn,
4746 hint,
4747 key,
4748 key_comparator,
4749 key_prefixing,
4750 // Recovery redo path: the listener is not active during
4751 // log replay (the evictor is wired AFTER recovery, and
4752 // the INList is rebuilt separately). EVICTOR-RECLAIM-1
4753 // registration happens on the live insert path.
4754 None,
4755 )?;
4756 return Self::redo_insert_recursive_inner(
4757 node_arc,
4758 key,
4759 data,
4760 lsn,
4761 max_entries,
4762 key_comparator,
4763 key_prefixing,
4764 all_left_so_far,
4765 all_right_so_far,
4766 );
4767 }
4768
4769 let r = Self::redo_insert_recursive_inner(
4770 &child_arc,
4771 key,
4772 data,
4773 lsn,
4774 max_entries,
4775 key_comparator,
4776 key_prefixing,
4777 all_left,
4778 all_right,
4779 );
4780 drop(parent_guard);
4781 r
4782 }
4783 }
4784
4785 /// Pre-warm the tree's internal `Vec<BinEntry>` capacity before a redo
4786 /// pass that will insert approximately `n` records.
4787 ///
4788 /// If the tree is empty, this is a no-op (there is no BIN yet to reserve
4789 /// capacity on). If the tree already has a root BIN (from a previous
4790 /// checkpoint), reserves `n.min(max_entries_per_node)` additional slots
4791 /// in that BIN's entries vector, eliminating the resize-double cycle
4792 /// during the redo loop.
4793 ///
4794 /// Wave 11-K optimisation (Fix 3).
4795 pub fn reserve_redo_capacity(&self, n: usize) {
4796 if n == 0 {
4797 return;
4798 }
4799 let root = match self.get_root() {
4800 Some(r) => r,
4801 None => return,
4802 };
4803 // Descend to the leftmost BIN and reserve there.
4804 let mut arc = root;
4805 loop {
4806 let guard = arc.read();
4807 match &*guard {
4808 TreeNode::Bottom(bin_guard) => {
4809 let additional = n
4810 .min(self.max_entries_per_node)
4811 .saturating_sub(bin_guard.entries.len());
4812 drop(guard);
4813 let mut wguard = arc.write();
4814 if let TreeNode::Bottom(bin) = &mut *wguard {
4815 bin.entries.reserve(additional);
4816 }
4817 return;
4818 }
4819 TreeNode::Internal(inner) => {
4820 let child = inner.get_child(0);
4821 drop(guard);
4822 match child {
4823 Some(c) => arc = c,
4824 None => return,
4825 }
4826 }
4827 }
4828 }
4829 }
4830
4831 /// Get the first (leftmost) BIN in the tree.
4832 ///
4833 /// Descends to the leftmost BIN by
4834 /// always following the first child slot at each upper IN level.
4835 pub fn get_first_node(&self) -> Option<SearchResult> {
4836 let mut guard: NodeArcReadGuard = self.get_root()?.read_arc();
4837
4838 loop {
4839 if guard.is_bin() {
4840 let n = guard.get_n_entries();
4841 if n == 0 {
4842 return None;
4843 }
4844 // TREE-F1: return the first LIVE slot, skipping known_deleted
4845 // slots (CursorImpl.java:2062-2064). If the leftmost BIN is
4846 // entirely KD during the reconstitution window the cursor's
4847 // get_first falls through to its cross-BIN advance.
4848 if let TreeNode::Bottom(b) = &*guard {
4849 match (0..b.entries.len()).find(|&i| b.slot_is_live(i)) {
4850 Some(i) => {
4851 return Some(SearchResult::with_values(
4852 true, i as i32, false,
4853 ));
4854 }
4855 None => return None,
4856 }
4857 }
4858 return Some(SearchResult::with_values(true, 0, false));
4859 }
4860
4861 // Capture the leftmost child Arc while holding `guard`, then
4862 // hand-over-hand: take the child read lock before releasing
4863 // the parent's. Same race fix as `Tree::search`.
4864 let next_arc = match &*guard {
4865 TreeNode::Internal(n_node) => n_node.get_child(0)?,
4866 _ => return None,
4867 };
4868 let next_guard = next_arc.read_arc();
4869 drop(guard);
4870 guard = next_guard;
4871 }
4872 }
4873
4874 /// Get the last (rightmost) BIN in the tree.
4875 ///
4876 /// Descends to the rightmost BIN by
4877 /// always following the last child slot at each upper IN level.
4878 pub fn get_last_node(&self) -> Option<SearchResult> {
4879 let mut guard: NodeArcReadGuard = self.get_root()?.read_arc();
4880
4881 loop {
4882 if guard.is_bin() {
4883 let n = guard.get_n_entries();
4884 if n == 0 {
4885 return None;
4886 }
4887 // TREE-F1: return the last LIVE slot, skipping known_deleted
4888 // slots (CursorImpl.java:2062-2064).
4889 if let TreeNode::Bottom(b) = &*guard {
4890 match (0..b.entries.len())
4891 .rev()
4892 .find(|&i| b.slot_is_live(i))
4893 {
4894 Some(i) => {
4895 return Some(SearchResult::with_values(
4896 true, i as i32, false,
4897 ));
4898 }
4899 None => return None,
4900 }
4901 }
4902 return Some(SearchResult::with_values(
4903 true,
4904 (n - 1) as i32,
4905 false,
4906 ));
4907 }
4908
4909 // Capture the rightmost child Arc while holding `guard`, then
4910 // hand-over-hand: take the child read lock before releasing
4911 // the parent's. Same race fix as `Tree::search`.
4912 let next_arc = match &*guard {
4913 TreeNode::Internal(n_node) => {
4914 n_node.get_child(n_node.entries.len().saturating_sub(1))?
4915 }
4916 _ => return None,
4917 };
4918 let next_guard = next_arc.read_arc();
4919 drop(guard);
4920 guard = next_guard;
4921 }
4922 }
4923
4924 /// Returns the number of root splits that have occurred.
4925 pub fn get_root_splits(&self) -> u64 {
4926 self.root_splits.load(Ordering::Relaxed)
4927 }
4928
4929 /// Returns the number of relatches required.
4930 pub fn get_relatches_required(&self) -> u64 {
4931 self.relatches_required.load(Ordering::Relaxed)
4932 }
4933
4934 /// Delete a key from the tree.
4935 ///
4936 /// Traverses the tree to find the BIN that should contain the key, then
4937 /// removes the entry. Returns true if the key was found and removed.
4938 ///
4939 /// Delete path in `Tree` from the.
4940 ///
4941 /// In-memory removal only — WAL logging for deletes is handled by the
4942 /// cursor layer (`cursor_impl.rs::log_ln_write`) before this is called,
4943 /// matching separation between LN logging and tree mutation.
4944 pub fn delete(&self, key: &[u8]) -> bool {
4945 let root = match self.get_root() {
4946 Some(r) => r,
4947 None => return false,
4948 };
4949
4950 // F8 consistency: insert accounts key + data + BIN_ENTRY_OVERHEAD; delete must
4951 // subtract the SAME (data_len was previously omitted, leaking
4952 // data_len from the cache counter on every delete and biasing the
4953 // evictor's over-budget view). Peek the data length before deleting.
4954 let data_len = if self.memory_counter.is_some() {
4955 self.search_with_data(key)
4956 .filter(|sf| sf.found)
4957 .and_then(|sf| sf.data.as_ref().map(|d| d.len()))
4958 .unwrap_or(0)
4959 } else {
4960 0
4961 };
4962
4963 let deleted =
4964 Self::delete_recursive(&root, key, self.key_comparator.as_ref());
4965
4966 // Update the memory counter when an entry is removed.
4967 // IN.updateMemorySize(-delta) → MemoryBudget.updateTreeMemoryUsage(-delta).
4968 if deleted && let Some(counter) = &self.memory_counter {
4969 let delta = (key.len() + data_len + BIN_ENTRY_OVERHEAD) as i64;
4970 counter.fetch_sub(delta, Ordering::Relaxed);
4971 }
4972
4973 deleted
4974 }
4975
4976 /// Recursive helper for `delete`: descend to the BIN that holds `key`
4977 /// and remove it.
4978 fn delete_recursive(
4979 node_arc: &Arc<RwLock<TreeNode>>,
4980 key: &[u8],
4981 key_comparator: Option<&KeyComparatorFn>,
4982 ) -> bool {
4983 // Latch coupling, mirroring `insert_recursive`. Without this,
4984 // delete has the same "BIN split out from under us" race: thread
4985 // A finds child_arc as the target BIN under parent.read(), drops
4986 // the lock, and another thread runs split_child(parent, …) that
4987 // moves the target key into the new sibling. A then takes
4988 // child_arc.write(), looks for the key in the (now left-half)
4989 // BIN, doesn't find it, and returns `false`. The caller treats
4990 // the `false` as "key was not present", but the key is actually
4991 // still in the tree (in the sibling). Subsequent operations
4992 // observe a stale record that should have been deleted —
4993 // semantically a lost delete.
4994 let parent_guard = node_arc.read();
4995 let is_bin = parent_guard.is_bin();
4996 let child_arc = if !is_bin {
4997 match &*parent_guard {
4998 TreeNode::Internal(n) => {
4999 // Find child slot with largest key <= search key
5000 let mut idx = 0usize;
5001 for (i, entry) in n.entries.iter().enumerate() {
5002 if i == 0 {
5003 idx = 0;
5004 } else {
5005 let ord = match key_comparator {
5006 Some(cmp) => cmp(entry.key.as_slice(), key),
5007 None => entry.key.as_slice().cmp(key),
5008 };
5009 if ord != std::cmp::Ordering::Greater {
5010 idx = i;
5011 } else {
5012 break;
5013 }
5014 }
5015 }
5016 n.get_child(idx)
5017 }
5018 _ => None,
5019 }
5020 } else {
5021 None
5022 };
5023
5024 if is_bin {
5025 // Drop the read lock before taking the write lock; the outer
5026 // call frame still holds the parent read lock so a concurrent
5027 // split_child cannot run on this BIN's parent until we unwind.
5028 drop(parent_guard);
5029 let mut g = node_arc.write();
5030 match &mut *g {
5031 TreeNode::Bottom(bin) => {
5032 if let Some(cmp) = key_comparator {
5033 bin.delete_cmp(key, cmp.as_ref())
5034 } else {
5035 // Entries store compressed (suffix) keys when key_prefix
5036 // is non-empty. Compress the search key before comparing.
5037 //
5038 // The caller is not required to ensure that `key`
5039 // shares this BIN's learned `key_prefix` — a stray
5040 // delete of a key that was never present (or that
5041 // sits under a different prefix) is legal and must
5042 // simply return `false`. Calling `compress_key`
5043 // unconditionally would `debug_assert!`-panic on
5044 // such inputs, so guard it the same way the cursor
5045 // path does.
5046 if !bin.key_prefix.is_empty()
5047 && !key.starts_with(bin.key_prefix.as_slice())
5048 {
5049 return false;
5050 }
5051 let suffix = bin.compress_key(key);
5052 match bin.key_binary_search(suffix.as_slice()) {
5053 Ok(idx) => {
5054 bin.entries.remove(idx);
5055 bin.keys.remove(idx); // T-2
5056 bin.lsn_rep.remove_shift(idx); // T-3
5057 // Mark dirty after any modification.
5058 bin.dirty = true;
5059 true
5060 }
5061 Err(_) => false,
5062 }
5063 }
5064 }
5065 _ => false,
5066 }
5067 } else {
5068 // Descend with parent_guard still held; the recursion will
5069 // hold its own read lock and drop ours after it returns.
5070 let r = match child_arc {
5071 Some(child) => {
5072 Self::delete_recursive(&child, key, key_comparator)
5073 }
5074 None => false,
5075 };
5076 drop(parent_guard);
5077 r
5078 }
5079 }
5080
5081 // ========================================================================
5082 // B-tree Merge / Compress
5083 // ========================================================================
5084
5085 /// Merge under-full sibling BIN pairs and remove empty subtrees.
5086 ///
5087 /// `INCompressor` / `Tree.compressInternal()` logic.
5088 ///
5089 /// merges two adjacent siblings when their combined entry count is
5090 /// ≤ `max_entries_per_node` (the merge threshold equal to the node
5091 /// capacity). The left sibling's entries are prepended into the right
5092 /// sibling; the parent key slot pointing at the left sibling is then
5093 /// removed from the parent IN with `deleteEntry`. If the parent IN
5094 /// becomes empty after the removal the process repeats recursively up
5095 /// the tree.
5096 ///
5097 /// This implementation performs a single post-order walk so that each
5098 /// level is compressed after all its children have been compressed.
5099 pub fn compress(&self) {
5100 let root = match self.get_root() {
5101 Some(r) => r,
5102 None => return,
5103 };
5104 Self::compress_node(&root, self.max_entries_per_node);
5105 }
5106
5107 // ── DST BIN-split gate shims (shuttle-only) ─────────────────────────────
5108 //
5109 // These expose the private split / merge-clear primitives so the shuttle
5110 // harness (`crates/noxu-tree/tests/shuttle_bin_split.rs`) can race them on
5111 // ONE shared child — the exact check-then-act interleaving that let the
5112 // BIN-split bug (`.agent/archived-audits/bench/
5113 // bug-bin-split-concurrency.md`) escape into a 96-thread benchmark instead
5114 // of DST. Compiled ONLY under `--cfg noxu_shuttle`; production never sees
5115 // them (zero change, verified by `cargo tree`).
5116
5117 /// The node capacity / split-and-merge threshold for this tree.
5118 #[cfg(noxu_shuttle)]
5119 pub fn shuttle_max_entries(&self) -> usize {
5120 self.max_entries_per_node
5121 }
5122
5123 /// Drive `split_child(parent, child_index)` with default (no-comparator,
5124 /// no-prefix, no-listener) parameters — the same call the insert path
5125 /// makes after it has dropped the parent read lock (the drop→reacquire
5126 /// window where the race opens).
5127 #[cfg(noxu_shuttle)]
5128 pub fn shuttle_split_child(
5129 parent: &Arc<RwLock<TreeNode>>,
5130 child_index: usize,
5131 max_entries: usize,
5132 insert_key: &[u8],
5133 ) -> Result<(), TreeError> {
5134 Self::split_child(
5135 parent,
5136 child_index,
5137 max_entries,
5138 Lsn::new(1, 999),
5139 SplitHint::Normal,
5140 insert_key,
5141 None, // no comparator
5142 false, // key_prefixing off
5143 None, // no InListListener
5144 )
5145 }
5146
5147 /// Simulate the racing INCompressor merge that CLEARS a child in place
5148 /// (`compress_node`'s `entries.clear()` on the merged-away left sibling),
5149 /// under the child's write lock — the stale state a second `split_child`
5150 /// observes after the caller's fullness check was already passed under the
5151 /// now-dropped parent read lock. Returns the entry count observed BEFORE
5152 /// clearing (so the harness can assert it raced a still-full child).
5153 #[cfg(noxu_shuttle)]
5154 pub fn shuttle_clear_child(child_arc: &Arc<RwLock<TreeNode>>) -> usize {
5155 let mut cg = child_arc.write();
5156 let before = cg.get_n_entries();
5157 match &mut *cg {
5158 TreeNode::Bottom(b) => {
5159 b.entries.clear();
5160 b.lsn_rep = LsnRep::Empty;
5161 b.keys = KeyRep::new();
5162 }
5163 TreeNode::Internal(n) => {
5164 n.entries.clear();
5165 n.lsn_rep = LsnRep::Empty;
5166 n.targets = TargetRep::None;
5167 }
5168 }
5169 before
5170 }
5171
5172 /// Drive one checkpoint dirty-BIN flush pass over this tree, faithful to
5173 /// the lock/dirty sequence in `noxu_recovery::Checkpointer::
5174 /// flush_one_tree_bins` — MINUS the WAL write, which needs a `LogManager`
5175 /// this pure-tree shuttle harness does not build.
5176 ///
5177 /// The sequence this preserves (the part shuttle must schedule against a
5178 /// concurrent insert):
5179 /// 1. `collect_dirty_bins(db_id)` under a tree/node READ lock — the
5180 /// snapshot of dirty BIN `Arc`s at checkpoint start.
5181 /// 2. per BIN: take the node WRITE lock; apply the JE X-8 early-exit
5182 /// guard (`!b.dirty && dirty_count()==0` → skip a node an evictor or
5183 /// a racing pass already flushed+cleared); otherwise "log" it by
5184 /// snapshotting its keys and calling `clear_dirty_after_full_log`
5185 /// (the real `flush_one_tree_bins` full-BIN path, sans the
5186 /// `lm.log(BIN, …)` between `serialize_full()` and
5187 /// `clear_dirty_after_full_log`).
5188 ///
5189 /// Returns the set of full keys captured in the flush (the keys present in
5190 /// each BIN at the instant it was write-locked and cleared) — i.e. the
5191 /// keys this checkpoint made durable. A shuttle harness races this against
5192 /// a concurrent insert and asserts the lost-dirty-node invariant: every
5193 /// inserted key is either in this captured set (flushed) OR still dirty in
5194 /// the tree afterwards (reflushed by the next checkpoint) — never silently
5195 /// clean-but-unflushed.
5196 ///
5197 /// The whole BIN mutation-and-clear runs under the SAME node write lock a
5198 /// concurrent `insert` takes, so the flush and the insert serialise on
5199 /// that latch; the capture-then-clear is atomic w.r.t. a racing insert on
5200 /// the same BIN. This is exactly why JE's checkpoint is consistent: the
5201 /// per-IN latch, not a global one, orders the snapshot-clear against
5202 /// concurrent tree mutation.
5203 #[cfg(noxu_shuttle)]
5204 pub fn shuttle_checkpoint_flush_bins(&self, db_id: u64) -> Vec<Vec<u8>> {
5205 // Step 1: snapshot dirty BINs under the read path (same call the
5206 // checkpointer makes).
5207 let dirty_bins = self.collect_dirty_bins(db_id);
5208 let mut captured: Vec<Vec<u8>> = Vec::new();
5209
5210 // Step 2: per-BIN write-lock, X-8 guard, capture keys, clear dirty.
5211 for (_node_db_id, bin_arc) in dirty_bins {
5212 let mut bin_guard = bin_arc.write();
5213 let b = match &mut *bin_guard {
5214 TreeNode::Bottom(b) => b,
5215 _ => continue,
5216 };
5217 let dirty = b.dirty_count();
5218 // JE X-8 early exit: a node already flushed+cleared between the
5219 // snapshot and this write-lock acquisition.
5220 if !b.dirty && dirty == 0 {
5221 continue;
5222 }
5223 // "Full BIN" path: capture every key (what serialize_full would
5224 // have written to the WAL) BEFORE clearing dirty — atomic under
5225 // the node write lock.
5226 for i in 0..b.entries.len() {
5227 if let Some(k) = b.get_full_key(i) {
5228 captured.push(k);
5229 }
5230 }
5231 b.clear_dirty_after_full_log(Lsn::new(1, 1));
5232 }
5233 captured
5234 }
5235
5236 /// Snapshot every full key currently present in the tree together with
5237 /// whether it would be reflushed by the next checkpoint — i.e. whether its
5238 /// slot is dirty OR its containing BIN is dirty. A key that is present but
5239 /// NOT dirty (slot clean AND BIN clean) has been captured by a checkpoint
5240 /// full-log; a key that is present AND dirty will be picked up by the next
5241 /// `collect_dirty_bins` pass.
5242 ///
5243 /// The shuttle recovery-vs-mutation gate uses this to assert the
5244 /// LOST-DIRTY-NODE invariant: every concurrently-inserted key is EITHER in
5245 /// the checkpoint's captured set OR still dirty here — never present but
5246 /// silently clean-yet-unflushed (the lost-dirty-node bug, where a
5247 /// checkpoint clears the dirty flag without having captured the slot).
5248 ///
5249 /// Walks under READ locks only (no mutation), reusing the same recursive
5250 /// descent shape as `collect_dirty_bins`.
5251 #[cfg(noxu_shuttle)]
5252 pub fn shuttle_key_dirty_states(&self) -> Vec<(Vec<u8>, bool)> {
5253 let mut out: Vec<(Vec<u8>, bool)> = Vec::new();
5254 if let Some(root) = self.get_root() {
5255 Self::shuttle_key_dirty_states_recursive(&root, &mut out);
5256 }
5257 out
5258 }
5259
5260 #[cfg(noxu_shuttle)]
5261 fn shuttle_key_dirty_states_recursive(
5262 node_arc: &Arc<RwLock<TreeNode>>,
5263 out: &mut Vec<(Vec<u8>, bool)>,
5264 ) {
5265 let guard = node_arc.read();
5266 match &*guard {
5267 TreeNode::Bottom(b) => {
5268 let bin_dirty = b.dirty;
5269 for i in 0..b.entries.len() {
5270 if let Some(k) = b.get_full_key(i) {
5271 // Reflushed next checkpoint iff the slot is dirty or
5272 // the whole BIN is dirty.
5273 let dirty = bin_dirty || b.entries[i].dirty;
5274 out.push((k, dirty));
5275 }
5276 }
5277 }
5278 TreeNode::Internal(n) => {
5279 let children: Vec<Arc<RwLock<TreeNode>>> =
5280 n.resident_children();
5281 drop(guard);
5282 for child in children {
5283 Self::shuttle_key_dirty_states_recursive(&child, out);
5284 }
5285 }
5286 }
5287 }
5288
5289 /// Recursive post-order compress helper.
5290 ///
5291 /// Visits children first (post-order), then scans adjacent child
5292 /// pairs in the current IN and merges them when the merge condition
5293 /// holds: `left.n_entries + right.n_entries <= max_entries`.
5294 ///
5295 /// After merging, the parent entry for the left sibling is deleted.
5296 /// The loop restarts after each merge so that newly under-full pairs
5297 /// created by previous merges are also considered.
5298 fn compress_node(node_arc: &Arc<RwLock<TreeNode>>, max_entries: usize) {
5299 // Collect child arcs to recurse without holding the node lock.
5300 let children: Vec<Arc<RwLock<TreeNode>>> = {
5301 let g = node_arc.read();
5302 match &*g {
5303 TreeNode::Internal(n) => n.resident_children(),
5304 // BINs are leaves; nothing to compress at this level.
5305 TreeNode::Bottom(_) => return,
5306 }
5307 };
5308
5309 // Post-order: recurse into every child before working on this level.
5310 for child in &children {
5311 Self::compress_node(child, max_entries);
5312 }
5313
5314 // Compress the current IN level: merge adjacent under-full children.
5315 // Repeat until a full pass produces no merges.
5316 loop {
5317 let n_entries = {
5318 let g = node_arc.read();
5319 g.get_n_entries()
5320 };
5321
5322 let mut merged_any = false;
5323
5324 // `i` is the index of the *left* candidate; right is at `i+1`.
5325 let mut i = 0usize;
5326 while i + 1 < n_entries {
5327 // Fetch left and right child arcs.
5328 let (left_arc, right_arc) = {
5329 let g = node_arc.read();
5330 match &*g {
5331 TreeNode::Internal(p) => {
5332 let l = p.get_child(i);
5333 let r = p.get_child(i + 1);
5334 match (l, r) {
5335 (Some(l), Some(r)) => (l, r),
5336 _ => {
5337 i += 1;
5338 continue;
5339 }
5340 }
5341 }
5342 TreeNode::Bottom(_) => return,
5343 }
5344 };
5345
5346 let left_n = { left_arc.read().get_n_entries() };
5347 let right_n = { right_arc.read().get_n_entries() };
5348
5349 // merge condition: combined count fits within one node.
5350 if left_n + right_n > max_entries {
5351 i += 1;
5352 continue;
5353 }
5354
5355 // Determine node kind from left child.
5356 let left_is_bin = { left_arc.read().is_bin() };
5357
5358 if left_is_bin {
5359 // BIN merge: decompress left entries to full keys, then
5360 // prepend into right BIN (also decompressed), and finally
5361 // recompute the merged BIN's prefix.
5362 // merge left into right, then
5363 // recalcKeyPrefix on the merged node.
5364 let left_full_entries: Vec<BinEntry> = {
5365 {
5366 let g = left_arc.read();
5367 match &*g {
5368 TreeNode::Bottom(b) => (0..b.entries.len())
5369 .map(|j| BinEntry {
5370 data: b.entries[j].data.clone(),
5371 known_deleted: b.entries[j]
5372 .known_deleted,
5373 dirty: b.entries[j].dirty,
5374 expiration_time: b.entries[j]
5375 .expiration_time,
5376 })
5377 .collect(),
5378 _ => {
5379 i += 1;
5380 continue;
5381 }
5382 }
5383 }
5384 };
5385 // T-3 / T-2: capture left's per-slot LSNs and FULL keys.
5386 let (left_full_lsns, left_full_keys): (
5387 Vec<Lsn>,
5388 Vec<Vec<u8>>,
5389 ) = {
5390 let g = left_arc.read();
5391 match &*g {
5392 TreeNode::Bottom(b) => (
5393 (0..b.entries.len())
5394 .map(|j| b.get_lsn(j))
5395 .collect(),
5396 (0..b.entries.len())
5397 .map(|j| {
5398 b.get_full_key(j).unwrap_or_default()
5399 })
5400 .collect(),
5401 ),
5402 _ => (Vec::new(), Vec::new()),
5403 }
5404 };
5405 {
5406 {
5407 let mut g = right_arc.write();
5408 match &mut *g {
5409 TreeNode::Bottom(rb) => {
5410 // Decompress right entries to full keys.
5411 let right_full: Vec<BinEntry> = (0..rb
5412 .entries
5413 .len())
5414 .map(|j| BinEntry {
5415 data: rb.entries[j].data.clone(),
5416 known_deleted: rb.entries[j]
5417 .known_deleted,
5418 dirty: rb.entries[j].dirty,
5419 expiration_time: rb.entries[j]
5420 .expiration_time,
5421 })
5422 .collect();
5423 // T-3 / T-2: right's per-slot LSNs + keys.
5424 let right_full_lsns: Vec<Lsn> =
5425 (0..rb.entries.len())
5426 .map(|j| rb.get_lsn(j))
5427 .collect();
5428 let right_full_keys: Vec<Vec<u8>> =
5429 (0..rb.entries.len())
5430 .map(|j| {
5431 rb.get_full_key(j)
5432 .unwrap_or_default()
5433 })
5434 .collect();
5435 // Left entries are all smaller; prepend.
5436 let mut combined = left_full_entries;
5437 combined.extend(right_full);
5438 let mut combined_lsns = left_full_lsns;
5439 combined_lsns.extend(right_full_lsns);
5440 let mut combined_keys = left_full_keys;
5441 combined_keys.extend(right_full_keys);
5442 // Reset prefix and assign full keys.
5443 rb.key_prefix = Vec::new();
5444 rb.entries = combined;
5445 // T-3: rebuild the merged LSN array.
5446 rb.lsn_rep =
5447 LsnRep::from_lsns(&combined_lsns);
5448 // T-2: rebuild the merged key rep (Default;
5449 // recompute below compresses + compacts).
5450 rb.keys = KeyRep::from_keys(combined_keys);
5451 // Recompute prefix on merged BIN.
5452 if rb.entries.len() >= 2 {
5453 rb.recompute_key_prefix();
5454 } else {
5455 rb.keys
5456 .compact(rb.compact_max_key_length);
5457 }
5458 rb.dirty = true;
5459 }
5460 _ => {
5461 i += 1;
5462 continue;
5463 }
5464 }
5465 }
5466 }
5467 // Clear the now-merged left BIN.
5468 {
5469 let mut g = left_arc.write();
5470 if let TreeNode::Bottom(lb) = &mut *g {
5471 lb.entries.clear();
5472 lb.lsn_rep = LsnRep::Empty; // T-3
5473 lb.keys = KeyRep::new(); // T-2
5474 lb.key_prefix = Vec::new();
5475 lb.dirty = true;
5476 }
5477 }
5478 } else {
5479 // Upper-IN merge: prepend left's InEntries into right.
5480 // T-4: capture left's resident children alongside its
5481 // entries so they travel into the merged right IN.
5482 let (left_in_entries, left_children): (
5483 Vec<InEntry>,
5484 Vec<Option<ChildArc>>,
5485 ) = {
5486 let g = left_arc.read();
5487 match &*g {
5488 TreeNode::Internal(n) => {
5489 let children = (0..n.entries.len())
5490 .map(|j| n.get_child(j))
5491 .collect();
5492 (n.entries.clone(), children)
5493 }
5494 _ => {
5495 i += 1;
5496 continue;
5497 }
5498 }
5499 };
5500 // T-3: capture left's per-slot LSNs.
5501 let left_in_lsns: Vec<Lsn> = {
5502 let g = left_arc.read();
5503 match &*g {
5504 TreeNode::Internal(n) => (0..n.entries.len())
5505 .map(|j| n.get_lsn(j))
5506 .collect(),
5507 _ => Vec::new(),
5508 }
5509 };
5510 let n_left = left_in_entries.len();
5511 {
5512 {
5513 let mut g = right_arc.write();
5514 match &mut *g {
5515 TreeNode::Internal(rn) => {
5516 // Snapshot right's existing children, then
5517 // rebuild the merged entry + target arrays
5518 // (left half first, then right half).
5519 let right_children: Vec<Option<ChildArc>> =
5520 (0..rn.entries.len())
5521 .map(|j| rn.get_child(j))
5522 .collect();
5523 // T-3: snapshot right's LSNs too.
5524 let right_in_lsns: Vec<Lsn> =
5525 (0..rn.entries.len())
5526 .map(|j| rn.get_lsn(j))
5527 .collect();
5528 let mut combined = left_in_entries.clone();
5529 combined.append(&mut rn.entries);
5530 rn.entries = combined;
5531 // T-3: rebuild the merged LSN array.
5532 let mut combined_lsns =
5533 left_in_lsns.clone();
5534 combined_lsns.extend(right_in_lsns);
5535 rn.lsn_rep =
5536 LsnRep::from_lsns(&combined_lsns);
5537 rn.targets = TargetRep::None;
5538 for (j, c) in
5539 left_children.iter().enumerate()
5540 {
5541 if let Some(child) = c {
5542 rn.set_child(
5543 j,
5544 Some(child.clone()),
5545 );
5546 }
5547 }
5548 for (j, c) in
5549 right_children.into_iter().enumerate()
5550 {
5551 if c.is_some() {
5552 rn.set_child(n_left + j, c);
5553 }
5554 }
5555 rn.dirty = true;
5556 }
5557 _ => {
5558 i += 1;
5559 continue;
5560 }
5561 }
5562 }
5563 }
5564 // Update parent pointers for moved children.
5565 for child in left_children.into_iter().flatten() {
5566 let mut cg = child.write();
5567 cg.set_parent(Some(Arc::downgrade(&right_arc)));
5568 }
5569 // Clear the now-merged left IN.
5570 {
5571 let mut g = left_arc.write();
5572 if let TreeNode::Internal(ln) = &mut *g {
5573 ln.entries.clear();
5574 ln.lsn_rep = LsnRep::Empty; // T-3
5575 ln.targets = TargetRep::None;
5576 ln.dirty = true;
5577 }
5578 }
5579 }
5580
5581 // Remove the right sibling's parent slot and update
5582 // the left slot to point at the merged right child.
5583 //
5584 // We keep the LEFT slot's key (which is the correct minimum for
5585 // the merged BIN's range) and remove the RIGHT slot (i+1).
5586 // This avoids having to update the parent key when i == 0.
5587 {
5588 {
5589 let mut g = node_arc.write();
5590 match &mut *g {
5591 TreeNode::Internal(p) => {
5592 // Update left slot (i) to point at right_arc
5593 // (which now contains the merged entries).
5594 if i < p.entries.len() {
5595 p.set_child(i, Some(right_arc.clone()));
5596 }
5597 // Remove right slot (i+1) — it is now redundant.
5598 // T-4: remove_entry shifts the child array too.
5599 if i + 1 < p.entries.len() {
5600 p.remove_entry(i + 1);
5601 }
5602 p.dirty = true;
5603 }
5604 TreeNode::Bottom(_) => return,
5605 }
5606 }
5607 }
5608
5609 merged_any = true;
5610 // Advance i to check the merged BIN against its new right
5611 // sibling (the old slot i+2 is now at i+1).
5612 i += 1;
5613 let updated_n = { node_arc.read().get_n_entries() };
5614 if i + 1 >= updated_n {
5615 break;
5616 }
5617 }
5618
5619 if !merged_any {
5620 break;
5621 }
5622 }
5623 }
5624
5625 // ========================================================================
5626 // BIN slot compression
5627 // ========================================================================
5628
5629 /// Compress deleted slots from a BIN node, then prune it from its parent
5630 /// IN when it becomes empty.
5631 ///
5632 /// (the in-place slot-removal
5633 /// path, NOT the sibling-merge path handled by `compress()`).
5634 ///
5635 /// # Algorithm
5636 ///
5637 /// 1. If the BIN is a delta, skip — deltas cannot be compressed.
5638 /// 2. Remove all slots where `entry.known_deleted` is true. This mirrors
5639 /// `bin.compress(!bin.shouldLogDelta(), localTracker)`.
5640 /// 3. If the BIN is now empty, remove it from its parent IN. This mirrors
5641 /// `pruneBIN(db, binRef, idKey)` → `tree.delete(idKey)`.
5642 ///
5643 /// # Arguments
5644 ///
5645 /// * `bin_arc` — the BIN to compress (must be a `TreeNode::Bottom`).
5646 ///
5647 /// # Returns
5648 ///
5649 /// `true` if compression made progress (slots were removed or the BIN was
5650 /// pruned), `false` if the BIN was skipped (delta, no cursors issue, etc.).
5651 pub fn compress_bin(&self, bin_arc: &Arc<RwLock<TreeNode>>) -> bool {
5652 self.compress_bin_with_lock_check(bin_arc, None)
5653 }
5654
5655 /// Like [`compress_bin`](Self::compress_bin), but consults a caller-supplied
5656 /// `is_locked` predicate before physically removing each `known_deleted`
5657 /// slot. If `is_locked(slot_lsn)` returns `true`, the slot is SKIPPED
5658 /// (left for a later compression pass after the locking txn resolves).
5659 ///
5660 /// This is the faithful port of JE `BIN.compress` (BIN.java:1141-1172):
5661 ///
5662 /// > We have to be able to lock the LN before we can compress the entry.
5663 /// > If we can't, then skip over it. ... it is more efficient to call
5664 /// > `isLockUncontended` than to actually lock the LN, since we would
5665 /// > release the lock immediately.
5666 ///
5667 /// ```text
5668 /// if (lsn != DbLsn.NULL_LSN &&
5669 /// !lockManager.isLockUncontended(lsn)) {
5670 /// anyLocked = true;
5671 /// continue;
5672 /// }
5673 /// ```
5674 ///
5675 /// JE's `isLockUncontended(lsn)` (LockManager.java:692) returns
5676 /// `nWaiters() == 0 && nOwners() == 0`. Our `is_locked(lsn)` is its
5677 /// inverse: the dbi layer supplies a closure over the `LockManager` that
5678 /// returns `true` iff the slot's LSN has any owner or waiter
5679 /// (`LockManager::get_lock_info(lsn) != (0, 0)`). A `NULL_LSN` slot is
5680 /// always discardable without locking (JE: "Can discard a NULL_LSN entry
5681 /// without locking"), so we never invoke the predicate for it.
5682 ///
5683 /// # Layering (noxu-tree -/-> noxu-txn)
5684 ///
5685 /// The predicate is a `&dyn Fn(u64) -> bool`, NOT a `LockManager`
5686 /// reference, so noxu-tree never depends on noxu-txn. The lock knowledge
5687 /// lives entirely in the dbi-supplied closure.
5688 ///
5689 /// # Lock ordering (no deadlock)
5690 ///
5691 /// `is_locked` is invoked while this method holds the **BIN write latch**.
5692 /// The dbi closure calls `LockManager::get_lock_info`, which takes a
5693 /// lock-table *shard* mutex for a single, non-blocking critical section
5694 /// and releases it before returning — it never waits and never re-enters
5695 /// the tree. The LockManager has no edge back into a BIN latch (lock
5696 /// acquisition descends the tree from the dbi/cursor layer, never the
5697 /// reverse). The only ordering is therefore BIN-latch -> shard-mutex,
5698 /// which is acyclic; no lock cycle exists, so the predicate cannot
5699 /// deadlock against the latch.
5700 ///
5701 /// When `is_locked` is `None` (recovery, BIN-delta replay, unit tests with
5702 /// no lock manager) behavior is identical to the historical
5703 /// `compress_bin`: every `known_deleted` slot is removed.
5704 pub fn compress_bin_with_lock_check(
5705 &self,
5706 bin_arc: &Arc<RwLock<TreeNode>>,
5707 is_locked: Option<&dyn Fn(u64) -> bool>,
5708 ) -> bool {
5709 // ---- Step 1: collect metadata without holding the write lock ----
5710 let (is_delta, n_entries, id_key) = {
5711 {
5712 let g = bin_arc.read();
5713 match &*g {
5714 TreeNode::Bottom(b) => {
5715 // Identifier key = first full key in the BIN
5716 // (the: bin.getIdentifierKey()).
5717 let id_key = b.get_full_key(0);
5718 (b.is_delta, b.entries.len(), id_key)
5719 }
5720 _ => return false, // not a BIN
5721 }
5722 }
5723 };
5724
5725 // If (bin.isBINDelta()) return; — deltas cannot be compressed.
5726 if is_delta {
5727 return false;
5728 }
5729
5730 // ---- Step 2: remove known-deleted slots) ----
5731 // We compress dirty slots too (compress_dirty_slots = true) because
5732 // we are not writing a BIN-delta here.
5733 let removed_any = {
5734 {
5735 let mut g = bin_arc.write();
5736 match &mut *g {
5737 TreeNode::Bottom(b) => {
5738 let before = b.entries.len();
5739 // BIN.compress(): walk backwards to remove
5740 // deleted slots without index confusion.
5741 //
5742 // IC-3 — JE `BIN.compress` (BIN.java:1141-1172) does
5743 // NOT compress a slot it cannot lock: "We have to be
5744 // able to lock the LN before we can compress the
5745 // entry. If we can't, then skip over it." JE calls
5746 // `lockManager.isLockUncontended(lsn)` and, on a
5747 // contended slot, does `anyLocked = true; continue;`.
5748 // We mirror that here via the optional `is_locked`
5749 // predicate (supplied by the dbi layer, closing over
5750 // the LockManager — see
5751 // `compress_bin_with_lock_check`). This removes the
5752 // previously fragile implicit invariant ("no code path
5753 // ever tombstones a slot before its txn commits"):
5754 // even if a future write path leaves an uncommitted,
5755 // write-locked `known_deleted` tombstone in a BinStub,
5756 // the predicate keeps the compressor from physically
5757 // removing a slot a live txn still references.
5758 //
5759 // When `is_locked` is `None` (recovery / BIN-delta
5760 // replay / lock-manager-less tests) behavior is
5761 // unchanged: every `known_deleted` slot is removed,
5762 // matching the historical safe-by-invariant path.
5763 let mut j = b.entries.len();
5764 while j > 0 {
5765 j -= 1;
5766 if b.entries[j].known_deleted {
5767 // IC-3 lock check (JE BIN.compress). A
5768 // NULL_LSN slot is always discardable without
5769 // locking (JE: "Can discard a NULL_LSN entry
5770 // without locking"), so we only consult the
5771 // predicate for a non-null LSN.
5772 if let Some(is_locked) = is_locked {
5773 let slot_lsn = b.get_lsn(j);
5774 if !slot_lsn.is_null()
5775 && is_locked(slot_lsn.as_u64())
5776 {
5777 // Slot still write-locked by an
5778 // in-flight txn — leave it for a later
5779 // pass (JE: anyLocked = true; continue).
5780 continue;
5781 }
5782 }
5783 // JE `IN.deleteEntry` (IN.java:3466): removing a
5784 // DIRTY slot must prohibit the next delta — a
5785 // delta only carries dirty slots, so the removal
5786 // would otherwise be silently lost. Force a
5787 // full BIN on the next log.
5788 if b.entries[j].dirty {
5789 b.prohibit_next_delta = true;
5790 }
5791 b.entries.remove(j);
5792 b.keys.remove(j); // T-2
5793 b.lsn_rep.remove_shift(j); // T-3
5794 b.dirty = true;
5795 }
5796 }
5797 // Recompute prefix after slot removal, since the
5798 // remaining keys may share a longer common prefix.
5799 // After compress(), call recalcKeyPrefix().
5800 if b.entries.len() >= 2 {
5801 b.recompute_key_prefix();
5802 } else if b.entries.len() < 2 {
5803 b.key_prefix = Vec::new();
5804 }
5805 b.entries.len() < before
5806 }
5807 _ => false,
5808 }
5809 }
5810 };
5811
5812 // ---- Step 3: prune empty BIN from parent ----
5813 // If (empty) pruneBIN(db, binRef, idKey) → tree.delete(idKey).
5814 // We only prune when the BIN is actually empty after compression.
5815 let now_empty = { bin_arc.read().get_n_entries() == 0 };
5816
5817 if now_empty {
5818 // pruneBIN re-descends to the SPECIFIC empty BIN and removes its
5819 // parent-IN slot ONLY IF the BIN is still empty (and has no
5820 // cursors and is not a delta) UNDER THE PARENT LATCH.
5821 //
5822 // We must NOT use `self.delete(&id_key)` here (IC-1): that
5823 // re-descends by key and removes whatever live entry now matches
5824 // `id_key`. Between reading `now_empty` (a fresh read lock taken
5825 // after the compression write lock was dropped) and acting on it,
5826 // a concurrent insert can repopulate this BIN; `self.delete` would
5827 // then drop a LIVE entry — tree corruption / lost write.
5828 //
5829 // JE `INCompressor.pruneBIN` (INCompressor.java ~line 502-510)
5830 // calls `tree.delete(idKey)`, and JE `Tree.delete` /
5831 // `searchDeletableSubTree` (Tree.java ~line 755-800) re-validates
5832 // `bin.getNEntries() != 0` → NODE_NOT_EMPTY (abort) and
5833 // `bin.nCursors() > 0` → CURSORS_EXIST (abort) while holding the
5834 // parent (branch) latch. `prune_empty_bin` reproduces exactly
5835 // that re-validation. See `prune_empty_bin` below.
5836 //
5837 // Note: we only attempt the prune if n_entries was > 0 before
5838 // compression (an already-empty BIN we never populated is left
5839 // alone, matching the pre-existing guard).
5840 if let Some(key) = id_key
5841 && n_entries > 0
5842 {
5843 self.prune_empty_bin(&key);
5844 }
5845 return true;
5846 }
5847
5848 removed_any
5849 }
5850
5851 /// Re-descend to the leaf BIN that should contain `id_key` and remove its
5852 /// parent-IN child slot ONLY IF the BIN is still safe to prune.
5853 ///
5854 /// This is the faithful port of JE `Tree.delete(idKey)` /
5855 /// `Tree.searchDeletableSubTree` (Tree.java ~line 755-800) as invoked by
5856 /// `INCompressor.pruneBIN` (INCompressor.java ~line 502-510). JE takes the
5857 /// branch-parent latch, re-descends to the specific empty BIN, and aborts
5858 /// the prune (removing NOTHING) if any of the following changed since the
5859 /// compressor observed the BIN as empty:
5860 ///
5861 /// * `bin.getNEntries() != 0` → `NodeNotEmptyException` (a concurrent
5862 /// insert repopulated the BIN — IC-1: we must NOT delete a live entry).
5863 /// * `bin.isBINDelta()` → `unexpectedState` (deltas are never empty).
5864 /// * `bin.nCursors() > 0` → `CursorsExistException` (a cursor is parked
5865 /// on the empty BIN; requeue rather than orphan the cursor).
5866 ///
5867 /// The re-check and the slot removal both happen while holding the
5868 /// **parent IN write latch**. Holding the parent write latch blocks every
5869 /// descender (insert / delete take `parent.read()` hand-over-hand), so a
5870 /// concurrent insert cannot reach the BIN between our re-check and the
5871 /// slot removal — the TOCTOU window IC-1 describes is closed.
5872 ///
5873 /// Returns `true` iff a parent-IN slot was removed, `false` otherwise
5874 /// (BIN repopulated, has a cursor, is a delta, vanished, or is the root —
5875 /// in every `false` case NOTHING is removed).
5876 pub fn prune_empty_bin(&self, id_key: &[u8]) -> bool {
5877 let root = match self.get_root() {
5878 Some(r) => r,
5879 None => return false,
5880 };
5881
5882 // If the root itself is the BIN (single-BIN tree) there is no parent
5883 // IN to remove a slot from. JE's searchDeletableSubTree returns null
5884 // ("the entire tree is empty") and keeps the root BIN; we do the same.
5885 if root.read().is_bin() {
5886 return false;
5887 }
5888
5889 // Descend by id_key tracking the IN that is the *parent of the leaf
5890 // BIN* and the child index within it. Hand-over-hand read coupling
5891 // keeps the descent consistent with concurrent splits, exactly like
5892 // `get_parent_bin_for_child_ln`.
5893 let (parent_arc, child_index) = {
5894 let mut parent_arc: Arc<RwLock<TreeNode>> = root.clone();
5895 let mut guard: NodeArcReadGuard = root.read_arc();
5896 loop {
5897 let (next_arc, idx) = match &*guard {
5898 TreeNode::Internal(n) => {
5899 if n.entries.is_empty() {
5900 return false;
5901 }
5902 let idx = self.upper_in_floor_index(&n.entries, id_key);
5903 match n.get_child(idx) {
5904 Some(c) => (c, idx),
5905 None => return false,
5906 }
5907 }
5908 TreeNode::Bottom(_) => {
5909 unreachable!("is_bin checked before / below")
5910 }
5911 };
5912 // Is the next node the leaf BIN? If so, `guard`'s node is the
5913 // parent IN we want and `idx` is the child slot.
5914 if next_arc.read().is_bin() {
5915 drop(guard);
5916 break (parent_arc, idx);
5917 }
5918 let next_guard = next_arc.read_arc();
5919 drop(guard);
5920 parent_arc = next_arc;
5921 guard = next_guard;
5922 }
5923 };
5924
5925 // ---- Re-validate and remove the slot UNDER THE PARENT WRITE LATCH ----
5926 // Holding parent.write() excludes all descenders (they need
5927 // parent.read()), so the BIN cannot be repopulated between the
5928 // re-check and the slot removal.
5929 let mut parent_guard = parent_arc.write();
5930 let pruned_bin_id;
5931 let removed_key_len = match &mut *parent_guard {
5932 TreeNode::Internal(p) => {
5933 let child = match p.get_child(child_index) {
5934 Some(c) => c,
5935 None => return false, // slot already vacated / invalid
5936 };
5937 // Re-validate the child BIN under the parent latch.
5938 {
5939 let cg = child.read();
5940 match &*cg {
5941 TreeNode::Bottom(b) => {
5942 // JE: bin.getNEntries() != 0 → NODE_NOT_EMPTY (abort).
5943 if !b.entries.is_empty() {
5944 return false;
5945 }
5946 // JE: bin.isBINDelta() → unexpectedState (abort).
5947 if b.is_delta {
5948 return false;
5949 }
5950 // JE: bin.nCursors() > 0 → CURSORS_EXIST (abort).
5951 if b.cursor_count > 0 {
5952 return false;
5953 }
5954 pruned_bin_id = b.node_id;
5955 }
5956 // A concurrent split could in principle have replaced
5957 // the child with an IN; never prune in that case.
5958 TreeNode::Internal(_) => return false,
5959 }
5960 }
5961 // Safe to prune: remove the BIN's slot from the parent IN.
5962 // Mirrors the parent-slot removal `Tree.delete` performs for
5963 // an empty BIN (Tree.java deleteEntry under the branch latch).
5964 // T-4: remove_entry shifts the node-level child array too.
5965 let removed = p.remove_entry(child_index);
5966 p.dirty = true;
5967 removed.key.len()
5968 }
5969 TreeNode::Bottom(_) => return false,
5970 };
5971 drop(parent_guard);
5972
5973 // JE: removing the BIN slot detaches the BIN from the tree; the
5974 // evictor must drop it from its LRU lists (Evictor.remove).
5975 self.note_removed(pruned_bin_id);
5976
5977 // Preserve the memory-counter bookkeeping that `self.delete` performed
5978 // (IN.updateMemorySize(-delta) → MemoryBudget.updateTreeMemoryUsage).
5979 // The pruned slot's key plus the fixed per-entry overhead matches the
5980 // `delete` accounting (key.len() + BIN_ENTRY_OVERHEAD).
5981 if let Some(counter) = &self.memory_counter {
5982 let delta = (removed_key_len + BIN_ENTRY_OVERHEAD) as i64;
5983 counter.fetch_sub(delta, Ordering::Relaxed);
5984 }
5985
5986 true
5987 }
5988
5989 /// Detach the resident child node `node_id` from its parent IN, dropping
5990 /// the strong `Arc` so the node is actually freed from memory, and return
5991 /// the heap bytes reclaimed (0 if not found / not detachable).
5992 ///
5993 /// This is the faithful port of JE `IN.detachNode(idx, updateLsn, newLsn)`
5994 /// (IN.java ~4019) as called from `Evictor.evict` (Evictor.java ~3035):
5995 /// `evict` measures `target.getBudgetedMemorySize()` and then
5996 /// `parent.detachNode(index, ...)` does `setTarget(idx, null)` to drop the
5997 /// child reference and `getInMemoryINs().remove(child)` to drop it from
5998 /// the INList.
5999 ///
6000 /// EV-13: before this method existed, the evictor credited
6001 /// `node_size_fn(node_id)` bytes back to the budget and removed the node
6002 /// from the LRU lists, but the parent's `InEntry.child` still held a
6003 /// strong `Arc` — so the node was never dropped from the heap. The budget
6004 /// over-credited (claimed bytes freed that were not), `cache_usage`
6005 /// drifted below reality, and the evictor under-fired. Detaching here
6006 /// drops the `Arc` for real and credits exactly the measured size.
6007 ///
6008 /// The detach happens **under the parent IN write latch** (JE detaches
6009 /// under the parent's latch), so no concurrent descender can re-cache the
6010 /// child between measurement and detach. The slot (key + LSN) is kept —
6011 /// only the in-memory `child` target is cleared — matching JE's
6012 /// `setTarget(idx, null)` which leaves the `ChildReference` LSN intact so
6013 /// the node can be re-fetched from the log later.
6014 ///
6015 /// Returns `0` if the node is not a resident child of any IN (e.g. it is
6016 /// the root, already detached, or was pinned and could not be latched).
6017 pub fn detach_node_by_id(&self, node_id: u64) -> u64 {
6018 let root = match self.get_root() {
6019 Some(r) => r,
6020 None => return 0,
6021 };
6022
6023 // The root has no parent IN to detach from (JE evicts the root via a
6024 // separate evictRoot path; we keep the root resident here).
6025 let root_id = {
6026 let g = root.read();
6027 match &*g {
6028 TreeNode::Internal(n) => n.node_id,
6029 TreeNode::Bottom(b) => b.node_id,
6030 }
6031 };
6032 if root_id == node_id {
6033 return 0;
6034 }
6035
6036 // Locate the parent IN and the child slot index.
6037 let (parent_arc, child_index) =
6038 match Self::find_parent_of_node_id(&root, node_id) {
6039 Some(p) => p,
6040 None => return 0,
6041 };
6042
6043 // ---- Measure + detach UNDER THE PARENT WRITE LATCH ----
6044 // Holding parent.write() excludes all descenders (they take
6045 // parent.read() hand-over-hand), so the child cannot be re-cached or
6046 // re-pinned between the measurement and the detach. Mirrors JE
6047 // detachNode running under the parent latch held by Evictor.evict.
6048 let mut parent_guard = parent_arc.write();
6049 let TreeNode::Internal(p) = &mut *parent_guard else {
6050 return 0; // parent is not an IN (concurrent restructure)
6051 };
6052 if child_index >= p.entries.len() {
6053 return 0;
6054 }
6055 // T-4: detach the cached child via the node-level INTargetRep, leaving
6056 // the slot's key/LSN intact for re-fetch (JE IN.setTarget(idx, null)).
6057 let child = match p.take_child(child_index) {
6058 Some(c) => c, // child Arc removed from the slot
6059 None => return 0, // already detached
6060 };
6061
6062 // Measure the child's real heap footprint while we still hold it.
6063 // JE: long evictedBytes = target.getBudgetedMemorySize().
6064 let freed = child.read().budgeted_memory_size();
6065
6066 // EV-14 re-fetch correctness: the parent slot LSN must point at the
6067 // child's CURRENT on-disk version so `child_at_or_fetch` re-reads the
6068 // right bytes (JE `IN.updateEntry(idx, newLsn)` is called whenever a
6069 // child is logged; the parent slot LSN tracks the child's LSN). The
6070 // evictor only fully evicts/detaches a CLEAN BIN (it logs+clears dirty
6071 // BINs via flush_dirty_node_to_log first, which sets `last_full_lsn`),
6072 // so the child's authoritative LSN is its `last_full_lsn`. Stamp it
6073 // into the parent slot before dropping the child; if it is null (the
6074 // child was never logged) leave the existing slot LSN intact rather
6075 // than writing a null — a never-logged clean child cannot occur on
6076 // the evict path, but be conservative.
6077 let child_full_lsn = match &*child.read() {
6078 TreeNode::Bottom(b) => b.last_full_lsn,
6079 TreeNode::Internal(_) => NULL_LSN,
6080 };
6081 if child_full_lsn != NULL_LSN {
6082 p.set_lsn(child_index, child_full_lsn);
6083 }
6084
6085 // Mark the parent dirty: the slot's in-memory target changed (JE
6086 // detachNode sets dirty when updateLsn; we conservatively mark dirty
6087 // so the parent is re-logged with the now-non-resident slot).
6088 p.dirty = true;
6089
6090 // Drop the strong Arc explicitly so the node is freed now (the slot's
6091 // `child` is already None). If any other resident path still held a
6092 // strong reference this would not free — but the tree is the sole
6093 // strong owner of a cached child, so this drops the last strong ref.
6094 drop(parent_guard);
6095 drop(child);
6096
6097 // JE: getInMemoryINs().remove(child) — drop it from the evictor LRU.
6098 self.note_removed(node_id);
6099
6100 // NOTE: the live tree-memory counter (`memory_counter`) is the SAME
6101 // `Arc<AtomicI64>` the evictor's Arbiter uses as `cache_usage`. The
6102 // evictor decrements it once via `Arbiter::release_memory(bytes)` for
6103 // the full eviction batch, so detach must NOT decrement here too —
6104 // that would double-credit and drive `cache_usage` below reality
6105 // (the very drift EV-13 fixes, in the other direction). We only
6106 // measure-and-free; the caller does the single counter update.
6107 freed
6108 }
6109
6110 /// Evict the root IN of this tree (EV-14).
6111 ///
6112 /// Faithful port of JE `Evictor.evictRoot` (Evictor.java:3050-3110) plus
6113 /// the `RootEvictor.doWork` + `Tree.withRootLatchedExclusive` framing
6114 /// (Evictor.java:2529-2576, Tree.java:508-517). Unlike a normal IN, the
6115 /// root has no parent slot to detach from; instead the *tree's* root
6116 /// reference is the equivalent of the `RootChildReference`, so eviction:
6117 ///
6118 /// 1. Latches the root reference exclusively (`rootLatch.acquireExclusive`
6119 /// via `withRootLatchedExclusive`).
6120 /// 2. Re-checks that the root is still resident and still evictable
6121 /// (no resident children, no pinned BIN — JE `RootEvictor.doWork`
6122 /// re-latches and re-checks `rootIN == target && rootIN.isRoot()`).
6123 /// 3. If the root is dirty, LOGS it first so the on-disk version is
6124 /// current and updates `root_log_lsn` to the new LSN (JE
6125 /// `evictRoot`: `long newLsn = target.log(...); rootRef.setLsn(newLsn)`).
6126 /// 4. Clears the in-memory root (`rootRef.clearTarget()` — JE leaves the
6127 /// `ChildReference` LSN intact; here `root_log_lsn` is that LSN) and
6128 /// `note_removed`s it from the evictor LRU (JE `inList.remove(target)`).
6129 ///
6130 /// On the next access `fetch_root_from_log` re-materializes the root from
6131 /// `root_log_lsn` (JE `Tree.getRootINRootAlreadyLatched` →
6132 /// `root.fetchTarget`).
6133 ///
6134 /// # Conditions (eviction is REFUSED, returning `None`, when)
6135 ///
6136 /// * there is no log manager wired (the root could never be re-fetched),
6137 /// * the tree has no resident root (already evicted),
6138 /// * the root has any resident child (JE only evicts a childless root —
6139 /// the `hasCachedChildren` skip in `processTarget`; a root with cached
6140 /// children would orphan them, the EV-6 invariant),
6141 /// * the root is a BIN pinned by a cursor (`cursor_count > 0`),
6142 /// * the root is dirty but we have no clean persisted version AND logging
6143 /// it fails, or
6144 /// * the root is clean but `root_log_lsn` is null (never logged — cannot
6145 /// be re-fetched; happens only for a brand-new unlogged tree).
6146 ///
6147 /// Returns `Some((freed_bytes, was_dirty))` on success, where `freed_bytes`
6148 /// is the root's measured heap footprint (JE
6149 /// `target.getBudgetedMemorySize()`) and `was_dirty` reports whether the
6150 /// root had to be logged (JE `rootEvictor.flushed`, which drives
6151 /// `nDirtyNodesEvicted` and `modifyDbRoot`).
6152 pub fn evict_root(&self, db_id: u64) -> Option<(u64, bool)> {
6153 // A root with no re-fetch path must never be made non-resident.
6154 self.log_manager.as_ref()?;
6155
6156 // JE `Tree.withRootLatchedExclusive(rootEvictor)`: hold the root latch
6157 // exclusively across the whole evict so no descender or splitter can
6158 // observe/install a half-evicted root. Acquiring `self.root.write()`
6159 // is the Noxu equivalent (it is the lock guarding the root pointer).
6160 let mut root_slot = self.root.write();
6161 let root_arc = root_slot.as_ref()?.clone();
6162
6163 // JE `RootEvictor.doWork`: re-latch the target and re-check the
6164 // conditions. We hold the node guard for the duration.
6165 let node_guard = root_arc.write();
6166
6167 // EV-6 / JE `processTarget` hasCachedChildren skip: a root with any
6168 // resident child must NOT be evicted (it would orphan the child).
6169 // EV-14 only evicts an *idle* root whose children are already
6170 // non-resident (or which is itself a leaf BIN).
6171 let (node_id, was_dirty, freed) = match &*node_guard {
6172 TreeNode::Internal(n) => {
6173 if !n.resident_children().is_empty() {
6174 return None; // has cached children — keep resident
6175 }
6176 (n.node_id, n.dirty, node_guard.budgeted_memory_size())
6177 }
6178 TreeNode::Bottom(b) => {
6179 if b.cursor_count > 0 {
6180 return None; // pinned by a cursor — keep resident
6181 }
6182 (
6183 b.node_id,
6184 b.dirty || b.dirty_count() > 0,
6185 node_guard.budgeted_memory_size(),
6186 )
6187 }
6188 };
6189
6190 // If dirty, log the root first so the on-disk version is current,
6191 // then record the new LSN as the root's re-fetch point (JE
6192 // `evictRoot`: target.log(...) + rootRef.setLsn(newLsn)).
6193 if was_dirty {
6194 let lm = self.log_manager.as_ref()?; // checked above; re-borrow
6195 let node_bytes = node_guard.write_to_bytes();
6196 let is_bin = node_guard.is_bin();
6197 let entry = noxu_log::entry::in_log_entry::InLogEntry::new(
6198 db_id, NULL_LSN, // prev_full_lsn
6199 NULL_LSN, // prev_delta_lsn
6200 node_bytes,
6201 );
6202 let mut buf = bytes::BytesMut::with_capacity(entry.log_size());
6203 entry.write_to_log(&mut buf);
6204 let entry_type = if is_bin {
6205 noxu_log::LogEntryType::BIN
6206 } else {
6207 noxu_log::LogEntryType::IN
6208 };
6209 // flush_required = true so the root's bytes are durable before we
6210 // drop the in-memory copy (JE logs synchronously in evictRoot).
6211 let new_lsn = match lm.log(
6212 entry_type,
6213 &buf,
6214 noxu_log::Provisional::No,
6215 true, // flush_required
6216 false, // fsync at next checkpoint
6217 ) {
6218 Ok(l) => l,
6219 Err(_) => return None, // could not log — keep the root resident
6220 };
6221 *self.root_log_lsn.write() = new_lsn;
6222 } else {
6223 // Clean root: it must already be re-fetchable. If it was never
6224 // logged (root_log_lsn null) we cannot evict it safely.
6225 if *self.root_log_lsn.read() == NULL_LSN {
6226 return None;
6227 }
6228 }
6229
6230 // JE `rootRef.clearTarget()` + `inList.remove(target)`: drop the
6231 // in-memory root and remove it from the evictor LRU. The root_log_lsn
6232 // is the surviving `ChildReference` LSN used to re-fetch it.
6233 drop(node_guard);
6234 *root_slot = None;
6235 drop(root_slot);
6236 self.note_removed(node_id);
6237
6238 Some((freed, was_dirty))
6239 }
6240
6241 /// Re-materialize an evicted root IN from its persisted `root_log_lsn`
6242 /// (EV-14, piece B).
6243 /// Faithful to JE `Tree.getRootINRootAlreadyLatched` (Tree.java:477-516)
6244 /// which calls `root.fetchTarget(database, null)` when the in-memory
6245 /// target is null. Idempotent and cheap when the root is already
6246 /// resident: returns the resident root without touching the log.
6247 ///
6248 /// Returns `None` only when the tree is genuinely empty (no resident root
6249 /// AND `root_log_lsn` is null) or when the re-fetch fails (no log manager,
6250 /// log read error, deserialize failure) — callers then see an empty tree,
6251 /// never wrong data.
6252 pub fn fetch_root_from_log(&self) -> Option<Arc<RwLock<TreeNode>>> {
6253 // Fast path: root already resident.
6254 if let Some(r) = self.root.read().clone() {
6255 return Some(r);
6256 }
6257 // Take the write lock and re-check (another thread may have re-fetched
6258 // it while we waited — JE upgrades the root latch the same way).
6259 let mut root_slot = self.root.write();
6260 if let Some(r) = root_slot.as_ref() {
6261 return Some(r.clone());
6262 }
6263 let log_lsn = *self.root_log_lsn.read();
6264 let node = self.fetch_node_from_log(log_lsn)?;
6265 let node_id = node.node_id();
6266 let arc = Arc::new(RwLock::new(node));
6267 *root_slot = Some(arc.clone());
6268 drop(root_slot);
6269 // JE: a fetched IN is added back to the INList (Evictor LRU).
6270 self.note_added(node_id);
6271 Some(arc)
6272 }
6273
6274 /// Return the resident child Arc for slot `idx` of `parent_arc`, fetching
6275 /// it from its slot LSN and installing it if it is not resident (EV-14 /
6276 /// EV-13 re-fetch on descent).
6277 ///
6278 /// Faithful to JE `ChildReference.fetchTarget` (and `IN.fetchTarget`):
6279 /// when a slot's in-memory target is null but its LSN is valid, the node
6280 /// is read back from the log and cached in the slot. Installing the
6281 /// fetched child requires the parent EX-latch, so this takes the parent
6282 /// write lock; the fast path (child already resident) takes only a read
6283 /// lock.
6284 ///
6285 /// Returns `None` only when the slot index is out of range, the slot has
6286 /// no valid LSN, or the log read/deserialize fails — callers then treat
6287 /// the descent as terminating in an empty subtree, never wrong data.
6288 fn child_at_or_fetch(
6289 &self,
6290 parent_arc: &Arc<RwLock<TreeNode>>,
6291 idx: usize,
6292 ) -> Option<ChildArc> {
6293 // Fast path: child already cached (read lock only).
6294 {
6295 let g = parent_arc.read();
6296 if let TreeNode::Internal(n) = &*g {
6297 if let Some(c) = n.get_child(idx) {
6298 return Some(c);
6299 }
6300 } else {
6301 return None; // BINs have no IN children
6302 }
6303 }
6304 // Slow path: fetch the child from its slot LSN under the parent
6305 // EX-latch (JE installs the fetched target under the IN latch).
6306 let mut g = parent_arc.write();
6307 let TreeNode::Internal(n) = &mut *g else {
6308 return None;
6309 };
6310 // Re-check: another thread may have fetched it while we upgraded.
6311 if let Some(c) = n.get_child(idx) {
6312 return Some(c);
6313 }
6314 if idx >= n.entries.len() {
6315 return None;
6316 }
6317 let child_lsn = n.get_lsn(idx);
6318 let node = self.fetch_node_from_log(child_lsn)?;
6319 let node_id = node.node_id();
6320 let arc: ChildArc = Arc::new(RwLock::new(node));
6321 n.set_child(idx, Some(arc.clone()));
6322 drop(g);
6323 // JE: a fetched IN is added back to the INList (Evictor LRU).
6324 self.note_added(node_id);
6325 Some(arc)
6326 }
6327
6328 /// Check whether a BIN node is a candidate for slot compression and,
6329 /// if so, trigger `compress_bin`.
6330 ///
6331 /// from (the opportunistic / lazy compression path).
6332 ///
6333 /// # Algorithm
6334 ///
6335 /// 1. Skip the BIN if it is a delta or has no defunct (known-deleted) slots.
6336 /// 2. If compression succeeds and the BIN becomes empty, it is pruned.
6337 ///
6338 /// # Returns
6339 ///
6340 /// `true` if compression was triggered (regardless of whether any slots
6341 /// were actually removed), `false` if the BIN does not need compression.
6342 pub fn maybe_compress_bin_and_parent(
6343 &self,
6344 bin_arc: &Arc<RwLock<TreeNode>>,
6345 ) -> bool {
6346 // Check whether the BIN has any deleted slots worth compressing.
6347 // lazyCompress: skip deltas and BINs with no defunct slots.
6348 let should_compress = {
6349 {
6350 let g = bin_arc.read();
6351 match &*g {
6352 TreeNode::Bottom(b) => {
6353 // Skip deltas (the: !in.isBIN() || in.isBINDelta()).
6354 if b.is_delta {
6355 false
6356 } else {
6357 // Check for any known-deleted slot
6358 // (the: for (int i=0; i < bin.getNEntries(); i++) {
6359 // if (bin.isDefunct(i)) { ... break; }
6360 // }).
6361 b.entries.iter().any(|e| e.known_deleted)
6362 }
6363 }
6364 _ => false,
6365 }
6366 }
6367 };
6368
6369 if !should_compress {
6370 return false;
6371 }
6372
6373 self.compress_bin(bin_arc)
6374 }
6375
6376 // ========================================================================
6377 // Latch-coupling validation
6378 // ========================================================================
6379
6380 /// Validate that `parent.entries[child_index].child` still points at
6381 /// `child_arc` after acquiring the child's latch.
6382 ///
6383 /// Re-latch validation step inside the
6384 /// `Tree.searchSplitsAllowed`: after a concurrent split the parent
6385 /// slot that previously held the child may have changed. Callers that
6386 /// plan to mutate the child must verify the parent-child link is still
6387 /// intact before proceeding.
6388 ///
6389 /// Returns `true` if the parent-child link is intact.
6390 pub fn validate_parent_child(
6391 parent: &Arc<RwLock<TreeNode>>,
6392 child_index: usize,
6393 child_arc: &Arc<RwLock<TreeNode>>,
6394 ) -> bool {
6395 let g = parent.read();
6396 match &*g {
6397 TreeNode::Internal(p) => match p.child_ref(child_index) {
6398 Some(stored) => Arc::ptr_eq(stored, child_arc),
6399 None => false,
6400 },
6401 TreeNode::Bottom(_) => false,
6402 }
6403 }
6404
6405 /// Search for the BIN that should contain `key`, with latch-coupling
6406 /// validation at every level of descent.
6407 ///
6408 /// .
6409 ///
6410 /// The difference from `search()` is that after obtaining the child
6411 /// arc we call `validate_parent_child` to confirm the parent still
6412 /// holds the expected Arc. If the link has been broken (e.g. by a
6413 /// concurrent split that relocated the child) the traversal restarts
6414 /// from the root.
6415 ///
6416 /// Returns a `SearchResult` if the key is (or should be) in the tree,
6417 /// `None` if the tree is empty.
6418 ///
6419 /// Same as [`Tree::search`] but exposes the hand-over-hand latch
6420 /// coupling explicitly. Kept as a public, equivalent API for
6421 /// callers (today only tests) that want to verify the
6422 /// latch-coupling behaviour against `search()` itself.
6423 ///
6424 /// Both `search()` and this method use the same `read_arc()`
6425 /// hand-over-hand: take the child read guard *before* dropping
6426 /// the parent guard, so a concurrent `split_child(parent, ..)`
6427 /// (which takes `parent.write()`) cannot run between when we
6428 /// captured the child Arc and when we entered the child. There
6429 /// is no validate-and-restart loop because the coupling makes
6430 /// the race unreachable.
6431 pub fn search_with_coupling(&self, key: &[u8]) -> Option<SearchResult> {
6432 let root = self.get_root()?;
6433 let mut guard: NodeArcReadGuard = root.read_arc();
6434
6435 loop {
6436 if guard.is_bin() {
6437 let index = guard.find_entry(key, true, true);
6438 let found = index >= 0 && (index & EXACT_MATCH != 0);
6439 return Some(SearchResult::with_values(
6440 found,
6441 index & 0xFFFF,
6442 false,
6443 ));
6444 }
6445
6446 let parent_arc = NodeArcReadGuard::rwlock(&guard).clone();
6447 let next_idx = match &*guard {
6448 TreeNode::Internal(n) => {
6449 if n.entries.is_empty() {
6450 return None;
6451 }
6452 let idx = self.upper_in_floor_index(&n.entries, key);
6453 match n.get_child(idx) {
6454 Some(c) => {
6455 let next_guard = c.read_arc();
6456 drop(guard);
6457 guard = next_guard;
6458 continue;
6459 }
6460 None => idx, // EV-14/EV-13: re-fetch below.
6461 }
6462 }
6463 TreeNode::Bottom(_) => {
6464 unreachable!("is_bin() returned false above")
6465 }
6466 };
6467 // Hand-over-hand: take the child read guard before
6468 // releasing the parent guard. Closes the
6469 // descender-vs-splitter window: a concurrent
6470 // split_child(parent, ..) takes parent.write(), which
6471 // blocks while we still hold parent.read().
6472 drop(guard);
6473 let child = self.child_at_or_fetch(&parent_arc, next_idx)?;
6474 guard = child.read_arc();
6475 }
6476 }
6477
6478 // ========================================================================
6479 // BIN-Delta reconstitution
6480 // ========================================================================
6481
6482 /// Increments the cursor-pin count on a BIN node.
6483 ///
6484 /// Called by `CursorImpl` when it positions on (or enters) a BIN.
6485 /// The evictor will not select a BIN with `cursor_count > 0` for eviction
6486 /// (`RealNodeInfo.pin_count`), matching `BIN.incrementCursorCount()`.
6487 pub fn pin_bin(bin_arc: &Arc<RwLock<TreeNode>>) {
6488 let mut guard = bin_arc.write();
6489 if let TreeNode::Bottom(ref mut stub) = *guard {
6490 stub.cursor_count += 1;
6491 }
6492 }
6493
6494 /// Decrements the cursor-pin count on a BIN node.
6495 ///
6496 /// Called by `CursorImpl` when it moves away from or closes on a BIN.
6497 /// Uses `saturating_sub` to guard against an accidental double-unpin.
6498 /// Matching `BIN.decrementCursorCount()`.
6499 pub fn unpin_bin(bin_arc: &Arc<RwLock<TreeNode>>) {
6500 let mut guard = bin_arc.write();
6501 if let TreeNode::Bottom(ref mut stub) = *guard {
6502 stub.cursor_count = stub.cursor_count.saturating_sub(1);
6503 }
6504 }
6505
6506 /// Returns `true` if the given `BinStub` is a BIN-delta (not a full BIN).
6507 ///
6508 /// `IN.isBINDelta()`.
6509 pub fn bin_is_delta(bin: &BinStub) -> bool {
6510 bin.is_delta
6511 }
6512
6513 /// Merge delta entries into a full BIN's entry list.
6514 ///
6515 /// - For each delta entry: if a matching key already exists in `bin`,
6516 /// replace it (delta is authoritative).
6517 /// - Otherwise insert the delta entry in sorted position.
6518 ///
6519 /// Delta entries carry **full** keys (prefix already prepended by the
6520 /// caller). After applying all delta entries the BIN's prefix is
6521 /// recomputed so the final state is consistent.
6522 ///
6523 /// All delta entries are considered to be the most-recently-dirtied
6524 /// state, exactly as in where delta slots supersede full-BIN slots.
6525 pub fn apply_delta_to_bin(
6526 bin: &mut BinStub,
6527 delta_entries: Vec<(Vec<u8>, Lsn, Option<Vec<u8>>)>,
6528 ) {
6529 for (full_key, lsn, data) in delta_entries {
6530 // `full_key` is a full (uncompressed) key here.
6531 bin.insert_with_prefix(full_key, lsn, data);
6532 }
6533 bin.dirty = true;
6534 }
6535
6536 /// Reconstitute a BIN-delta into a full BIN.
6537 ///
6538 /// from the:
6539 ///
6540 /// 1. Extract the delta entries from `self` (this BIN-delta), decompressing
6541 /// them to full keys.
6542 /// 2. Apply them onto `base` (the previously logged full BIN) via
6543 /// `apply_delta_to_bin`.
6544 /// 3. Copy `base`'s merged entries and prefix back into `self`.
6545 /// 4. Clear the `is_delta` flag so subsequent code treats `self` as
6546 /// a full BIN.
6547 ///
6548 /// After this call `self` is a full BIN; `base` should be discarded.
6549 pub fn mutate_to_full_bin(delta: &mut BinStub, mut base: BinStub) {
6550 // Decompress delta entries to full keys before applying.
6551 let delta_full_entries: Vec<(Vec<u8>, Lsn, Option<Vec<u8>>)> = (0
6552 ..delta.entries.len())
6553 .map(|i| {
6554 (
6555 delta.get_full_key(i).unwrap_or_default(),
6556 delta.get_lsn(i),
6557 delta.entries[i].data.clone(),
6558 )
6559 })
6560 .collect();
6561 // reconstituteBIN + resetContent + setBINDelta(false).
6562 Self::apply_delta_to_bin(&mut base, delta_full_entries);
6563 delta.entries = base.entries;
6564 delta.lsn_rep = base.lsn_rep; // T-3
6565 delta.keys = base.keys; // T-2
6566 delta.key_prefix = base.key_prefix;
6567 delta.is_delta = false;
6568 delta.dirty = true;
6569 }
6570
6571 /// Read an IN/BIN log entry at `log_lsn` and deserialise it into a
6572 /// `TreeNode`, ready to be installed as a (re-fetched) resident node.
6573 ///
6574 /// JE `LogManager.getLogEntry(lsn)` + `IN.readFromLog` as used by
6575 /// `ChildReference.fetchTarget` (the path that re-materializes a
6576 /// non-resident node from its persisted LSN on descent) and by
6577 /// `Tree.getRootINRootAlreadyLatched` for the root. The freshly-fetched
6578 /// node has no resident children (`TargetRep::None`); its own children, if
6579 /// any, are re-fetched on demand the same way when the descent reaches
6580 /// them.
6581 ///
6582 /// Returns `None` if the LSN is null, the log read fails, the entry is not
6583 /// an IN/BIN, or deserialisation fails (the caller treats this as "node
6584 /// unavailable" rather than panicking, matching the graceful-degradation
6585 /// policy of `mutate_to_full_bin_from_log`).
6586 fn fetch_node_from_log(&self, log_lsn: Lsn) -> Option<TreeNode> {
6587 if log_lsn == NULL_LSN {
6588 return None;
6589 }
6590 let lm = self.log_manager.as_ref()?;
6591 let (entry_type, payload) = lm.read_entry(log_lsn).ok()?;
6592 // The on-disk payload is an `InLogEntry` body (db_id | prev_full_lsn
6593 // | prev_delta_lsn | len | node_data). The recovery scanner strips
6594 // this header before calling `recover_in_redo`; re-fetch must do the
6595 // same so `deserialize_*` sees the bare node bytes. JE
6596 // `INLogEntry.readEntry` parses the same wrapper.
6597 let in_entry =
6598 noxu_log::entry::in_log_entry::InLogEntry::read_from_log(&payload)
6599 .ok()?;
6600 let node_data = &in_entry.node_data;
6601 use noxu_log::LogEntryType;
6602 match entry_type {
6603 LogEntryType::BIN => {
6604 Self::deserialize_bin(node_data).map(TreeNode::Bottom)
6605 }
6606 LogEntryType::IN => {
6607 Self::deserialize_upper_in(node_data).map(TreeNode::Internal)
6608 }
6609 // BIN-deltas are never logged as the *root* version and are
6610 // reconstituted by the BIN-delta path, not here.
6611 _ => {
6612 log::warn!(
6613 "fetch_node_from_log: expected IN/BIN entry at LSN {:?}, \
6614 got {:?}",
6615 log_lsn,
6616 entry_type
6617 );
6618 None
6619 }
6620 }
6621 }
6622
6623 /// Reconstitute a BIN-delta into a full BIN by reading the base from log.
6624 ///
6625 /// — the
6626 /// single-argument overload that calls `fetchFullBIN(databaseImpl)` to
6627 /// read the last full BIN from the log manager automatically.
6628 ///
6629 /// Algorithm:
6630 /// 1. If `delta.last_full_lsn == NULL_LSN`, the BIN was never written as a
6631 /// full entry; there is no base to merge so the delta IS the full BIN.
6632 /// Clear `is_delta` and return.
6633 /// 2. Read the full-BIN log entry at `delta.last_full_lsn` using
6634 /// `log_manager.read_entry(lsn)`.
6635 /// 3. Deserialize the payload with `BinStub::deserialize_full()`.
6636 /// 4. Delegate to `Self::mutate_to_full_bin(delta, base)` to merge and
6637 /// replace `delta`'s contents.
6638 ///
6639 /// On any read / parse failure the function falls back to clearing the
6640 /// `is_delta` flag without merging, so the caller always gets a non-delta
6641 /// BIN (possibly missing some old slots). This mirrors the
6642 /// `EnvironmentFailureException` path but gracefully degrades instead of
6643 /// panicking.
6644 ///
6645 /// `BIN.fetchFullBIN(dbImpl)` + `BIN.mutateToFullBIN(boolean)`.
6646 pub fn mutate_to_full_bin_from_log(
6647 delta: &mut BinStub,
6648 log_manager: &noxu_log::LogManager,
6649 ) {
6650 if !delta.is_delta {
6651 // Already a full BIN; nothing to do.
6652 return;
6653 }
6654
6655 if delta.last_full_lsn == NULL_LSN {
6656 // BIN has never been logged as a full entry — the in-memory delta
6657 // is effectively the full state. During recovery this path is
6658 // harmless.
6659 delta.is_delta = false;
6660 return;
6661 }
6662
6663 // Read the full-BIN log entry at last_full_lsn.
6664 // `envImpl.getLogManager().getEntryHandleFileNotFound(lsn)`.
6665 match log_manager.read_entry(delta.last_full_lsn) {
6666 Ok((entry_type, payload)) => {
6667 use noxu_log::LogEntryType;
6668 if entry_type == LogEntryType::BIN {
6669 if let Some(mut base) = BinStub::deserialize_full(&payload)
6670 {
6671 // Set the base's last_full_lsn so it is preserved
6672 // into the merged result.
6673 base.last_full_lsn = delta.last_full_lsn;
6674 Self::mutate_to_full_bin(delta, base);
6675 return;
6676 }
6677 // Deserialization failed — fall through to graceful degradation.
6678 log::warn!(
6679 "mutate_to_full_bin_from_log: failed to deserialize \
6680 full BIN at LSN {:?}; keeping delta as-is",
6681 delta.last_full_lsn
6682 );
6683 } else {
6684 log::warn!(
6685 "mutate_to_full_bin_from_log: expected BIN entry at \
6686 LSN {:?}, got {:?}",
6687 delta.last_full_lsn,
6688 entry_type
6689 );
6690 }
6691 }
6692 Err(e) => {
6693 log::warn!(
6694 "mutate_to_full_bin_from_log: failed to read log at \
6695 LSN {:?}: {}",
6696 delta.last_full_lsn,
6697 e
6698 );
6699 }
6700 }
6701
6702 // Graceful degradation: promote the delta to a "full" BIN without
6703 // the base slots. The BIN will be re-logged as a full BIN at the
6704 // next checkpoint.
6705 delta.is_delta = false;
6706 delta.dirty = true;
6707 }
6708
6709 // ========================================================================
6710 // getNextBin / getPrevBin
6711 // ========================================================================
6712
6713 /// Return the entries of the BIN immediately to the right of the BIN
6714 /// that contains (or would contain) `current_key`.
6715 ///
6716 /// → `Tree.getNextIN(forward=true)`.
6717 ///
6718 /// # Algorithm
6719 /// 1. Build a root-to-BIN path for `current_key`.
6720 /// 2. Walk the path back up looking for a parent that has a slot to the
6721 /// right of the slot we descended through.
6722 /// 3. When found, descend to the leftmost BIN of that sibling subtree.
6723 /// 4. If no such parent exists, return `None` (no next BIN).
6724 pub fn get_next_bin(
6725 &self,
6726 current_key: &[u8],
6727 ) -> Option<Vec<(BinEntry, Lsn, Vec<u8>)>> {
6728 let root = self.get_root()?;
6729 self.get_adjacent_bin(&root, current_key, true)
6730 }
6731
6732 /// Return the entries of the BIN immediately to the left of the BIN
6733 /// that contains (or would contain) `current_key`.
6734 ///
6735 /// → `Tree.getNextIN(forward=false)`.
6736 pub fn get_prev_bin(
6737 &self,
6738 current_key: &[u8],
6739 ) -> Option<Vec<(BinEntry, Lsn, Vec<u8>)>> {
6740 let root = self.get_root()?;
6741 self.get_adjacent_bin(&root, current_key, false)
6742 }
6743
6744 /// Core implementation shared by `get_next_bin` and `get_prev_bin`.
6745 ///
6746 /// Builds the path from `root` down to the BIN for `current_key`
6747 /// (each element records the parent arc, the slot index taken,
6748 /// and the child Arc reached) using `read_arc()` hand-over-hand
6749 /// latch coupling.
6750 ///
6751 /// The ascent re-acquires the parent's read lock one level at a
6752 /// time. To handle a concurrent split that completes between
6753 /// path capture and ascent, we validate that the slot still
6754 /// holds the child Arc we descended through. If the slot
6755 /// mismatches we retry the whole operation from root with a
6756 /// short pause between attempts. The retry budget is generous
6757 /// (`MAX_ASCENT_ATTEMPTS`) so that the typical case of a few
6758 /// cascading splits between two BIN-level cursor steps is
6759 /// absorbed without surfacing as a false end-of-iteration.
6760 /// After exhausting the budget we conservatively return `None`,
6761 /// signalling "no adjacent BIN found"; the cursor will then
6762 /// either restart its scan or report end-of-iteration. The
6763 /// budget is finite so a pathological workload (a thread
6764 /// permanently splitting under us) cannot livelock the lookup.
6765 /// JE `Tree.getNextIN` / `Tree.getPrevIN`.
6766 ///
6767 /// R3 fix (2026-06-16): converted from `static fn` to `&self` so that the
6768 /// IN-level descent uses `self.upper_in_floor_index` (comparator-aware)
6769 /// instead of a raw byte `<=`. Without this, databases with a custom
6770 /// comparator (secondary indexes, sorted-dup) could descend to the wrong
6771 /// child → wrong adjacent BIN → incorrect cursor iteration across BIN
6772 /// boundaries. Mirrors `Tree.getNextIN`/`Tree.getPrevIN` using the
6773 /// comparator-aware `IN.findEntry`.
6774 fn get_adjacent_bin(
6775 &self,
6776 root: &Arc<RwLock<TreeNode>>,
6777 current_key: &[u8],
6778 forward: bool,
6779 ) -> Option<Vec<(BinEntry, Lsn, Vec<u8>)>> {
6780 const MAX_ASCENT_ATTEMPTS: u32 = 8;
6781 for attempt in 0..MAX_ASCENT_ATTEMPTS {
6782 match self.get_adjacent_bin_attempt(root, current_key, forward) {
6783 AdjacentBinOutcome::Found(v) => return Some(v),
6784 AdjacentBinOutcome::NoAdjacent => return None,
6785 AdjacentBinOutcome::SplitRaceRetry => {
6786 // Brief pause to let the splitter finish.
6787 if attempt + 1 < MAX_ASCENT_ATTEMPTS {
6788 std::thread::yield_now();
6789 }
6790 }
6791 }
6792 }
6793 // Exhausted retry budget. Signal "no adjacent" so the
6794 // cursor can fall back to its end-of-iteration path.
6795 None
6796 }
6797
6798 /// One attempt at `get_adjacent_bin`. The tri-state return
6799 /// value distinguishes "no adjacent BIN exists" (which the
6800 /// caller should propagate as end-of-iteration) from "a
6801 /// concurrent split invalidated our path" (which the caller
6802 /// should retry from root).
6803 fn get_adjacent_bin_attempt(
6804 &self,
6805 root: &Arc<RwLock<TreeNode>>,
6806 current_key: &[u8],
6807 forward: bool,
6808 ) -> AdjacentBinOutcome {
6809 // Path entry: (parent_arc, slot_idx_taken, child_arc_reached).
6810 // The child Arc lets the ascent validate that the slot still
6811 // points to the same node we descended through.
6812 let mut path: Vec<(
6813 Arc<RwLock<TreeNode>>,
6814 usize,
6815 Arc<RwLock<TreeNode>>,
6816 )> = Vec::new();
6817
6818 let mut guard: NodeArcReadGuard = root.read_arc();
6819 loop {
6820 if guard.is_bin() {
6821 break;
6822 }
6823
6824 let (next_arc, slot_idx) = match &*guard {
6825 TreeNode::Internal(n) => {
6826 if n.entries.is_empty() {
6827 return AdjacentBinOutcome::NoAdjacent;
6828 }
6829 // R3 fix: use comparator-aware upper_in_floor_index so
6830 // that custom-comparator / sorted-dup databases descend
6831 // to the correct child. Mirrors JE Tree.getNextIN which
6832 // uses IN.findEntry (comparator-aware) not raw byte order.
6833 let idx =
6834 self.upper_in_floor_index(&n.entries, current_key);
6835 let child = match n.get_child(idx) {
6836 Some(c) => c,
6837 None => return AdjacentBinOutcome::NoAdjacent,
6838 };
6839 (child, idx)
6840 }
6841 TreeNode::Bottom(_) => unreachable!(),
6842 };
6843
6844 // Record the parent and the child we are about to enter
6845 // — the child Arc lets the ascent validate the slot.
6846 let parent_arc = NodeArcReadGuard::rwlock(&guard).clone();
6847 path.push((parent_arc, slot_idx, Arc::clone(&next_arc)));
6848
6849 // Hand-over-hand: take child read lock BEFORE releasing parent.
6850 let next_guard = next_arc.read_arc();
6851 drop(guard);
6852 guard = next_guard;
6853 }
6854 drop(guard);
6855
6856 // Ascend the path. At each level, validate that
6857 // `parent.entries[taken_idx].child == descended_child` before
6858 // trusting `taken_idx` as a coordinate. If not, return
6859 // `SplitRaceRetry` so the caller restarts from root.
6860 while let Some((parent_arc, taken_idx, descended_child)) = path.pop() {
6861 let parent_guard = parent_arc.read();
6862 let (n_entries, slot_still_valid) = match &*parent_guard {
6863 TreeNode::Internal(p) => {
6864 let n = p.entries.len();
6865 let valid = p
6866 .child_ref(taken_idx)
6867 .is_some_and(|c| Arc::ptr_eq(c, &descended_child));
6868 (n, valid)
6869 }
6870 _ => return AdjacentBinOutcome::NoAdjacent,
6871 };
6872 drop(parent_guard);
6873
6874 if !slot_still_valid {
6875 return AdjacentBinOutcome::SplitRaceRetry;
6876 }
6877
6878 let sibling_idx = if forward {
6879 taken_idx + 1
6880 } else if taken_idx == 0 {
6881 // No left sibling at this level — ascend further.
6882 continue;
6883 } else {
6884 taken_idx - 1
6885 };
6886
6887 if forward && sibling_idx >= n_entries {
6888 // No right sibling at this level — ascend further.
6889 continue;
6890 }
6891
6892 // Found a sibling slot — fetch the sibling child arc.
6893 let sibling_arc = {
6894 let g = parent_arc.read();
6895 match &*g {
6896 TreeNode::Internal(p) => match p.get_child(sibling_idx) {
6897 Some(c) => c,
6898 None => return AdjacentBinOutcome::NoAdjacent,
6899 },
6900 _ => return AdjacentBinOutcome::NoAdjacent,
6901 }
6902 };
6903
6904 // Descend to the leftmost (forward) or rightmost (!forward) BIN.
6905 return match Self::descend_to_edge_bin(&sibling_arc, forward) {
6906 Some(v) => AdjacentBinOutcome::Found(v),
6907 None => AdjacentBinOutcome::NoAdjacent,
6908 };
6909 }
6910
6911 // Exhausted path without finding a sibling → no adjacent BIN.
6912 AdjacentBinOutcome::NoAdjacent
6913 }
6914
6915 /// Descend to the leftmost BIN (`forward = true`) or rightmost BIN
6916 /// (`forward = false`) in the sub-tree rooted at `node_arc`.
6917 ///
6918 /// `Tree.searchSubTree(SearchType.LEFT / RIGHT, targetLevel)`.
6919 fn descend_to_edge_bin(
6920 node_arc: &Arc<RwLock<TreeNode>>,
6921 forward: bool,
6922 ) -> Option<Vec<(BinEntry, Lsn, Vec<u8>)>> {
6923 // Hand-over-hand latch coupling — see Tree::search.
6924 let mut guard: NodeArcReadGuard = node_arc.read_arc();
6925
6926 loop {
6927 if guard.is_bin() {
6928 return match &*guard {
6929 TreeNode::Bottom(b) => {
6930 // Return entries with full (decompressed) keys so that
6931 // callers always work with complete keys.
6932 //
6933 // TREE-F1: KD slots are NOT filtered here — the BIN's
6934 // slot indices are returned verbatim so the cursor can
6935 // skip KD slots itself (CursorImpl getNext loop;
6936 // CursorImpl.java:2062-2064) and continue to the next
6937 // BIN when an edge BIN is entirely KD during the
6938 // BIN-delta reconstitution window.
6939 let full_entries: Vec<(BinEntry, Lsn, Vec<u8>)> = (0
6940 ..b.entries.len())
6941 .map(|i| {
6942 (
6943 BinEntry {
6944 data: b.entries[i].data.clone(),
6945 known_deleted: b.entries[i]
6946 .known_deleted,
6947 dirty: b.entries[i].dirty,
6948 expiration_time: b.entries[i]
6949 .expiration_time,
6950 },
6951 b.get_lsn(i),
6952 b.get_full_key(i).unwrap_or_default(),
6953 )
6954 })
6955 .collect();
6956 Some(full_entries)
6957 }
6958 _ => None,
6959 };
6960 }
6961
6962 let next = match &*guard {
6963 TreeNode::Internal(n) => {
6964 if forward {
6965 n.get_child(0)?
6966 } else {
6967 n.get_child(n.entries.len().saturating_sub(1))?
6968 }
6969 }
6970 _ => return None,
6971 };
6972 // Take child read lock BEFORE releasing parent's.
6973 let next_guard = next.read_arc();
6974 drop(guard);
6975 guard = next_guard;
6976 }
6977 }
6978}
6979
6980// ============================================================================
6981// Tree statistics
6982// ============================================================================
6983
6984/// Statistics collected by a full tree walk.
6985///
6986/// `TreeWalkerStatsAccumulator`.
6987#[derive(Debug, Default, Clone, PartialEq, Eq)]
6988pub struct TreeStats {
6989 /// Number of BINs (bottom internal nodes).
6990 pub n_bins: u64,
6991 /// Number of upper INs.
6992 pub n_ins: u64,
6993 /// Total number of entries across all nodes.
6994 pub n_entries: u64,
6995 /// Height of the tree (1 = root is a BIN, 2 = one level above BINs, …).
6996 pub height: u32,
6997}
6998
6999impl Tree {
7000 /// Walks the entire tree and collects structural statistics.
7001 ///
7002 /// `TreeWalkerStatsAccumulator` pattern — performs a simple
7003 /// recursive DFS and counts INs, BINs, entries, and tree height.
7004 pub fn collect_stats(&self) -> TreeStats {
7005 let mut stats = TreeStats::default();
7006 if let Some(root) = self.get_root() {
7007 Self::collect_stats_recursive(&root, &mut stats, 0);
7008 }
7009 stats
7010 }
7011
7012 fn collect_stats_recursive(
7013 node_arc: &Arc<RwLock<TreeNode>>,
7014 stats: &mut TreeStats,
7015 depth: u32,
7016 ) {
7017 let guard = node_arc.read();
7018
7019 let current_height = depth + 1;
7020 if current_height > stats.height {
7021 stats.height = current_height;
7022 }
7023
7024 match &*guard {
7025 TreeNode::Bottom(b) => {
7026 stats.n_bins += 1;
7027 stats.n_entries += b.entries.len() as u64;
7028 }
7029 TreeNode::Internal(n) => {
7030 stats.n_ins += 1;
7031 stats.n_entries += n.entries.len() as u64;
7032 // Collect child arcs before releasing the guard.
7033 let children: Vec<Arc<RwLock<TreeNode>>> =
7034 n.resident_children();
7035 // Release guard before recursing to avoid lock ordering issues.
7036 drop(guard);
7037 for child in children {
7038 Self::collect_stats_recursive(&child, stats, depth + 1);
7039 }
7040 }
7041 }
7042 }
7043
7044 /// Collects all dirty BINs as (Arc to node, db_id) pairs.
7045 ///
7046 /// The checkpoint path calls this to enumerate BINs that need to be
7047 /// logged. For each dirty BIN the checkpoint decides — based on the
7048 /// BIN-delta threshold — whether to write a full `BIN` entry or a
7049 /// `BINDelta` entry.
7050 ///
7051 /// `Checkpointer.processINList()` which iterates the dirty
7052 /// IN list accumulated during normal operation.
7053 pub fn collect_dirty_bins(
7054 &self,
7055 db_id: u64,
7056 ) -> Vec<(u64, Arc<RwLock<TreeNode>>)> {
7057 let mut result = Vec::new();
7058 if let Some(root) = self.get_root() {
7059 Self::collect_dirty_bins_recursive(&root, db_id, &mut result);
7060 }
7061 result
7062 }
7063
7064 fn collect_dirty_bins_recursive(
7065 node_arc: &Arc<RwLock<TreeNode>>,
7066 db_id: u64,
7067 out: &mut Vec<(u64, Arc<RwLock<TreeNode>>)>,
7068 ) {
7069 let guard = node_arc.read();
7070 match &*guard {
7071 TreeNode::Bottom(b) => {
7072 // Include this BIN if it is dirty or has any dirty slots.
7073 if b.dirty || b.dirty_count() > 0 {
7074 out.push((db_id, Arc::clone(node_arc)));
7075 }
7076 }
7077 TreeNode::Internal(n) => {
7078 let children: Vec<Arc<RwLock<TreeNode>>> =
7079 n.resident_children();
7080 drop(guard);
7081 for child in children {
7082 Self::collect_dirty_bins_recursive(&child, db_id, out);
7083 } // guard already dropped
7084 }
7085 }
7086 }
7087
7088 /// Collect all BINs that have at least one `known_deleted` slot.
7089 ///
7090 /// INCompressor queue-drain scan in the: the daemon iterates
7091 /// the in-memory IN list and identifies BINs that still hold zombie deleted
7092 /// slots. Each returned `Arc` can be passed directly to `compress_bin()`.
7093 pub fn collect_bins_with_known_deleted(
7094 &self,
7095 ) -> Vec<Arc<RwLock<TreeNode>>> {
7096 let mut result = Vec::new();
7097 if let Some(root) = self.get_root() {
7098 Self::collect_bins_with_known_deleted_recursive(&root, &mut result);
7099 }
7100 result
7101 }
7102
7103 fn collect_bins_with_known_deleted_recursive(
7104 node_arc: &Arc<RwLock<TreeNode>>,
7105 out: &mut Vec<Arc<RwLock<TreeNode>>>,
7106 ) {
7107 let guard = node_arc.read();
7108 match &*guard {
7109 TreeNode::Bottom(b) => {
7110 if b.entries.iter().any(|e| e.known_deleted) {
7111 out.push(Arc::clone(node_arc));
7112 }
7113 }
7114 TreeNode::Internal(n) => {
7115 let children: Vec<Arc<RwLock<TreeNode>>> =
7116 n.resident_children();
7117 drop(guard);
7118 for child in children {
7119 Self::collect_bins_with_known_deleted_recursive(
7120 &child, out,
7121 );
7122 }
7123 }
7124 }
7125 }
7126
7127 /// Collect all dirty upper (non-BIN) internal nodes, sorted ascending by
7128 /// level (bottom-up order, BIN level excluded).
7129 ///
7130 /// Serialise an upper-IN node (level > 1) by node_id for off-heap storage.
7131 ///
7132 /// Traverses the tree to find the internal node whose matches,
7133 /// then calls to produce a compact byte
7134 /// representation. Returns if the node is not found or is a BIN
7135 /// (BINs are not upper INs).
7136 ///
7137 /// Mirrors `OffHeapAllocator` serialises the same bytes that would be written
7138 /// to the log, allowing the evictor to store upper-INs off-heap and avoid
7139 /// log-file reads on the next traversal.
7140 pub fn serialize_upper_in(&self, node_id: u64) -> Option<Vec<u8>> {
7141 let root = self.get_root()?;
7142 Self::find_and_serialize_upper_in(&root, node_id)
7143 }
7144
7145 fn find_and_serialize_upper_in(
7146 node_arc: &Arc<RwLock<TreeNode>>,
7147 target_id: u64,
7148 ) -> Option<Vec<u8>> {
7149 let guard = node_arc.read();
7150 match &*guard {
7151 TreeNode::Bottom(_) => None, // BINs are not upper INs
7152 TreeNode::Internal(n) => {
7153 if n.node_id == target_id {
7154 // Serialise InNodeStub for off-heap storage.
7155 // Format: node_id(u64BE) | level(i32BE) | n_entries(u32BE)
7156 // then per-entry: key_len(u32BE) | key | lsn(u64BE)
7157 let mut buf = Vec::new();
7158 buf.extend_from_slice(&n.node_id.to_be_bytes());
7159 buf.extend_from_slice(&n.level.to_be_bytes());
7160 buf.extend_from_slice(
7161 &(n.entries.len() as u32).to_be_bytes(),
7162 );
7163 for (i, e) in n.entries.iter().enumerate() {
7164 buf.extend_from_slice(
7165 &(e.key.len() as u32).to_be_bytes(),
7166 );
7167 buf.extend_from_slice(&e.key);
7168 buf.extend_from_slice(
7169 &n.get_lsn(i).as_u64().to_be_bytes(),
7170 );
7171 }
7172 return Some(buf);
7173 }
7174 // Recurse into children before releasing the guard so we
7175 // hold the minimum read-lock duration.
7176 let children: Vec<Arc<RwLock<TreeNode>>> =
7177 n.resident_children();
7178 drop(guard);
7179 for child in &children {
7180 if let Some(bytes) =
7181 Self::find_and_serialize_upper_in(child, target_id)
7182 {
7183 return Some(bytes);
7184 }
7185 }
7186 None
7187 }
7188 }
7189 }
7190
7191 /// Upper-IN traversal in `Checkpointer.processINList()` from
7192 /// — visits all `TreeNode::Internal` nodes whose `dirty` flag is set
7193 /// and returns them together with their level, sorted lowest-level-first
7194 /// so the checkpointer can log them bottom-up. The root is always the
7195 /// last entry (highest level), which must be logged `Provisional::No`.
7196 pub fn collect_dirty_upper_ins(
7197 &self,
7198 _db_id: u64,
7199 ) -> Vec<(i32, Arc<RwLock<TreeNode>>)> {
7200 let mut result: Vec<(i32, Arc<RwLock<TreeNode>>)> = Vec::new();
7201 if let Some(root) = self.get_root() {
7202 Self::collect_dirty_upper_ins_recursive(&root, &mut result);
7203 }
7204 result.sort_by_key(|(level, _)| *level);
7205 result
7206 }
7207
7208 fn collect_dirty_upper_ins_recursive(
7209 node_arc: &Arc<RwLock<TreeNode>>,
7210 out: &mut Vec<(i32, Arc<RwLock<TreeNode>>)>,
7211 ) {
7212 let guard = node_arc.read();
7213 match &*guard {
7214 TreeNode::Bottom(_) => {
7215 // BINs are handled by flush_dirty_bins_internal; skip here.
7216 }
7217 TreeNode::Internal(n) => {
7218 let is_dirty = n.dirty;
7219 // REC-AA: return the node's ACTUAL tree level (n.level, in
7220 // MAIN_LEVEL|n units), not a root-relative depth. The level
7221 // must be on the same scale as a BIN's `level` (BIN_LEVEL =
7222 // MAIN_LEVEL|1) so that the checkpointer's flush-level
7223 // computation and the evictor's `node_level < flush_level`
7224 // comparison are meaningful. With a root-relative depth the
7225 // root had the SMALLEST value (0) and the IN above the BINs
7226 // the LARGEST, inverting the provisional/non-provisional
7227 // boundary; with n.level the root has the largest level, as JE
7228 // expects.
7229 let level = n.level;
7230 let children: Vec<Arc<RwLock<TreeNode>>> =
7231 n.resident_children();
7232 drop(guard);
7233 // Recurse into children first (bottom-up ordering).
7234 for child in &children {
7235 Self::collect_dirty_upper_ins_recursive(child, out);
7236 }
7237 // Add this node after children (so parent comes after all descendants).
7238 if is_dirty {
7239 out.push((level, Arc::clone(node_arc)));
7240 }
7241 }
7242 }
7243 }
7244
7245 // ========================================================================
7246 // Tree.java ports: 8 additional tree methods (Task #82)
7247 // ========================================================================
7248
7249 /// Returns `true` if the root node is currently loaded in memory.
7250 ///
7251 /// .
7252 pub fn is_root_resident(&self) -> bool {
7253 self.root.read().is_some()
7254 }
7255
7256 /// Returns the root node `Arc` if present, or `None`.
7257 ///
7258 /// .
7259 pub fn get_resident_root_in(&self) -> Option<Arc<RwLock<TreeNode>>> {
7260 self.root.read().clone()
7261 }
7262
7263 /// Returns the BIN that should contain a slot for `key` (the "parent" of
7264 /// LN slots).
7265 ///
7266 /// . Descends the tree
7267 /// exactly like `search()` and returns the leaf-level BIN arc, or `None`
7268 /// if the tree is empty.
7269 ///
7270 /// Uses `read_arc()` hand-over-hand on the descent — the child
7271 /// guard is taken before the parent guard is dropped, matching
7272 /// `search()`. Returns the BIN Arc with no read lock held; the
7273 /// caller must take whatever lock it needs to operate on the
7274 /// returned BIN.
7275 pub fn get_parent_bin_for_child_ln(
7276 &self,
7277 key: &[u8],
7278 ) -> Option<Arc<RwLock<TreeNode>>> {
7279 let root = self.get_root()?;
7280 let mut current_arc: Arc<RwLock<TreeNode>> = root.clone();
7281 let mut guard: NodeArcReadGuard = root.read_arc();
7282
7283 loop {
7284 if guard.is_bin() {
7285 drop(guard);
7286 return Some(current_arc);
7287 }
7288
7289 let parent_arc = current_arc.clone();
7290 let next_idx = match &*guard {
7291 TreeNode::Internal(n) => {
7292 if n.entries.is_empty() {
7293 return None;
7294 }
7295 let idx = self.upper_in_floor_index(&n.entries, key);
7296 match n.get_child(idx) {
7297 Some(c) => {
7298 let next_guard = c.read_arc();
7299 drop(guard);
7300 current_arc = c;
7301 guard = next_guard;
7302 continue;
7303 }
7304 None => idx, // EV-14/EV-13: re-fetch below.
7305 }
7306 }
7307 TreeNode::Bottom(_) => {
7308 unreachable!("is_bin() returned false above")
7309 }
7310 };
7311 // Hand-over-hand: take child guard before dropping parent.
7312 drop(guard);
7313 let child = self.child_at_or_fetch(&parent_arc, next_idx)?;
7314 let next_guard = child.read_arc();
7315 current_arc = child;
7316 guard = next_guard;
7317 }
7318 }
7319
7320 /// Returns the BIN where `key` should be inserted.
7321 ///
7322 /// . Semantically identical to
7323 /// `get_parent_bin_for_child_ln` — expressed as a separate method to match
7324 /// API surface.
7325 ///
7326 /// Implemented as a delegation to `get_parent_bin_for_child_ln`,
7327 /// which uses `read_arc()` hand-over-hand on the descent.
7328 pub fn find_bin_for_insert(
7329 &self,
7330 key: &[u8],
7331 ) -> Option<Arc<RwLock<TreeNode>>> {
7332 self.get_parent_bin_for_child_ln(key)
7333 }
7334
7335 /// Search for a BIN, allowing splits during descent (preemptive splitting).
7336 ///
7337 /// . This thin wrapper
7338 /// delegates to `search()` and returns the result wrapped in `Some`.
7339 /// The full split-allowed descent is performed by `insert()` internally;
7340 /// this method exposes the same result type for callers that only need to
7341 /// locate the BIN.
7342 ///
7343 /// Returns `None` if the tree is empty.
7344 pub fn search_splits_allowed(&self, key: &[u8]) -> Option<SearchResult> {
7345 self.search(key)
7346 }
7347
7348 /// Traverses the entire tree and returns every IN and BIN node as a flat
7349 /// list.
7350 ///
7351 /// . Used by recovery to rebuild
7352 /// the in-memory IN list after log replay. The walk is a BFS from the
7353 /// root; every `Arc<RwLock<TreeNode>>` encountered (both Internal and
7354 /// Bottom variants) is included in the result.
7355 pub fn rebuild_in_list(&self) -> Vec<Arc<RwLock<TreeNode>>> {
7356 let mut result = Vec::new();
7357 if let Some(root) = self.get_root() {
7358 Self::rebuild_in_list_recursive(&root, &mut result);
7359 }
7360 result
7361 }
7362
7363 fn rebuild_in_list_recursive(
7364 node_arc: &Arc<RwLock<TreeNode>>,
7365 out: &mut Vec<Arc<RwLock<TreeNode>>>,
7366 ) {
7367 // Push this node unconditionally — both INs and BINs belong in the list.
7368 out.push(Arc::clone(node_arc));
7369
7370 let guard = node_arc.read();
7371
7372 if let TreeNode::Internal(n) = &*guard {
7373 // Collect child arcs while holding the guard, then drop it before
7374 // recursing to avoid holding multiple locks simultaneously.
7375 let children: Vec<Arc<RwLock<TreeNode>>> = n.resident_children();
7376 drop(guard);
7377 for child in children {
7378 Self::rebuild_in_list_recursive(&child, out);
7379 }
7380 }
7381 // BIN nodes are leaves — no children to recurse into.
7382 }
7383
7384 /// Validates internal tree consistency.
7385 ///
7386 /// . Primarily a debug/test tool.
7387 ///
7388 /// Rules checked:
7389 /// - An empty tree (no root) is trivially valid → returns `true`.
7390 /// - A non-empty tree must have a non-null root.
7391 /// - Every Internal node must have at least one entry.
7392 /// - Every child pointer that is `Some` must be readable (lock must be
7393 /// acquirable — i.e., no poisoned locks).
7394 ///
7395 /// Returns `true` if no inconsistencies are detected, `false` otherwise.
7396 pub fn validate_in_list(&self) -> bool {
7397 match self.get_root() {
7398 None => true, // empty tree is always valid
7399 Some(root) => Self::validate_node(&root),
7400 }
7401 }
7402
7403 fn validate_node(node_arc: &Arc<RwLock<TreeNode>>) -> bool {
7404 let guard = node_arc.read();
7405
7406 match &*guard {
7407 TreeNode::Bottom(_bin) => {
7408 // BIN nodes are always structurally valid at this level.
7409 true
7410 }
7411 TreeNode::Internal(n) => {
7412 // An Internal node must have at least one entry.
7413 if n.entries.is_empty() {
7414 return false;
7415 }
7416 // Collect child arcs before dropping the guard.
7417 let children: Vec<Arc<RwLock<TreeNode>>> =
7418 n.resident_children();
7419 drop(guard);
7420 // Recursively validate every resident child.
7421 for child in children {
7422 if !Self::validate_node(&child) {
7423 return false;
7424 }
7425 }
7426 true
7427 }
7428 }
7429 }
7430
7431 /// Traverses the tree to find the parent IN that contains `child_node_id`
7432 /// as one of its child slots.
7433 ///
7434 /// . Used by the cleaner
7435 /// migration path to re-insert migrated INs after eviction/fetch.
7436 ///
7437 /// Returns `(parent_arc, slot_index)` where `slot_index` is the position
7438 /// in the parent's `entries` vector whose child matches `child_node_id`,
7439 /// or `None` if no such parent is found.
7440 pub fn get_parent_in_for_child_in(
7441 &self,
7442 child_node_id: u64,
7443 ) -> Option<(Arc<RwLock<TreeNode>>, usize)> {
7444 let root = self.get_root()?;
7445 Self::find_parent_of_node_id(&root, child_node_id)
7446 }
7447
7448 /// Recursive DFS helper for `get_parent_in_for_child_in`.
7449 ///
7450 /// Scans every entry in each Internal node. When a child's node_id
7451 /// matches `target_id` the parent arc and slot index are returned.
7452 fn find_parent_of_node_id(
7453 node_arc: &Arc<RwLock<TreeNode>>,
7454 target_id: u64,
7455 ) -> Option<(Arc<RwLock<TreeNode>>, usize)> {
7456 let guard = node_arc.read();
7457
7458 let TreeNode::Internal(n) = &*guard else {
7459 // BIN nodes have no IN children — cannot be a parent of another IN.
7460 return None;
7461 };
7462
7463 // Check whether any child of this IN has the target node_id.
7464 let mut children: Vec<(usize, Arc<RwLock<TreeNode>>)> = Vec::new();
7465 for slot in 0..n.entries.len() {
7466 if let Some(child_arc) = n.child_ref(slot) {
7467 // Read the child's node_id under a separate lock (acquire child
7468 // while parent guard is still held — this is intentional for
7469 // the ID comparison only; we release both immediately after).
7470 let child_id = {
7471 let cg = child_arc.read();
7472 match &*cg {
7473 TreeNode::Internal(cn) => cn.node_id,
7474 TreeNode::Bottom(cb) => cb.node_id,
7475 }
7476 };
7477
7478 if child_id == target_id {
7479 // Found — return a clone of this node as parent.
7480 let parent_clone = Arc::clone(node_arc);
7481 return Some((parent_clone, slot));
7482 }
7483
7484 // Not found at this slot; schedule this child for recursion.
7485 children.push((slot, Arc::clone(child_arc)));
7486 }
7487 }
7488 // Release parent guard before recursing.
7489 drop(guard);
7490
7491 // Recurse into each Internal child.
7492 for (_slot, child_arc) in children {
7493 if let Some(result) =
7494 Self::find_parent_of_node_id(&child_arc, target_id)
7495 {
7496 return Some(result);
7497 }
7498 }
7499
7500 None
7501 }
7502
7503 /// Propagates the dirty flag upward from `node_arc` to the root.
7504 ///
7505 /// Implicit dirty propagation: after modifying any node,
7506 /// all ancestors on the path to the root must also be marked dirty so
7507 /// the checkpointer logs them.
7508 ///
7509 /// In this happens through `IN.setDirty(true)` calls at each level
7510 /// during split/insert callbacks. Here we walk the weak parent chain.
7511 /// Reconstitute a BIN-delta by merging it onto a base full BIN.
7512 ///
7513 /// Implements JE `BINDelta.reconstituteBIN(databaseImpl)` for the recovery
7514 /// path where the log manager is not available as a `LogManager` but as
7515 /// raw serialized bytes.
7516 ///
7517 /// Algorithm:
7518 /// 1. Deserialise `base_bytes` as a full `BinStub`.
7519 /// 2. Apply `delta_bytes` slots onto the base using `BinStub::apply_delta`
7520 /// (raw slot overlay).
7521 /// 3. Recompute key prefix so prefix-compressed entries are consistent.
7522 ///
7523 /// Returns `None` if either byte slice is malformed.
7524 ///
7525 /// JE `BINDelta.reconstituteBIN` / `BINDelta.applyDelta`
7526 /// (DRIFT-10 / Stage 3).
7527 pub fn reconstitute_bin_delta(
7528 base_bytes: &[u8],
7529 delta_bytes: &[u8],
7530 ) -> Option<BinStub> {
7531 let mut base = BinStub::deserialize_full(base_bytes)?;
7532 // Apply the delta slots onto the base.
7533 // Note: BinStub::apply_delta uses slot-index addressing into base.entries,
7534 // extending with new entries when the slot_idx >= base.entries.len().
7535 // After apply_delta we recompute the key prefix to fix prefix compression.
7536 BinStub::apply_delta(&mut base, delta_bytes)?;
7537 // Recompute prefix so prefix-compressed BINs are consistent after merge.
7538 base.recompute_key_prefix();
7539 base.is_delta = false;
7540 base.dirty = false;
7541 Some(base)
7542 }
7543
7544 pub fn propagate_dirty_to_root(node_arc: &Arc<RwLock<TreeNode>>) {
7545 let parent_weak = { node_arc.read().get_parent() };
7546
7547 if let Some(parent_arc) = parent_weak.and_then(|w| w.upgrade()) {
7548 {
7549 let mut g = parent_arc.write();
7550 g.set_dirty(true);
7551 }
7552 // Recurse further up.
7553 Self::propagate_dirty_to_root(&parent_arc);
7554 }
7555 }
7556
7557 // ========================================================================
7558 // IN-redo: JE RecoveryManager.recoverIN / recoverRootIN / recoverChildIN
7559 // ========================================================================
7560
7561 /// Deserialise an upper-IN node from bytes produced by
7562 /// `TreeNode::write_to_bytes()` / `flush_one_tree_upper_ins`.
7563 ///
7564 /// Format: node_id(u64BE) | level(i32BE) | n_entries(u32BE) | dirty(u8)
7565 /// | per-entry: key_len(u16BE) | key | lsn(u64BE)
7566 ///
7567 /// JE `INFileReader.getIN(db)` / `IN.readFromLog`.
7568 pub fn deserialize_upper_in(bytes: &[u8]) -> Option<InNodeStub> {
7569 if bytes.len() < 13 {
7570 return None;
7571 }
7572 let node_id = u64::from_be_bytes(bytes[0..8].try_into().ok()?);
7573 let level = i32::from_be_bytes(bytes[8..12].try_into().ok()?);
7574 let n_entries =
7575 u32::from_be_bytes(bytes[12..16].try_into().ok()?) as usize;
7576 // dirty byte (1 byte after n_entries)
7577 if bytes.len() < 17 {
7578 return None;
7579 }
7580 let mut pos = 17usize; // skip node_id(8) + level(4) + n_entries(4) + dirty(1)
7581 let mut entries = Vec::with_capacity(n_entries);
7582 let mut lsns: Vec<Lsn> = Vec::with_capacity(n_entries);
7583 for _ in 0..n_entries {
7584 if pos + 2 > bytes.len() {
7585 return None;
7586 }
7587 let key_len =
7588 u16::from_be_bytes(bytes[pos..pos + 2].try_into().ok()?)
7589 as usize;
7590 pos += 2;
7591 if pos + key_len > bytes.len() {
7592 return None;
7593 }
7594 let key = bytes[pos..pos + key_len].to_vec();
7595 pos += key_len;
7596 if pos + 8 > bytes.len() {
7597 return None;
7598 }
7599 let lsn = noxu_util::Lsn::from_u64(u64::from_be_bytes(
7600 bytes[pos..pos + 8].try_into().ok()?,
7601 ));
7602 pos += 8;
7603 entries.push(InEntry { key });
7604 lsns.push(lsn); // T-3
7605 }
7606 Some(InNodeStub {
7607 node_id,
7608 level,
7609 entries,
7610 // T-4: a freshly deserialized IN has no resident children.
7611 targets: TargetRep::None,
7612 dirty: false,
7613 generation: 0,
7614 parent: None,
7615 lsn_rep: LsnRep::from_lsns(&lsns), // T-3
7616 })
7617 }
7618
7619 /// Deserialise a BIN from bytes produced by `BinStub::serialize_full()`.
7620 ///
7621 /// Thin wrapper so the recovery path does not need to import `BinStub`
7622 /// directly from callers that only have the raw bytes.
7623 ///
7624 /// JE `INFileReader.getIN(db)` for a BIN entry.
7625 pub fn deserialize_bin(bytes: &[u8]) -> Option<BinStub> {
7626 let mut bin = BinStub::deserialize_full(bytes)?;
7627 bin.dirty = false; // freshly loaded from log — clean for now
7628 Some(bin)
7629 }
7630
7631 /// Apply a logged IN/BIN to the in-memory tree during the recovery redo pass.
7632 ///
7633 /// Implements JE `RecoveryManager.recoverIN`:
7634 /// - `is_root` nodes are handled by `recover_root_in`.
7635 /// - non-root nodes are handled by `recover_child_in`.
7636 ///
7637 /// `log_lsn` is the LSN at which this IN/BIN was logged. The currency
7638 /// check in `recover_child_in` uses this to decide whether to replace the
7639 /// in-memory slot (tree slot LSN < log_lsn → replace; equal → noop;
7640 /// greater → skip).
7641 ///
7642 /// JE `RecoveryManager.recoverIN` / `replayOneIN`
7643 /// (RecoveryManager.java ~lines 1200–1280).
7644 pub fn recover_in_redo(
7645 &self,
7646 log_lsn: noxu_util::Lsn,
7647 is_root: bool,
7648 is_bin: bool,
7649 node_data: &[u8],
7650 ) -> InRedoResult {
7651 if is_bin {
7652 let Some(bin) = Self::deserialize_bin(node_data) else {
7653 return InRedoResult::DeserializeFailed;
7654 };
7655 if is_root {
7656 self.recover_root_bin(log_lsn, bin)
7657 } else {
7658 self.recover_child_bin(log_lsn, bin)
7659 }
7660 } else {
7661 let Some(upper) = Self::deserialize_upper_in(node_data) else {
7662 return InRedoResult::DeserializeFailed;
7663 };
7664 if is_root {
7665 self.recover_root_upper_in(log_lsn, upper)
7666 } else {
7667 self.recover_child_upper_in(log_lsn, upper)
7668 }
7669 }
7670 }
7671
7672 /// Recover a root BIN.
7673 ///
7674 /// If no root exists or the existing root is older (lower LSN), install
7675 /// this BIN as the new root.
7676 ///
7677 /// JE `RecoveryManager.recoverRootIN` / `RootUpdater.doWork`
7678 /// (RecoveryManager.java ~lines 1293–1410).
7679 fn recover_root_bin(
7680 &self,
7681 log_lsn: noxu_util::Lsn,
7682 bin: BinStub,
7683 ) -> InRedoResult {
7684 let mut root_guard = self.root.write();
7685 let existing_lsn = *self.root_log_lsn.read();
7686 match &*root_guard {
7687 None => {
7688 // No root — install this BIN as the root.
7689 // JE: `root == null` case in `RootUpdater.doWork`.
7690 let node = TreeNode::Bottom(bin);
7691 *root_guard = Some(Arc::new(RwLock::new(node)));
7692 *self.root_log_lsn.write() = log_lsn;
7693 InRedoResult::Inserted
7694 }
7695 Some(_) => {
7696 // JE: `originalLsn = root.getLsn()`; replace if logLsn > originalLsn.
7697 if log_lsn > existing_lsn {
7698 let node = TreeNode::Bottom(bin);
7699 *root_guard = Some(Arc::new(RwLock::new(node)));
7700 *self.root_log_lsn.write() = log_lsn;
7701 InRedoResult::Replaced
7702 } else {
7703 InRedoResult::Skipped
7704 }
7705 }
7706 }
7707 }
7708
7709 /// Recover a root upper IN.
7710 ///
7711 /// JE `RecoveryManager.recoverRootIN` for a non-BIN root.
7712 fn recover_root_upper_in(
7713 &self,
7714 log_lsn: noxu_util::Lsn,
7715 upper: InNodeStub,
7716 ) -> InRedoResult {
7717 let mut root_guard = self.root.write();
7718 let existing_lsn = *self.root_log_lsn.read();
7719 match &*root_guard {
7720 None => {
7721 let node = TreeNode::Internal(upper);
7722 *root_guard = Some(Arc::new(RwLock::new(node)));
7723 *self.root_log_lsn.write() = log_lsn;
7724 InRedoResult::Inserted
7725 }
7726 Some(_) => {
7727 if log_lsn > existing_lsn {
7728 let node = TreeNode::Internal(upper);
7729 *root_guard = Some(Arc::new(RwLock::new(node)));
7730 *self.root_log_lsn.write() = log_lsn;
7731 InRedoResult::Replaced
7732 } else {
7733 InRedoResult::Skipped
7734 }
7735 }
7736 }
7737 }
7738
7739 /// Recover a non-root BIN.
7740 ///
7741 /// Implements the three-case currency check from JE
7742 /// `RecoveryManager.recoverChildIN`
7743 /// (RecoveryManager.java lines 1412–1500):
7744 ///
7745 /// 1. Node not in tree: skip (parent logged a later structure that already
7746 /// omits this node, or node was deleted).
7747 /// 2. Physical match (slot LSN == log_lsn): noop — already current.
7748 /// 3. Logical match: another version of the node is in the slot.
7749 /// Replace if tree slot LSN < log_lsn (tree is older), skip otherwise.
7750 fn recover_child_bin(
7751 &self,
7752 log_lsn: noxu_util::Lsn,
7753 bin: BinStub,
7754 ) -> InRedoResult {
7755 let node_id = bin.node_id;
7756 let Some((parent_arc, slot)) = self.get_parent_in_for_child_in(node_id)
7757 else {
7758 // Case 1: not in tree.
7759 return InRedoResult::NotInTree;
7760 };
7761 let mut parent = parent_arc.write();
7762 let TreeNode::Internal(ref mut p) = *parent else {
7763 return InRedoResult::NotInTree;
7764 };
7765 let tree_lsn = p.get_lsn(slot); // T-3
7766 if tree_lsn == log_lsn {
7767 // Case 2: physical match — noop.
7768 InRedoResult::Skipped
7769 } else if tree_lsn < log_lsn {
7770 // Case 3: logical match, tree is older — replace.
7771 // JE `parent.recoverIN(idx, inFromLog, logLsn, lastLoggedSize)`.
7772 let new_arc = Arc::new(RwLock::new(TreeNode::Bottom(bin)));
7773 // Set parent back-pointer on the new node.
7774 {
7775 let mut ng = new_arc.write();
7776 if let TreeNode::Bottom(ref mut b) = *ng {
7777 b.parent = Some(Arc::downgrade(&parent_arc));
7778 }
7779 }
7780 p.set_child(slot, Some(new_arc));
7781 p.set_lsn(slot, log_lsn); // T-3
7782 InRedoResult::Replaced
7783 } else {
7784 // tree_lsn > log_lsn: tree already holds a newer version.
7785 InRedoResult::Skipped
7786 }
7787 }
7788
7789 /// Recover a non-root upper IN.
7790 ///
7791 /// JE `RecoveryManager.recoverChildIN` for a non-BIN node.
7792 fn recover_child_upper_in(
7793 &self,
7794 log_lsn: noxu_util::Lsn,
7795 upper: InNodeStub,
7796 ) -> InRedoResult {
7797 let node_id = upper.node_id;
7798 let Some((parent_arc, slot)) = self.get_parent_in_for_child_in(node_id)
7799 else {
7800 return InRedoResult::NotInTree;
7801 };
7802 let mut parent = parent_arc.write();
7803 let TreeNode::Internal(ref mut p) = *parent else {
7804 return InRedoResult::NotInTree;
7805 };
7806 let tree_lsn = p.get_lsn(slot); // T-3
7807 if tree_lsn == log_lsn {
7808 InRedoResult::Skipped
7809 } else if tree_lsn < log_lsn {
7810 let new_arc = Arc::new(RwLock::new(TreeNode::Internal(upper)));
7811 {
7812 let mut ng = new_arc.write();
7813 if let TreeNode::Internal(ref mut n) = *ng {
7814 n.parent = Some(Arc::downgrade(&parent_arc));
7815 }
7816 }
7817 p.set_child(slot, Some(new_arc));
7818 p.set_lsn(slot, log_lsn); // T-3
7819 InRedoResult::Replaced
7820 } else {
7821 InRedoResult::Skipped
7822 }
7823 }
7824}
7825
7826/// Result of a single `recover_in_redo` call.
7827///
7828/// JE traces the same outcomes in `RecoveryManager` debug logging.
7829#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7830pub enum InRedoResult {
7831 /// Node was inserted as the new root.
7832 Inserted,
7833 /// Node replaced an older version in the tree.
7834 Replaced,
7835 /// Node not applied: tree already holds an equal or newer version.
7836 Skipped,
7837 /// Node not found in tree (parent logged later structure that excludes it).
7838 NotInTree,
7839 /// Deserialisation of `node_data` bytes failed.
7840 DeserializeFailed,
7841}
7842
7843/// Global node ID counter for generating unique node IDs.
7844///
7845/// This is the SINGLE source of node-ids for the whole tree subsystem. The
7846/// BIN constructor (`bin.rs`) and `node.rs` route through `generate_node_id`
7847/// so that, after crash recovery, a freshly allocated node-id is always
7848/// strictly greater than every node-id present in the recovered log.
7849///
7850/// JE ref: `NodeSequence.getNextLocalNodeId` (a single per-env counter) and
7851/// `IN.nodeId` allocation; `NodeSequence.initRealNodeId` seeds the counter
7852/// from the recovered `CheckpointEnd.lastLocalNodeId`. The env seeds this
7853/// counter post-recovery via `seed_node_id_counter`.
7854static NODE_ID_COUNTER: std::sync::atomic::AtomicU64 =
7855 std::sync::atomic::AtomicU64::new(1);
7856
7857/// Generates a unique node ID.
7858pub fn generate_node_id() -> u64 {
7859 NODE_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
7860}
7861
7862/// Returns the node-id that would be generated next (without allocating it).
7863///
7864/// Used by recovery seeding and by tests to assert no node-id reuse after a
7865/// restart.
7866pub fn peek_next_node_id_counter() -> u64 {
7867 NODE_ID_COUNTER.load(std::sync::atomic::Ordering::SeqCst)
7868}
7869
7870/// Seeds the node-id counter so the next generated id is `> last_node_id`.
7871///
7872/// Called by `EnvironmentImpl` after recovery with the recovered
7873/// `use_max_node_id`, mirroring `NodeSequence.initRealNodeId` /
7874/// `setLastNodeId`: post-restart allocation must never reuse a node-id that
7875/// is already in the log. Monotonic: never lowers the counter.
7876pub fn seed_node_id_counter(last_node_id: u64) {
7877 let want_next = last_node_id.saturating_add(1);
7878 // Bump only if our current next is below the recovered floor.
7879 let mut cur = NODE_ID_COUNTER.load(std::sync::atomic::Ordering::SeqCst);
7880 while cur < want_next {
7881 match NODE_ID_COUNTER.compare_exchange_weak(
7882 cur,
7883 want_next,
7884 std::sync::atomic::Ordering::SeqCst,
7885 std::sync::atomic::Ordering::SeqCst,
7886 ) {
7887 Ok(_) => break,
7888 Err(observed) => cur = observed,
7889 }
7890 }
7891}
7892
7893#[cfg(test)]
7894mod tests {
7895 use super::*;
7896
7897 // ====================================================================
7898 // T-3: LsnRep packed-LSN encoding (IN.entryLsnByteArray / getLsn /
7899 // setLsnInternal, IN.java:1752-1935).
7900 // ====================================================================
7901
7902 /// All-NULL node uses the 0-byte Empty rep; reads return NULL_LSN.
7903 #[test]
7904 fn lsnrep_empty_is_zero_bytes() {
7905 let rep = LsnRep::new(64);
7906 assert!(matches!(rep, LsnRep::Empty));
7907 assert_eq!(rep.memory_size(), 0);
7908 assert_eq!(rep.get(0), NULL_LSN);
7909 assert_eq!(rep.get(63), NULL_LSN);
7910 }
7911
7912 /// LSNs sharing a file number pack to the Compact rep (4 bytes/slot,
7913 /// base_file_number-relative) and round-trip exactly.
7914 #[test]
7915 fn lsnrep_compact_roundtrip_same_file() {
7916 let mut rep = LsnRep::new(8);
7917 for i in 0..8u32 {
7918 rep.set(i as usize, Lsn::new(7, 1000 + i), 8);
7919 }
7920 assert!(matches!(rep, LsnRep::Compact { .. }));
7921 for i in 0..8u32 {
7922 assert_eq!(rep.get(i as usize), Lsn::new(7, 1000 + i));
7923 }
7924 // 8 slots * 4 bytes = 32 bytes, far below 8 * 8 = 64 for raw u64.
7925 assert_eq!(rep.memory_size(), 8 * 4);
7926 }
7927
7928 /// NULL_LSN is stored via the 0xffffff file-offset sentinel, NOT u64::MAX,
7929 /// so a node with NULL slots still packs Compact (the blocker JE solves).
7930 #[test]
7931 fn lsnrep_null_does_not_force_long() {
7932 let mut rep = LsnRep::new(4);
7933 rep.set(0, Lsn::new(3, 50), 4);
7934 rep.set(1, NULL_LSN, 4);
7935 rep.set(2, Lsn::new(3, 60), 4);
7936 rep.set(3, NULL_LSN, 4);
7937 assert!(
7938 matches!(rep, LsnRep::Compact { .. }),
7939 "NULL slots must NOT force the Long rep"
7940 );
7941 assert_eq!(rep.get(0), Lsn::new(3, 50));
7942 assert_eq!(rep.get(1), NULL_LSN);
7943 assert_eq!(rep.get(2), Lsn::new(3, 60));
7944 assert_eq!(rep.get(3), NULL_LSN);
7945 }
7946
7947 /// base_file_number tracks the minimum; setting a lower file number
7948 /// re-bases the whole array (adjustFileNumbers) while staying Compact.
7949 #[test]
7950 fn lsnrep_rebase_on_lower_file_number() {
7951 let mut rep = LsnRep::new(3);
7952 rep.set(0, Lsn::new(10, 5), 3);
7953 rep.set(1, Lsn::new(12, 6), 3);
7954 // A lower file number re-bases base_file_number to 8.
7955 rep.set(2, Lsn::new(8, 7), 3);
7956 assert!(matches!(rep, LsnRep::Compact { .. }));
7957 assert_eq!(rep.get(0), Lsn::new(10, 5));
7958 assert_eq!(rep.get(1), Lsn::new(12, 6));
7959 assert_eq!(rep.get(2), Lsn::new(8, 7));
7960 }
7961
7962 /// A file-number spread > 127 forces the Long fallback (mutateToLongArray),
7963 /// still round-tripping every slot.
7964 #[test]
7965 fn lsnrep_mutates_to_long_on_wide_file_range() {
7966 let mut rep = LsnRep::new(2);
7967 rep.set(0, Lsn::new(1, 5), 2);
7968 rep.set(1, Lsn::new(1000, 6), 2); // diff 999 > 127 -> Long
7969 assert!(matches!(rep, LsnRep::Long(_)));
7970 assert_eq!(rep.get(0), Lsn::new(1, 5));
7971 assert_eq!(rep.get(1), Lsn::new(1000, 6));
7972 }
7973
7974 /// A file offset > MAX_FILE_OFFSET (0xfffffe) forces the Long fallback.
7975 #[test]
7976 fn lsnrep_mutates_to_long_on_large_offset() {
7977 let mut rep = LsnRep::new(2);
7978 rep.set(0, Lsn::new(1, 10), 2);
7979 rep.set(1, Lsn::new(1, 0x00ff_ffff), 2); // > MAX_FILE_OFFSET -> Long
7980 assert!(matches!(rep, LsnRep::Long(_)));
7981 assert_eq!(rep.get(1), Lsn::new(1, 0x00ff_ffff));
7982 }
7983
7984 /// insert_shift / remove_shift keep slots aligned (INArrayRep.copy).
7985 #[test]
7986 fn lsnrep_insert_and_remove_shift() {
7987 let mut rep = LsnRep::from_lsns(&[
7988 Lsn::new(2, 1),
7989 Lsn::new(2, 2),
7990 Lsn::new(2, 3),
7991 ]);
7992 // Insert a new slot at index 1.
7993 rep.insert_shift(1, 4);
7994 rep.set(1, Lsn::new(2, 99), 4);
7995 assert_eq!(rep.get(0), Lsn::new(2, 1));
7996 assert_eq!(rep.get(1), Lsn::new(2, 99));
7997 assert_eq!(rep.get(2), Lsn::new(2, 2));
7998 assert_eq!(rep.get(3), Lsn::new(2, 3));
7999 // Remove slot 1.
8000 rep.remove_shift(1);
8001 assert_eq!(rep.get(0), Lsn::new(2, 1));
8002 assert_eq!(rep.get(1), Lsn::new(2, 2));
8003 assert_eq!(rep.get(2), Lsn::new(2, 3));
8004 }
8005
8006 #[test]
8007 fn test_empty_tree() {
8008 let tree = Tree::new(1, 128);
8009 assert!(tree.is_empty());
8010 assert_eq!(tree.get_database_id(), 1);
8011 assert_eq!(tree.get_root_splits(), 0);
8012 }
8013
8014 #[test]
8015 fn test_redo_insert_older_lsn_does_not_overwrite_newer_slot() {
8016 // REC-F2 reproduce-first: redo() must be idempotent w.r.t. slot
8017 // currency. JE RecoveryManager.redo() (line ~2512/2544) only
8018 // replaces a slot when logrecLsn > treeLsn. A later redo of an
8019 // OLDER committed LN for the same key must NOT revert the slot to
8020 // the older value or reset the slot LSN backward.
8021 let tree = Tree::new(1, 128);
8022 let key = b"k".to_vec();
8023
8024 // Install the newer version at LSN X (e.g. the BIN-logged value).
8025 let newer = Lsn::new(5, 500);
8026 tree.redo_insert(&key, b"new", newer).unwrap();
8027
8028 // Replay an OLDER committed LN at Y < X for the same key.
8029 let older = Lsn::new(2, 200);
8030 tree.redo_insert(&key, b"old", older).unwrap();
8031
8032 // The newer value and LSN must survive.
8033 let got = tree.search_with_data(&key).expect("key present");
8034 assert!(got.found);
8035 assert_eq!(
8036 got.data.as_deref(),
8037 Some(&b"new"[..]),
8038 "older-LSN redo reverted committed data"
8039 );
8040 assert_eq!(
8041 got.lsn,
8042 newer.as_u64(),
8043 "older-LSN redo reset slot LSN backward"
8044 );
8045
8046 // A redo at a strictly NEWER LSN must still replace (replace-only
8047 // when log_lsn > slot_lsn, matching JE lsnCmp > 0).
8048 let newest = Lsn::new(9, 900);
8049 tree.redo_insert(&key, b"newest", newest).unwrap();
8050 let got = tree.search_with_data(&key).expect("key present");
8051 assert_eq!(got.data.as_deref(), Some(&b"newest"[..]));
8052 assert_eq!(got.lsn, newest.as_u64());
8053 }
8054
8055 #[test]
8056 fn test_insert_single() {
8057 let tree = Tree::new(1, 128);
8058 let key = b"testkey".to_vec();
8059 let data = b"testdata".to_vec();
8060 let lsn = Lsn::new(1, 100);
8061
8062 let result = tree.insert(key.clone(), data, lsn);
8063 assert!(result.is_ok());
8064 assert!(result.unwrap()); // Should be a new insert
8065
8066 assert!(!tree.is_empty());
8067
8068 // Verify we can search for it
8069 let search_result = tree.search(&key);
8070 assert!(search_result.is_some());
8071 let sr = search_result.unwrap();
8072 assert!(sr.exact_parent_found || !sr.child_not_resident);
8073 }
8074
8075 #[test]
8076 fn test_insert_multiple() {
8077 let tree = Tree::new(1, 128);
8078
8079 let keys = vec![
8080 b"apple".to_vec(),
8081 b"banana".to_vec(),
8082 b"cherry".to_vec(),
8083 b"date".to_vec(),
8084 ];
8085
8086 for (i, key) in keys.iter().enumerate() {
8087 let data = format!("data{}", i).into_bytes();
8088 let lsn = Lsn::new(1, 100 + (i as u32) * 10);
8089 let result = tree.insert(key.clone(), data, lsn);
8090 assert!(result.is_ok());
8091 assert!(result.unwrap()); // All should be new inserts
8092 }
8093
8094 // Verify we can search for each
8095 for key in &keys {
8096 let search_result = tree.search(key);
8097 assert!(search_result.is_some());
8098 }
8099 }
8100
8101 #[test]
8102 fn test_insert_duplicate_key() {
8103 let tree = Tree::new(1, 128);
8104 let key = b"duplicate".to_vec();
8105 let data1 = b"first".to_vec();
8106 let data2 = b"second".to_vec();
8107 let lsn1 = Lsn::new(1, 100);
8108 let lsn2 = Lsn::new(1, 200);
8109
8110 // First insert
8111 let result1 = tree.insert(key.clone(), data1, lsn1);
8112 assert!(result1.is_ok());
8113 assert!(result1.unwrap()); // New insert
8114
8115 // Second insert with same key - should be update
8116 let result2 = tree.insert(key, data2, lsn2);
8117 assert!(result2.is_ok());
8118 assert!(!result2.unwrap()); // Update, not new insert
8119 }
8120
8121 #[test]
8122 fn test_search_empty_tree() {
8123 let tree = Tree::new(1, 128);
8124 let key = b"noexist".to_vec();
8125
8126 let result = tree.search(&key);
8127 assert!(result.is_none());
8128 }
8129
8130 #[test]
8131 fn test_first_and_last_node() {
8132 let tree = Tree::new(1, 128);
8133
8134 // Empty tree
8135 assert!(tree.get_first_node().is_none());
8136 assert!(tree.get_last_node().is_none());
8137
8138 // Insert some keys
8139 let keys = [b"a".to_vec(), b"b".to_vec(), b"c".to_vec()];
8140 for (i, key) in keys.iter().enumerate() {
8141 let data = format!("data{}", i).into_bytes();
8142 let lsn = Lsn::new(1, 100 + (i as u32) * 10);
8143 tree.insert(key.clone(), data, lsn).unwrap();
8144 }
8145
8146 // Now should have first and last
8147 let first = tree.get_first_node();
8148 assert!(first.is_some());
8149 assert_eq!(first.unwrap().index, 0);
8150
8151 let last = tree.get_last_node();
8152 assert!(last.is_some());
8153 assert_eq!(last.unwrap().index, 2);
8154 }
8155
8156 #[test]
8157 fn test_node_id_generation() {
8158 let id1 = generate_node_id();
8159 let id2 = generate_node_id();
8160 let id3 = generate_node_id();
8161
8162 assert!(id2 > id1);
8163 assert!(id3 > id2);
8164 }
8165
8166 #[test]
8167 fn test_tree_node_is_bin() {
8168 let bin = TreeNode::Bottom(BinStub {
8169 node_id: 1,
8170 level: BIN_LEVEL,
8171 entries: vec![],
8172 key_prefix: Vec::new(),
8173 dirty: false,
8174 is_delta: false,
8175 last_full_lsn: NULL_LSN,
8176 last_delta_lsn: NULL_LSN,
8177 generation: 0,
8178 parent: None,
8179 expiration_in_hours: true,
8180 cursor_count: 0,
8181 prohibit_next_delta: false,
8182 lsn_rep: LsnRep::Empty,
8183 keys: KeyRep::new(),
8184 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
8185 });
8186 assert!(bin.is_bin());
8187 assert_eq!(bin.level(), BIN_LEVEL);
8188
8189 let internal = TreeNode::Internal(InNodeStub {
8190 node_id: 2,
8191 level: MAIN_LEVEL + 2,
8192 entries: vec![],
8193 targets: TargetRep::None,
8194 dirty: false,
8195 generation: 0,
8196 parent: None,
8197 lsn_rep: LsnRep::Empty,
8198 });
8199 assert!(!internal.is_bin());
8200 assert_eq!(internal.level(), MAIN_LEVEL + 2);
8201 }
8202
8203 #[test]
8204 fn test_find_entry() {
8205 let mut entries = vec![];
8206 let mut keys = vec![];
8207 for i in 0..5 {
8208 entries.push(BinEntry {
8209 data: Some(vec![]),
8210 known_deleted: false,
8211 dirty: false,
8212 expiration_time: 0,
8213 });
8214 keys.push(format!("key{}", i).into_bytes());
8215 }
8216
8217 let bin = TreeNode::Bottom(BinStub {
8218 node_id: 1,
8219 level: BIN_LEVEL,
8220 entries,
8221 key_prefix: Vec::new(),
8222 dirty: false,
8223 is_delta: false,
8224 last_full_lsn: NULL_LSN,
8225 last_delta_lsn: NULL_LSN,
8226 generation: 0,
8227 parent: None,
8228 expiration_in_hours: true,
8229 cursor_count: 0,
8230 prohibit_next_delta: false,
8231 lsn_rep: LsnRep::Empty,
8232 keys: KeyRep::from_keys(keys),
8233 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
8234 });
8235
8236 // Search for existing key
8237 let result = bin.find_entry(b"key2", false, true);
8238 assert_eq!(result & 0xFFFF, 2);
8239 assert_ne!(result & EXACT_MATCH, 0);
8240
8241 // Search for non-existing key with exact=false
8242 let result = bin.find_entry(b"key15", false, false);
8243 assert_eq!(result & 0xFFFF, 2); // Would go between key1 and key2
8244 assert_eq!(result & EXACT_MATCH, 0);
8245 }
8246
8247 #[test]
8248 fn test_insert_until_full() {
8249 // With splits implemented, inserting beyond max_entries_per_node must
8250 // succeed (the tree splits proactively rather than returning an error).
8251 let tree = Tree::new(1, 3); // Small max to exercise splits
8252
8253 // Insert up to max
8254 for i in 0..3 {
8255 let key = format!("key{}", i).into_bytes();
8256 let data = format!("data{}", i).into_bytes();
8257 let lsn = Lsn::new(1, 100 + i);
8258 let result = tree.insert(key, data, lsn);
8259 assert!(result.is_ok(), "insert {} should succeed", i);
8260 }
8261
8262 // The 4th insert triggers a split and must also succeed.
8263 let key = b"key3".to_vec();
8264 let data = b"data3".to_vec();
8265 let lsn = Lsn::new(1, 103);
8266 let result = tree.insert(key.clone(), data, lsn);
8267 assert!(
8268 result.is_ok(),
8269 "insert after full should trigger split and succeed"
8270 );
8271 assert!(result.unwrap(), "should be a new insert");
8272
8273 // The inserted key must be findable after the split.
8274 let sr = tree.search(&key);
8275 assert!(sr.is_some(), "key3 must be searchable after split");
8276 assert!(sr.unwrap().exact_parent_found, "key3 must be found exactly");
8277 }
8278
8279 #[test]
8280 fn test_memory_counter_balanced_on_insert_delete_f8() {
8281 use std::sync::Arc;
8282 use std::sync::atomic::{AtomicI64, Ordering};
8283 // F8 regression: insert accounts key+data+48; delete must subtract the
8284 // SAME, so an insert+delete of the same record returns the counter to
8285 // its starting value (previously delete omitted data_len -> the counter
8286 // leaked data_len per delete, biasing the evictor over-budget view).
8287 let mut tree = Tree::new(1, 16);
8288 let counter = Arc::new(AtomicI64::new(0));
8289 tree.set_memory_counter(Arc::clone(&counter));
8290
8291 let key = b"a-key".to_vec();
8292 let data = vec![0u8; 200]; // non-trivial data length
8293 tree.insert(key.clone(), data.clone(), Lsn::new(0, 10)).unwrap();
8294 let after_insert = counter.load(Ordering::Relaxed);
8295 assert!(after_insert > 0, "insert must increase the counter");
8296 assert_eq!(
8297 after_insert,
8298 (key.len() + data.len() + BIN_ENTRY_OVERHEAD) as i64,
8299 "insert accounts key + data + per-slot BinEntry overhead"
8300 );
8301
8302 let deleted = tree.delete(&key);
8303 assert!(deleted);
8304 assert_eq!(
8305 counter.load(Ordering::Relaxed),
8306 0,
8307 "F8: delete must subtract key + data + BIN_ENTRY_OVERHEAD, returning the counter to its pre-insert value (no data_len leak)"
8308 );
8309 }
8310
8311 /// EV-13 (pass-post): a full-node detach must ACTUALLY drop the child
8312 /// `Arc` from the parent IN, not merely credit bytes. Before the fix the
8313 /// evictor credited `node_size_fn(node_id)` and removed the node from the
8314 /// LRU list, but the parent's `InEntry.child` still held a strong `Arc`,
8315 /// so the node was never freed (phantom free) and the budget over-credited.
8316 ///
8317 /// This test proves: after `detach_node_by_id` the held child `Arc` is the
8318 /// LAST strong reference (strong_count == 1), the parent slot's `child` is
8319 /// `None`, and the returned bytes equal the node's measured heap size.
8320 ///
8321 /// JE ref: `IN.detachNode` (`setTarget(idx, null)`) / `Evictor.evict`.
8322 #[test]
8323 fn test_ev13_detach_actually_frees_child() {
8324 // Tiny fanout forces a root split so we get a real IN parent with BIN
8325 // children that the evictor would target.
8326 let tree = Tree::new(7, 4);
8327 for i in 0u8..12 {
8328 tree.insert(
8329 vec![b'a' + i],
8330 vec![i; 8],
8331 Lsn::new(1, u32::from(i) + 1),
8332 )
8333 .unwrap();
8334 }
8335
8336 // Find a BIN child of the root IN (the eviction target) + its parent.
8337 let root = tree.get_root().expect("tree must have a root");
8338 let (parent_arc, child_idx, bin_id, expected_bytes) = {
8339 let rg = root.read();
8340 let TreeNode::Internal(n) = &*rg else {
8341 panic!("root must be an IN after split");
8342 };
8343 // Pick the first slot whose child is a resident BIN.
8344 let (idx, child) = n
8345 .first_resident_child()
8346 .expect("root must have a resident child");
8347 let (id, bytes) = {
8348 let cg = child.read();
8349 (
8350 match &*cg {
8351 TreeNode::Bottom(b) => b.node_id,
8352 TreeNode::Internal(n2) => n2.node_id,
8353 },
8354 cg.budgeted_memory_size(),
8355 )
8356 };
8357 (Arc::clone(&root), idx, id, bytes)
8358 };
8359
8360 // Hold an external strong reference to the child so we can observe its
8361 // strong_count drop when detach releases the parent's reference.
8362 let child_arc = {
8363 let pg = parent_arc.read();
8364 let TreeNode::Internal(n) = &*pg else { unreachable!() };
8365 Arc::clone(n.child_ref(child_idx).unwrap())
8366 };
8367 // Two strong refs now: the parent slot + our test handle.
8368 assert_eq!(
8369 Arc::strong_count(&child_arc),
8370 2,
8371 "precondition: parent slot + test handle hold the child"
8372 );
8373
8374 let freed = tree.detach_node_by_id(bin_id);
8375
8376 // 1. Bytes credited equal the measured heap size (no phantom credit).
8377 assert_eq!(
8378 freed, expected_bytes,
8379 "detach must credit the node's real measured heap size"
8380 );
8381 // 2. The parent slot's child is now None (JE setTarget(idx, null)).
8382 {
8383 let pg = parent_arc.read();
8384 let TreeNode::Internal(n) = &*pg else { unreachable!() };
8385 assert!(
8386 n.child_is_none(child_idx),
8387 "EV-13: parent slot must be detached (child == None)"
8388 );
8389 // The slot itself (key + LSN) is retained for re-fetch.
8390 assert!(
8391 !n.get_lsn(child_idx).is_null(),
8392 "detach keeps the slot LSN so the node can be re-fetched"
8393 );
8394 }
8395 // 3. Our handle is now the ONLY strong reference -> the parent really
8396 // dropped its Arc; the node is freed when we drop `child_arc`.
8397 // Before EV-13 this would be 2 (parent still held it) = phantom free.
8398 assert_eq!(
8399 Arc::strong_count(&child_arc),
8400 1,
8401 "EV-13: detach must drop the parent's strong Arc (no phantom free)"
8402 );
8403 }
8404
8405 /// EV-13: detach must NOT decrement the memory counter itself (the evictor
8406 /// owns that bookkeeping via `Arbiter::release_memory`). A double credit
8407 /// would drive `cache_usage` below reality.
8408 #[test]
8409 fn test_ev13_detach_does_not_touch_counter() {
8410 use std::sync::atomic::{AtomicI64, Ordering};
8411 let mut tree = Tree::new(8, 4);
8412 let counter = Arc::new(AtomicI64::new(0));
8413 tree.set_memory_counter(Arc::clone(&counter));
8414 for i in 0u8..12 {
8415 tree.insert(
8416 vec![b'a' + i],
8417 vec![i; 8],
8418 Lsn::new(1, u32::from(i) + 1),
8419 )
8420 .unwrap();
8421 }
8422 let before = counter.load(Ordering::Relaxed);
8423
8424 // Grab a BIN child id.
8425 let root = tree.get_root().unwrap();
8426 let bin_id = {
8427 let rg = root.read();
8428 let TreeNode::Internal(n) = &*rg else { unreachable!() };
8429 let child = n
8430 .resident_children()
8431 .into_iter()
8432 .next()
8433 .expect("resident child");
8434 match &*child.read() {
8435 TreeNode::Bottom(b) => b.node_id,
8436 TreeNode::Internal(n2) => n2.node_id,
8437 }
8438 };
8439
8440 let freed = tree.detach_node_by_id(bin_id);
8441 assert!(freed > 0, "detach must free a resident child");
8442 assert_eq!(
8443 counter.load(Ordering::Relaxed),
8444 before,
8445 "EV-13: detach must not change the counter (evictor credits once)"
8446 );
8447 }
8448
8449 /// EV-13: detaching the root or an unknown id is a no-op returning 0.
8450 #[test]
8451 fn test_ev13_detach_root_or_missing_is_noop() {
8452 let tree = Tree::new(9, 4);
8453 for i in 0u8..12 {
8454 tree.insert(
8455 vec![b'a' + i],
8456 vec![i; 8],
8457 Lsn::new(1, u32::from(i) + 1),
8458 )
8459 .unwrap();
8460 }
8461 let root_id = {
8462 let rg = tree.get_root().unwrap();
8463 let g = rg.read();
8464 match &*g {
8465 TreeNode::Internal(n) => n.node_id,
8466 TreeNode::Bottom(b) => b.node_id,
8467 }
8468 };
8469 assert_eq!(
8470 tree.detach_node_by_id(root_id),
8471 0,
8472 "root has no parent IN -> detach is a no-op"
8473 );
8474 assert_eq!(
8475 tree.detach_node_by_id(u64::MAX),
8476 0,
8477 "unknown node id -> detach is a no-op"
8478 );
8479 }
8480
8481 /// DBI-23 (pass-post): the live `memory_counter` must APPROXIMATE the real
8482 /// in-memory heap of the tree, not the old `key + data + 48` lower bound.
8483 ///
8484 /// JE keeps `inMemorySize` (`IN.getBudgetedMemorySize`) in lock-step with
8485 /// the per-node `computeMemorySize`; the over-budget arbiter sees the real
8486 /// figure so eviction fires at the right time. The previous Noxu live
8487 /// path undercounted each BIN slot (48 vs the 64-byte `BinEntry` struct)
8488 /// and never accounted the node-struct fixed overhead, so the counter ran
8489 /// below real heap and the evictor under-fired.
8490 ///
8491 /// We assert the live counter is within tolerance of
8492 /// `total_budgeted_memory` (the authoritative walk-and-sum oracle). The
8493 /// only gap is the per-node fixed struct overhead (BinStub/InNodeStub),
8494 /// which is a small fraction for non-trivial entries — the fix closes the
8495 /// dominant per-slot gap.
8496 #[test]
8497 fn test_dbi23_live_counter_approximates_real_heap() {
8498 use std::sync::atomic::{AtomicI64, Ordering};
8499 let mut tree = Tree::new(42, 32);
8500 let counter = Arc::new(AtomicI64::new(0));
8501 tree.set_memory_counter(Arc::clone(&counter));
8502
8503 // Insert N entries with realistic key+data sizes.
8504 let n = 400u32;
8505 for i in 0..n {
8506 let key = format!("key-{i:08}").into_bytes(); // 12 bytes
8507 let data = vec![0u8; 64]; // 64 bytes
8508 tree.insert(key, data, Lsn::new(1, i + 1)).unwrap();
8509 }
8510
8511 let live = counter.load(Ordering::Relaxed) as u64;
8512 let real = tree.total_budgeted_memory();
8513
8514 // The live counter must reflect the per-slot cost AFTER the T-2/T-3
8515 // compactions hoisted the per-slot key/LSN out of `BinEntry` into the
8516 // node-level reps. The per-slot live charge is now
8517 // `key + data + size_of::<BinEntry>() + 4` (the packed LSN slot); the
8518 // dominant data+key bytes are still charged in full. Assert the live
8519 // counter is at least the data-and-fixed portion (a stable floor that
8520 // does NOT assume the pre-compaction 64-byte slot).
8521 let new_lower_bound: u64 = (0..n)
8522 .map(|i| {
8523 let key_len = format!("key-{i:08}").len();
8524 (key_len + 64 + BIN_ENTRY_OVERHEAD) as u64
8525 })
8526 .sum();
8527
8528 assert!(
8529 live >= new_lower_bound,
8530 "DBI-23: live counter ({live}) must be >= the per-slot-correct \
8531 lower bound ({new_lower_bound})"
8532 );
8533
8534 // Within tolerance of real heap (the residual gap is the per-node
8535 // fixed struct overhead, intentionally not tracked incrementally).
8536 let lower = real * 80 / 100;
8537 assert!(
8538 live >= lower && live <= real,
8539 "DBI-23: live counter ({live}) must approximate real heap ({real}) \
8540 within tolerance [{lower}, {real}]"
8541 );
8542 }
8543
8544 #[test]
8545 fn test_delete_existing_key() {
8546 let tree = Tree::new(1, 128);
8547 let key = b"remove_me".to_vec();
8548 tree.insert(key.clone(), b"val".to_vec(), Lsn::new(1, 10)).unwrap();
8549 assert!(tree.delete(&key));
8550
8551 // After deletion the BIN is empty, so delete returns true the first
8552 // time and false the second time.
8553 assert!(!tree.delete(&key));
8554 }
8555
8556 #[test]
8557 fn test_delete_nonexistent_key() {
8558 let tree = Tree::new(1, 128);
8559 tree.insert(b"a".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
8560
8561 assert!(!tree.delete(b"zzz"));
8562 }
8563
8564 #[test]
8565 fn test_delete_empty_tree() {
8566 let tree = Tree::new(1, 128);
8567 assert!(!tree.delete(b"nothing"));
8568 }
8569
8570 #[test]
8571 fn test_delete_all_entries_makes_bin_empty() {
8572 let tree = Tree::new(1, 128);
8573 tree.insert(b"x".to_vec(), b"1".to_vec(), Lsn::new(1, 1)).unwrap();
8574 tree.insert(b"y".to_vec(), b"2".to_vec(), Lsn::new(1, 2)).unwrap();
8575
8576 assert!(tree.delete(b"x"));
8577 assert!(tree.delete(b"y"));
8578
8579 // Tree still has a root (empty BIN), so is_empty() returns false.
8580 assert!(!tree.is_empty());
8581 // get_first_node should return None for an empty BIN.
8582 assert!(tree.get_first_node().is_none());
8583 }
8584
8585 #[test]
8586 fn test_set_root_and_get_root() {
8587 let tree = Tree::new(1, 128);
8588 assert!(tree.get_root().is_none());
8589
8590 let bin = TreeNode::Bottom(BinStub {
8591 node_id: generate_node_id(),
8592 level: BIN_LEVEL,
8593 entries: vec![],
8594 key_prefix: Vec::new(),
8595 dirty: false,
8596 is_delta: false,
8597 last_full_lsn: NULL_LSN,
8598 last_delta_lsn: NULL_LSN,
8599 generation: 0,
8600 parent: None,
8601 expiration_in_hours: true,
8602 cursor_count: 0,
8603 prohibit_next_delta: false,
8604 lsn_rep: LsnRep::Empty,
8605 keys: KeyRep::new(),
8606 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
8607 });
8608 tree.set_root(bin);
8609 assert!(tree.get_root().is_some());
8610 }
8611
8612 // ========================================================================
8613 // Split / multi-level insert tests (new)
8614 // ========================================================================
8615
8616 /// inserting enough keys to fill the root IN causes
8617 /// the root IN itself to split, resulting in a tree with 3 or more levels.
8618 ///
8619 /// With max_entries_per_node = 4:
8620 /// - Each BIN holds 4 entries before it is split.
8621 /// - The root IN at level 2 holds up to 4 BIN children.
8622 /// - Filling those 4 BINs (16 entries) and adding a 17th forces the
8623 /// root IN to split, creating a level-3 root.
8624 #[test]
8625 fn test_insert_forces_root_split() {
8626 let tree = Tree::new(1, 4);
8627
8628 // 17 inserts with fanout 4 forces the root IN to split.
8629 for i in 0u32..20 {
8630 let key = format!("key{:04}", i).into_bytes();
8631 let data = format!("data{}", i).into_bytes();
8632 let lsn = Lsn::new(1, 100 + i);
8633 let r = tree.insert(key, data, lsn);
8634 assert!(r.is_ok(), "insert {} must succeed", i);
8635 }
8636
8637 // At least one root split must have occurred.
8638 assert!(
8639 tree.get_root_splits() > 0,
8640 "expected at least one root split after 20 inserts with fanout 4"
8641 );
8642
8643 // The root level must be > level-2 (i.e., the tree has grown to 3+ levels).
8644 let root_arc = tree.get_root().as_ref().unwrap().clone();
8645 let root_level = root_arc.read().level();
8646 let level_2 = MAIN_LEVEL | 2;
8647 assert!(
8648 root_level > level_2,
8649 "root level {} must be > level-2 after root split",
8650 root_level
8651 );
8652 }
8653
8654 /// Inserting 1000 keys in sorted order and verifying all are searchable.
8655 #[test]
8656 fn test_insert_many_keys() {
8657 let tree = Tree::new(1, 8);
8658 let n = 1000u32;
8659
8660 for i in 0..n {
8661 let key = format!("key{:08}", i).into_bytes();
8662 let data = format!("data{}", i).into_bytes();
8663 let lsn = Lsn::new(1, i);
8664 let r = tree.insert(key, data, lsn);
8665 assert!(r.is_ok(), "insert {} must succeed", i);
8666 }
8667
8668 // All keys must be findable.
8669 for i in 0..n {
8670 let key = format!("key{:08}", i).into_bytes();
8671 let sr = tree.search(&key);
8672 assert!(
8673 sr.is_some() && sr.unwrap().exact_parent_found,
8674 "key{:08} must be found after bulk insert",
8675 i
8676 );
8677 }
8678 }
8679
8680 /// Inserting 500 keys in pseudo-random (reverse) order and verifying all
8681 /// are searchable.
8682 #[test]
8683 fn test_insert_random_keys() {
8684 let tree = Tree::new(1, 8);
8685 let n = 500u32;
8686
8687 // Insert in reverse order as a simple non-sorted sequence.
8688 for i in (0..n).rev() {
8689 let key = format!("rkey{:08}", i).into_bytes();
8690 let data = format!("data{}", i).into_bytes();
8691 let lsn = Lsn::new(1, i);
8692 let r = tree.insert(key, data, lsn);
8693 assert!(r.is_ok(), "insert {} must succeed", i);
8694 }
8695
8696 for i in 0..n {
8697 let key = format!("rkey{:08}", i).into_bytes();
8698 let sr = tree.search(&key);
8699 assert!(
8700 sr.is_some() && sr.unwrap().exact_parent_found,
8701 "rkey{:08} must be found",
8702 i
8703 );
8704 }
8705 }
8706
8707 /// After any number of splits, every key inserted must still be findable.
8708 ///
8709 #[test]
8710 fn test_split_preserves_all_keys() {
8711 // Tiny fanout to maximise split frequency.
8712 let tree = Tree::new(1, 3);
8713 let n = 60u32;
8714
8715 let mut keys: Vec<Vec<u8>> = Vec::new();
8716 for i in 0..n {
8717 let key = format!("sk{:04}", i).into_bytes();
8718 keys.push(key.clone());
8719 let data = format!("d{}", i).into_bytes();
8720 let lsn = Lsn::new(1, i);
8721 let r = tree.insert(key, data, lsn);
8722 assert!(r.is_ok(), "insert {} must not fail", i);
8723 }
8724
8725 // After all inserts (and all the splits they induced), every key must
8726 // still be findable in the tree.
8727 for key in &keys {
8728 let sr = tree.search(key);
8729 assert!(
8730 sr.is_some() && sr.unwrap().exact_parent_found,
8731 "key {:?} must survive all splits",
8732 std::str::from_utf8(key).unwrap_or("?")
8733 );
8734 }
8735 }
8736
8737 /// The tree level (depth) must grow as keys are inserted and splits occur.
8738 #[test]
8739 fn test_tree_height_grows() {
8740 let tree = Tree::new(1, 4);
8741
8742 // With fanout 4, one level-2 root IN can hold 4 children. After enough
8743 // inserts the root itself will split and a level-3 node will appear.
8744 // Insert enough keys to force the root to split at least once.
8745 let n = 40u32;
8746 for i in 0..n {
8747 let key = format!("hk{:08}", i).into_bytes();
8748 let data = format!("d{}", i).into_bytes();
8749 let lsn = Lsn::new(1, i);
8750 tree.insert(key, data, lsn).unwrap();
8751 }
8752
8753 // At least one root split must have occurred.
8754 assert!(
8755 tree.get_root_splits() > 0,
8756 "expected root to have split at least once for {} keys with fanout 4",
8757 n
8758 );
8759
8760 // The root level must be > level-2 (i.e., the tree has grown past two levels).
8761 let root_arc = tree.get_root().as_ref().unwrap().clone();
8762 let root_level = root_arc.read().level();
8763 let level_2 = MAIN_LEVEL | 2;
8764 assert!(
8765 root_level > level_2,
8766 "root level {} must be > {} after enough inserts",
8767 root_level,
8768 level_2
8769 );
8770 }
8771
8772 #[test]
8773 fn test_find_entry_on_internal_node() {
8774 let mut entries = vec![];
8775 for i in 0..4 {
8776 entries.push(InEntry { key: format!("k{}", i).into_bytes() });
8777 }
8778 let internal = TreeNode::Internal(InNodeStub {
8779 node_id: 1,
8780 level: MAIN_LEVEL + 2,
8781 entries,
8782 targets: TargetRep::None,
8783 dirty: false,
8784 generation: 0,
8785 parent: None,
8786 lsn_rep: LsnRep::Empty,
8787 });
8788
8789 // Exact match
8790 let r = internal.find_entry(b"k2", false, true);
8791 assert_ne!(r & EXACT_MATCH, 0);
8792 assert_eq!(r & 0xFFFF, 2);
8793
8794 // No exact match with exact=true
8795 let r = internal.find_entry(b"kx", false, true);
8796 assert_eq!(r, -1);
8797 }
8798
8799 // St-H5: non-exact `find_entry` on an Internal node must return the FLOOR
8800 // child slot (largest entry ≤ key), not the insertion point. Entries are
8801 // k0,k1,k2,k3; slot 0 is the leftmost child.
8802 #[test]
8803 fn test_find_entry_internal_nonexact_returns_floor() {
8804 let mut entries = vec![];
8805 for i in 0..4 {
8806 entries.push(InEntry { key: format!("k{}", i).into_bytes() });
8807 }
8808 let internal = TreeNode::Internal(InNodeStub {
8809 node_id: 1,
8810 level: MAIN_LEVEL + 2,
8811 entries,
8812 targets: TargetRep::None,
8813 dirty: false,
8814 generation: 0,
8815 parent: None,
8816 lsn_rep: LsnRep::Empty,
8817 });
8818
8819 // Key below every separator floors to slot 0 (leftmost child).
8820 assert_eq!(internal.find_entry(b"a", false, false) & 0xFFFF, 0);
8821 // Between k1 and k2 floors to k1 (slot 1).
8822 assert_eq!(internal.find_entry(b"k1x", false, false) & 0xFFFF, 1);
8823 // Above every separator floors to the last slot (k3 = slot 3).
8824 assert_eq!(internal.find_entry(b"zzz", false, false) & 0xFFFF, 3);
8825 // Exact match still reported as the exact slot.
8826 let r = internal.find_entry(b"k2", false, false);
8827 assert_ne!(r & EXACT_MATCH, 0);
8828 assert_eq!(r & 0xFFFF, 2);
8829 }
8830
8831 // ========================================================================
8832 // New tests: dirty tracking, generation, parent pointers, log size, stats
8833 // ========================================================================
8834
8835 /// After inserting into a tree, the BIN (and root IN) must be dirty.
8836 ///
8837 /// The: Tree.insertLN() calls bin.setDirty(true) after each insert.
8838 #[test]
8839 fn test_insert_marks_bin_dirty() {
8840 let tree = Tree::new(1, 128);
8841 tree.insert(b"key1".to_vec(), b"val1".to_vec(), Lsn::new(1, 1))
8842 .unwrap();
8843
8844 let root_arc = tree.get_root().as_ref().unwrap().clone();
8845 // root is an upper IN — its slot 0 child is the BIN.
8846 let bin_arc = {
8847 let g = root_arc.read();
8848 match &*g {
8849 TreeNode::Internal(n) => n.get_child(0).unwrap(),
8850 _ => panic!("expected Internal root"),
8851 }
8852 };
8853
8854 let bin_dirty = bin_arc.read().is_dirty();
8855 assert!(bin_dirty, "BIN must be dirty after insert");
8856 }
8857
8858 /// Updating an existing key keeps the BIN dirty.
8859 #[test]
8860 fn test_update_keeps_bin_dirty() {
8861 let tree = Tree::new(1, 128);
8862 tree.insert(b"k".to_vec(), b"v1".to_vec(), Lsn::new(1, 1)).unwrap();
8863 // second insert is an update
8864 tree.insert(b"k".to_vec(), b"v2".to_vec(), Lsn::new(1, 2)).unwrap();
8865
8866 let root_arc = tree.get_root().as_ref().unwrap().clone();
8867 let bin_arc = {
8868 let g = root_arc.read();
8869 match &*g {
8870 TreeNode::Internal(n) => n.get_child(0).unwrap(),
8871 _ => panic!("expected Internal root"),
8872 }
8873 };
8874
8875 assert!(bin_arc.read().is_dirty(), "BIN must be dirty after update");
8876 }
8877
8878 /// After deleting a key the BIN must be dirty.
8879 #[test]
8880 fn test_delete_marks_bin_dirty() {
8881 let tree = Tree::new(1, 128);
8882 tree.insert(b"del".to_vec(), b"val".to_vec(), Lsn::new(1, 1)).unwrap();
8883
8884 // Manually clear dirty flag to verify delete re-sets it.
8885 {
8886 let root_arc = tree.get_root().as_ref().unwrap().clone();
8887 let bin_arc = {
8888 let g = root_arc.read();
8889 match &*g {
8890 TreeNode::Internal(n) => n.get_child(0).unwrap(),
8891 _ => panic!("expected Internal root"),
8892 }
8893 };
8894 bin_arc.write().set_dirty(false);
8895 assert!(!bin_arc.read().is_dirty());
8896 }
8897
8898 tree.delete(b"del");
8899
8900 let root_arc = tree.get_root().as_ref().unwrap().clone();
8901 let bin_arc = {
8902 let g = root_arc.read();
8903 match &*g {
8904 TreeNode::Internal(n) => n.get_child(0).unwrap(),
8905 _ => panic!("expected Internal root"),
8906 }
8907 };
8908 assert!(bin_arc.read().is_dirty(), "BIN must be dirty after delete");
8909 }
8910
8911 /// BIN's parent pointer must point to the root IN.
8912 #[test]
8913 fn test_bin_parent_pointer_set_on_initial_insert() {
8914 let tree = Tree::new(1, 128);
8915 tree.insert(b"k".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
8916
8917 let root_arc = tree.get_root().as_ref().unwrap().clone();
8918 let bin_arc = {
8919 let g = root_arc.read();
8920 match &*g {
8921 TreeNode::Internal(n) => n.get_child(0).unwrap(),
8922 _ => panic!("expected Internal root"),
8923 }
8924 };
8925
8926 let parent_weak = bin_arc.read().get_parent();
8927 assert!(parent_weak.is_some(), "BIN must have a parent pointer");
8928
8929 // Upgrading the weak pointer must give us the root arc.
8930 let parent_arc = parent_weak.unwrap().upgrade().unwrap();
8931 assert!(
8932 Arc::ptr_eq(&parent_arc, &root_arc),
8933 "BIN parent must be the root IN"
8934 );
8935 }
8936
8937 /// set_dirty / is_dirty round-trip on both variants.
8938 #[test]
8939 fn test_dirty_flag_roundtrip() {
8940 let mut bin_node = TreeNode::Bottom(BinStub {
8941 node_id: 1,
8942 level: BIN_LEVEL,
8943 entries: vec![],
8944 key_prefix: Vec::new(),
8945 dirty: false,
8946 is_delta: false,
8947 last_full_lsn: NULL_LSN,
8948 last_delta_lsn: NULL_LSN,
8949 generation: 0,
8950 parent: None,
8951 expiration_in_hours: true,
8952 cursor_count: 0,
8953 prohibit_next_delta: false,
8954 lsn_rep: LsnRep::Empty,
8955 keys: KeyRep::new(),
8956 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
8957 });
8958 assert!(!bin_node.is_dirty());
8959 bin_node.set_dirty(true);
8960 assert!(bin_node.is_dirty());
8961 bin_node.set_dirty(false);
8962 assert!(!bin_node.is_dirty());
8963
8964 let mut in_node = TreeNode::Internal(InNodeStub {
8965 node_id: 2,
8966 level: MAIN_LEVEL | 2,
8967 entries: vec![],
8968 targets: TargetRep::None,
8969 dirty: false,
8970 generation: 0,
8971 parent: None,
8972 lsn_rep: LsnRep::Empty,
8973 });
8974 assert!(!in_node.is_dirty());
8975 in_node.set_dirty(true);
8976 assert!(in_node.is_dirty());
8977 }
8978
8979 /// set_generation / get_generation round-trip on both variants.
8980 #[test]
8981 fn test_generation_roundtrip() {
8982 let mut bin_node = TreeNode::Bottom(BinStub {
8983 node_id: 1,
8984 level: BIN_LEVEL,
8985 entries: vec![],
8986 key_prefix: Vec::new(),
8987 dirty: false,
8988 is_delta: false,
8989 last_full_lsn: NULL_LSN,
8990 last_delta_lsn: NULL_LSN,
8991 generation: 0,
8992 parent: None,
8993 expiration_in_hours: true,
8994 cursor_count: 0,
8995 prohibit_next_delta: false,
8996 lsn_rep: LsnRep::Empty,
8997 keys: KeyRep::new(),
8998 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
8999 });
9000 assert_eq!(bin_node.get_generation(), 0);
9001 bin_node.set_generation(42);
9002 assert_eq!(bin_node.get_generation(), 42);
9003
9004 let mut in_node = TreeNode::Internal(InNodeStub {
9005 node_id: 2,
9006 level: MAIN_LEVEL | 2,
9007 entries: vec![],
9008 targets: TargetRep::None,
9009 dirty: false,
9010 generation: 0,
9011 parent: None,
9012 lsn_rep: LsnRep::Empty,
9013 });
9014 in_node.set_generation(99);
9015 assert_eq!(in_node.get_generation(), 99);
9016 }
9017
9018 /// log_size() must be consistent with write_to_bytes() length.
9019 #[test]
9020 fn test_log_size_matches_bytes_len() {
9021 // BIN stub with some entries.
9022 let bin_node = TreeNode::Bottom(BinStub {
9023 node_id: 7,
9024 level: BIN_LEVEL,
9025 entries: vec![
9026 BinEntry {
9027 data: Some(b"d1".to_vec()),
9028 known_deleted: false,
9029 dirty: false,
9030 expiration_time: 0,
9031 },
9032 BinEntry {
9033 data: None,
9034 known_deleted: false,
9035 dirty: false,
9036 expiration_time: 0,
9037 },
9038 ],
9039 key_prefix: Vec::new(),
9040 dirty: true,
9041 is_delta: false,
9042 last_full_lsn: NULL_LSN,
9043 last_delta_lsn: NULL_LSN,
9044 generation: 5,
9045 parent: None,
9046 expiration_in_hours: true,
9047 cursor_count: 0,
9048 prohibit_next_delta: false,
9049 lsn_rep: LsnRep::Empty,
9050 keys: KeyRep::from_keys(vec![b"alpha".to_vec(), b"beta".to_vec()]),
9051 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9052 });
9053 assert_eq!(bin_node.log_size(), bin_node.write_to_bytes().len());
9054
9055 // IN stub with some entries.
9056 let in_node = TreeNode::Internal(InNodeStub {
9057 node_id: 8,
9058 level: MAIN_LEVEL | 2,
9059 entries: vec![
9060 InEntry { key: vec![] },
9061 InEntry { key: b"mid".to_vec() },
9062 ],
9063 targets: TargetRep::None,
9064 dirty: false,
9065 generation: 0,
9066 parent: None,
9067 lsn_rep: LsnRep::Empty,
9068 });
9069 assert_eq!(in_node.log_size(), in_node.write_to_bytes().len());
9070 }
9071
9072 /// write_to_bytes() output contains the node_id and dirty flag.
9073 #[test]
9074 fn test_write_to_bytes_encodes_node_id_and_dirty() {
9075 let node = TreeNode::Bottom(BinStub {
9076 node_id: 0xDEAD_BEEF_0000_0001,
9077 level: BIN_LEVEL,
9078 entries: vec![],
9079 key_prefix: Vec::new(),
9080 dirty: true,
9081 is_delta: false,
9082 last_full_lsn: NULL_LSN,
9083 last_delta_lsn: NULL_LSN,
9084 generation: 0,
9085 parent: None,
9086 expiration_in_hours: true,
9087 cursor_count: 0,
9088 prohibit_next_delta: false,
9089 lsn_rep: LsnRep::Empty,
9090 keys: KeyRep::new(),
9091 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9092 });
9093 let bytes = node.write_to_bytes();
9094 // First 8 bytes = node_id big-endian.
9095 let id_bytes = &bytes[0..8];
9096 assert_eq!(id_bytes, 0xDEAD_BEEF_0000_0001u64.to_be_bytes());
9097 // Byte at offset 16 (after node_id[8] + level[4] + n_entries[4]) = dirty flag.
9098 assert_eq!(bytes[16], 1u8, "dirty flag must be 1");
9099 }
9100
9101 /// log_size() grows as entries are added.
9102 #[test]
9103 fn test_log_size_grows_with_entries() {
9104 let empty = TreeNode::Bottom(BinStub {
9105 node_id: 1,
9106 level: BIN_LEVEL,
9107 entries: vec![],
9108 key_prefix: Vec::new(),
9109 dirty: false,
9110 is_delta: false,
9111 last_full_lsn: NULL_LSN,
9112 last_delta_lsn: NULL_LSN,
9113 generation: 0,
9114 parent: None,
9115 expiration_in_hours: true,
9116 cursor_count: 0,
9117 prohibit_next_delta: false,
9118 lsn_rep: LsnRep::Empty,
9119 keys: KeyRep::new(),
9120 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9121 });
9122 let with_entry = TreeNode::Bottom(BinStub {
9123 node_id: 2,
9124 level: BIN_LEVEL,
9125 entries: vec![BinEntry {
9126 data: None,
9127 known_deleted: false,
9128 dirty: false,
9129 expiration_time: 0,
9130 }],
9131 key_prefix: Vec::new(),
9132 dirty: false,
9133 is_delta: false,
9134 last_full_lsn: NULL_LSN,
9135 last_delta_lsn: NULL_LSN,
9136 generation: 0,
9137 parent: None,
9138 expiration_in_hours: true,
9139 cursor_count: 0,
9140 prohibit_next_delta: false,
9141 lsn_rep: LsnRep::Empty,
9142 keys: KeyRep::from_keys(vec![b"longkey_here".to_vec()]),
9143 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9144 });
9145 assert!(
9146 with_entry.log_size() > empty.log_size(),
9147 "log_size must grow when entries are added"
9148 );
9149 }
9150
9151 /// propagate_dirty_to_root() marks all ancestors dirty.
9152 #[test]
9153 fn test_propagate_dirty_to_root() {
9154 // Build a 2-level tree manually: root IN -> BIN.
9155 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
9156 node_id: generate_node_id(),
9157 level: BIN_LEVEL,
9158 entries: vec![],
9159 key_prefix: Vec::new(),
9160 dirty: false,
9161 is_delta: false,
9162 last_full_lsn: NULL_LSN,
9163 last_delta_lsn: NULL_LSN,
9164 generation: 0,
9165 parent: None, // set below
9166 expiration_in_hours: true,
9167 cursor_count: 0,
9168 prohibit_next_delta: false,
9169 lsn_rep: LsnRep::Empty,
9170 keys: KeyRep::new(),
9171 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9172 })));
9173
9174 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
9175 node_id: generate_node_id(),
9176 level: MAIN_LEVEL | 2,
9177 entries: vec![InEntry { key: vec![] }],
9178 targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
9179 dirty: false,
9180 generation: 0,
9181 parent: None,
9182 lsn_rep: LsnRep::Empty,
9183 })));
9184
9185 // Wire BIN's parent to root.
9186 bin_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
9187
9188 // Root is not dirty before propagation.
9189 assert!(!root_arc.read().is_dirty());
9190
9191 // Propagate from the BIN up.
9192 Tree::propagate_dirty_to_root(&bin_arc);
9193
9194 // Root must now be dirty.
9195 assert!(
9196 root_arc.read().is_dirty(),
9197 "root must be dirty after propagate_dirty_to_root"
9198 );
9199 }
9200
9201 /// collect_stats() on an empty tree returns all-zero stats.
9202 #[test]
9203 fn test_collect_stats_empty_tree() {
9204 let tree = Tree::new(1, 128);
9205 let stats = tree.collect_stats();
9206 assert_eq!(stats, TreeStats::default());
9207 }
9208
9209 /// collect_stats() on a single-entry tree: 1 IN + 1 BIN, height 2.
9210 #[test]
9211 fn test_collect_stats_single_insert() {
9212 let tree = Tree::new(1, 128);
9213 tree.insert(b"k".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
9214 let stats = tree.collect_stats();
9215 assert_eq!(stats.n_bins, 1, "must have 1 BIN");
9216 assert_eq!(stats.n_ins, 1, "must have 1 upper IN");
9217 assert_eq!(stats.height, 2, "single-entry tree has height 2");
9218 assert!(stats.n_entries >= 1, "must have at least 1 entry total");
9219 }
9220
9221 /// collect_stats() with many inserts: entry count matches insert count.
9222 #[test]
9223 fn test_collect_stats_many_inserts() {
9224 let tree = Tree::new(1, 8);
9225 let n = 50u32;
9226 for i in 0..n {
9227 let key = format!("sk{:04}", i).into_bytes();
9228 tree.insert(key, b"v".to_vec(), Lsn::new(1, i)).unwrap();
9229 }
9230 let stats = tree.collect_stats();
9231 // All n entries should be accounted for across all BINs.
9232 // n_entries counts entries in both INs and BINs; BIN entries = n.
9233 // We verify BIN entry total equals n by summing manually.
9234 let bin_entries: u64 = stats.n_entries - stats.n_ins; // rough check
9235 // A more precise assertion: the sum of all BIN entries == n.
9236 // Since we can't easily separate, just assert the tree is non-trivial.
9237 assert!(stats.n_bins > 0, "must have at least one BIN");
9238 assert!(stats.height >= 2, "multi-entry tree has height >= 2");
9239 // Total entries in the tree must be >= n (BIN entries alone).
9240 assert!(
9241 bin_entries >= n as u64 || stats.n_entries >= n as u64,
9242 "entry count must account for all inserts"
9243 );
9244 }
9245
9246 // ========================================================================
9247 // Tests: B-tree merge / compress
9248 // ========================================================================
9249
9250 /// After deleting most keys from a tree, compress() must reduce the BIN
9251 /// count by merging under-full siblings.
9252 ///
9253 /// Strategy: build a large tree (many BINs), delete almost all keys,
9254 /// then verify compress() reduces n_bins and all surviving keys remain
9255 /// findable. We do not hard-code the exact BIN counts because the
9256 /// preemptive splitting strategy determines the exact split points.
9257 #[test]
9258 fn test_compress_merges_underfull_bins() {
9259 let tree = Tree::new(1, 8);
9260
9261 // Insert 64 sorted keys to build a multi-BIN tree.
9262 let n = 64u32;
9263 let keys: Vec<Vec<u8>> =
9264 (0..n).map(|i| format!("cm{:04}", i).into_bytes()).collect();
9265 for (i, key) in keys.iter().enumerate() {
9266 tree.insert(key.clone(), vec![i as u8], Lsn::new(1, i as u32))
9267 .unwrap();
9268 }
9269
9270 let stats_full = tree.collect_stats();
9271 assert!(
9272 stats_full.n_bins >= 2,
9273 "must have multiple BINs after 64 inserts"
9274 );
9275
9276 // Delete all but 4 widely-spaced keys (one roughly per BIN pair).
9277 // We keep every 16th key: k0000, k0016, k0032, k0048.
9278 let keep: std::collections::HashSet<u32> =
9279 [0, 16, 32, 48].iter().cloned().collect();
9280 for i in 0..n {
9281 if !keep.contains(&i) {
9282 let key = format!("cm{:04}", i).into_bytes();
9283 tree.delete(&key);
9284 }
9285 }
9286
9287 let stats_sparse = tree.collect_stats();
9288 assert!(
9289 stats_sparse.n_bins >= 2,
9290 "should still have multiple BINs before compress"
9291 );
9292
9293 // compress() must reduce BIN count since most BINs now hold 0–1 entries.
9294 tree.compress();
9295
9296 let stats_after = tree.collect_stats();
9297 assert!(
9298 stats_after.n_bins < stats_sparse.n_bins,
9299 "compress must reduce BIN count (was {}, now {})",
9300 stats_sparse.n_bins,
9301 stats_after.n_bins
9302 );
9303
9304 // Surviving keys must still be findable.
9305 for i in keep {
9306 let key = format!("cm{:04}", i).into_bytes();
9307 let sr = tree.search(&key);
9308 assert!(
9309 sr.is_some() && sr.unwrap().exact_parent_found,
9310 "key cm{:04} must survive compress",
9311 i
9312 );
9313 }
9314 }
9315
9316 /// compress() preserves all entries: a full-BIN tree has fewer merges
9317 /// but all keys remain accessible.
9318 #[test]
9319 fn test_compress_no_op_when_full() {
9320 // Insert exactly max_entries worth of keys into a single BIN — no split
9321 // will have occurred yet, and the BINs will all be reasonably full.
9322 // We can't prevent splits entirely (preemptive), but we can verify that
9323 // compress() never loses entries.
9324 let tree = Tree::new(1, 8);
9325 let n = 32u32;
9326 for i in 0..n {
9327 let key = format!("fn{:04}", i).into_bytes();
9328 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
9329 }
9330
9331 let stats_before = tree.collect_stats();
9332 tree.compress();
9333 let stats_after = tree.collect_stats();
9334
9335 // All keys still findable.
9336 for i in 0..n {
9337 let key = format!("fn{:04}", i).into_bytes();
9338 let sr = tree.search(&key);
9339 assert!(
9340 sr.is_some() && sr.unwrap().exact_parent_found,
9341 "key fn{:04} must be findable after compress",
9342 i
9343 );
9344 }
9345
9346 // BIN count must not increase.
9347 assert!(
9348 stats_after.n_bins <= stats_before.n_bins,
9349 "compress must not increase BIN count"
9350 );
9351 }
9352
9353 /// compress() on an empty tree must not panic.
9354 #[test]
9355 fn test_compress_empty_tree() {
9356 let tree = Tree::new(1, 4);
9357 tree.compress(); // must not panic
9358 }
9359
9360 /// Deterministic regression for the BIN/IN split-path check-then-act race
9361 /// (`.agent/archived-audits/bench/bug-bin-split-concurrency.md`).
9362 ///
9363 /// `insert_recursive_inner` checks `child.get_n_entries() >= max_entries`
9364 /// under a PARENT READ lock, drops that read lock (required — the split
9365 /// needs `parent.write()`), then calls `split_child`. In the drop→reacquire
9366 /// window a racing thread (a second splitter, or the INCompressor merging
9367 /// and CLEARING a sibling — `compress_node`'s `lb.entries.clear()`) can
9368 /// leave the child no longer full, or even empty. Pre-fix, `split_child`
9369 /// then built a `SplitEntries` from that stale child and
9370 /// `SplitEntries::get_key(split_index)` panicked with
9371 /// "index out of bounds: len is 0" on the empty entries vec.
9372 ///
9373 /// This test drives the exact interleaving deterministically: it builds a
9374 /// level-2 tree, empties a full BIN child in place (simulating the racing
9375 /// merge), then calls `split_child` on it directly. With the fix
9376 /// `split_child` re-validates fullness under the child write lock and
9377 /// returns `Ok(())` (a benign no-op); without the fix it panics in
9378 /// `get_key`.
9379 ///
9380 /// JE-faithful: `IN.split` re-checks `needsSplitting()` after latching the
9381 /// node it will split (IN.java IN.split / IN.needsSplitting).
9382 #[test]
9383 fn split_child_is_noop_when_child_no_longer_full() {
9384 let max_entries = 8usize;
9385 let tree = Tree::new(1, max_entries);
9386
9387 // Build a level-2 tree: insert enough sorted keys to force at least one
9388 // split so the root becomes an Internal node with BIN children.
9389 for i in 0..64u32 {
9390 tree.insert(
9391 format!("k{:04}", i).into_bytes(),
9392 vec![i as u8],
9393 Lsn::new(1, i),
9394 )
9395 .unwrap();
9396 }
9397
9398 let root_arc = tree.get_root().expect("root resident");
9399
9400 // Pick child slot 0 (any resident BIN child works — the panic is about
9401 // the child being empty at split time, not about how it got there).
9402 let child_arc = {
9403 let g = root_arc.read();
9404 let TreeNode::Internal(n) = &*g else {
9405 panic!("expected a level-2 tree (root should be Internal)");
9406 };
9407 n.get_child(0).expect("resident child at slot 0")
9408 };
9409 let child_index = 0usize;
9410
9411 // Simulate the racing merge: clear the child's entries in place, the
9412 // way `compress_node` clears the merged-away left sibling. This is the
9413 // stale state a second `split_child` (or a split racing the compressor)
9414 // observes after the fullness check was already passed under the now-
9415 // dropped parent read lock.
9416 {
9417 let mut cg = child_arc.write();
9418 match &mut *cg {
9419 TreeNode::Bottom(b) => {
9420 b.entries.clear();
9421 b.lsn_rep = LsnRep::Empty;
9422 b.keys = KeyRep::new();
9423 }
9424 TreeNode::Internal(n) => {
9425 n.entries.clear();
9426 n.lsn_rep = LsnRep::Empty;
9427 n.targets = TargetRep::None;
9428 }
9429 }
9430 assert_eq!(cg.get_n_entries(), 0, "child must now be empty");
9431 }
9432
9433 // Directly call the split path. Pre-fix this panics in
9434 // `SplitEntries::get_key(0)` on the empty vec; post-fix it re-validates
9435 // fullness under the child write lock and returns Ok(()) (no-op).
9436 let res = Tree::split_child(
9437 &root_arc,
9438 child_index,
9439 max_entries,
9440 Lsn::new(1, 999),
9441 SplitHint::Normal,
9442 b"k0000",
9443 None, // no comparator
9444 false, // key_prefixing off
9445 None, // no InListListener
9446 );
9447 assert!(
9448 res.is_ok(),
9449 "split_child on an emptied (no-longer-full) child must be a benign \
9450 no-op, got {:?}",
9451 res
9452 );
9453 }
9454
9455 /// After deleting all entries, compress() reduces BINs to 1.
9456 #[test]
9457 fn test_compress_removes_empty_bin_from_parent() {
9458 let tree = Tree::new(1, 4);
9459 // Insert enough keys to generate multiple BINs.
9460 let n = 16u32;
9461 for i in 0..n {
9462 let key = format!("ep{:04}", i).into_bytes();
9463 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
9464 }
9465
9466 let stats_before = tree.collect_stats();
9467 assert!(stats_before.n_bins >= 2, "need multiple BINs for this test");
9468
9469 // Delete everything except the very last key.
9470 for i in 0..n - 1 {
9471 let key = format!("ep{:04}", i).into_bytes();
9472 tree.delete(&key);
9473 }
9474
9475 tree.compress();
9476
9477 let stats_after = tree.collect_stats();
9478 assert!(
9479 stats_after.n_bins < stats_before.n_bins,
9480 "compress must reduce BIN count after mass deletion"
9481 );
9482
9483 // The surviving key must still be findable.
9484 let last_key = format!("ep{:04}", n - 1).into_bytes();
9485 let sr = tree.search(&last_key);
9486 assert!(
9487 sr.is_some() && sr.unwrap().exact_parent_found,
9488 "last key must survive after compress"
9489 );
9490 }
9491
9492 // ========================================================================
9493 // IC-1: prune_empty_bin must NOT remove a live entry when the BIN was
9494 // repopulated between the compressor observing it empty and the prune.
9495 // (Tree corruption / lost-write regression test.)
9496 // ========================================================================
9497
9498 /// Find a BIN arc that is currently empty (0 entries) and is NOT the
9499 /// root, returning it together with the `id_key` the compressor would
9500 /// have captured (here we just use any key that routes to that BIN).
9501 fn first_empty_non_root_bin(tree: &Tree) -> Option<Arc<RwLock<TreeNode>>> {
9502 let root = tree.get_root()?;
9503 for node in tree.rebuild_in_list() {
9504 if Arc::ptr_eq(&node, &root) {
9505 continue; // skip root (single-BIN tree is never pruned)
9506 }
9507 let is_empty_bin = {
9508 let g = node.read();
9509 matches!(&*g, TreeNode::Bottom(b) if b.entries.is_empty())
9510 };
9511 if is_empty_bin {
9512 return Some(node);
9513 }
9514 }
9515 None
9516 }
9517
9518 /// IC-1 (fail-pre / pass-post): the old `compress_bin` prune step called
9519 /// `self.delete(&id_key)`, which re-descends by key. If a concurrent
9520 /// insert repopulated the empty BIN with a LIVE entry under that same
9521 /// `id_key`, `self.delete` would silently remove the live entry — a lost
9522 /// write. `prune_empty_bin` re-validates `n_entries == 0` under the
9523 /// parent latch and must REMOVE NOTHING when the BIN is non-empty.
9524 ///
9525 /// JE `Tree.delete` / `searchDeletableSubTree` (Tree.java ~line 755-800):
9526 /// `bin.getNEntries() != 0` → NODE_NOT_EMPTY (abort prune).
9527 #[test]
9528 fn test_ic1_prune_empty_bin_aborts_when_repopulated() {
9529 let tree = Tree::new(1, 4);
9530 let n = 16u32;
9531 for i in 0..n {
9532 let key = format!("ic{:04}", i).into_bytes();
9533 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
9534 }
9535 assert!(
9536 tree.collect_stats().n_bins >= 2,
9537 "need multiple BINs for this test"
9538 );
9539
9540 // Empty out one whole BIN by deleting every key it holds. We delete
9541 // the lowest 4 keys (ic0000..ic0003) which share the first BIN, then
9542 // physically compress it so it has 0 entries.
9543 for i in 0..4 {
9544 let key = format!("ic{:04}", i).into_bytes();
9545 tree.delete(&key);
9546 }
9547
9548 // Locate the now-empty BIN and the id_key the compressor would use.
9549 let empty_bin = match first_empty_non_root_bin(&tree) {
9550 Some(b) => b,
9551 // If the layout didn't leave an isolated empty BIN, the scenario
9552 // isn't reproducible on this build; treat as vacuously passing.
9553 None => return,
9554 };
9555
9556 // SIMULATE THE RACE: a concurrent insert repopulates the empty BIN
9557 // with a LIVE entry *before* the prune runs. We insert directly into
9558 // the BIN arc to model the insert that lands after `now_empty` was
9559 // read. Pick a key that routes to this BIN.
9560 let live_key = format!("ic{:04}", 1).into_bytes(); // was deleted above
9561 {
9562 let mut g = empty_bin.write();
9563 if let TreeNode::Bottom(b) = &mut *g {
9564 // T-2/T-3: route through the insert helper so entries/keys/
9565 // lsn_rep stay in lock step.
9566 b.insert_with_prefix(
9567 live_key.clone(),
9568 Lsn::new(1, 1),
9569 Some(vec![0xAB]),
9570 );
9571 }
9572 }
9573 let id_key = {
9574 let g = empty_bin.read();
9575 match &*g {
9576 TreeNode::Bottom(b) => b.get_full_key(0).unwrap(),
9577 _ => unreachable!(),
9578 }
9579 };
9580
9581 // Prune must ABORT (return false) because the BIN is no longer empty,
9582 // and must NOT remove the live entry.
9583 let pruned = tree.prune_empty_bin(&id_key);
9584 assert!(!pruned, "IC-1: prune must abort when the BIN was repopulated");
9585
9586 // The live entry must still be present in the BIN.
9587 let still_there = {
9588 let g = empty_bin.read();
9589 match &*g {
9590 TreeNode::Bottom(b) => {
9591 b.entries.iter().enumerate().any(|(i, _)| {
9592 b.key_prefix.is_empty() && b.get_key(i) == live_key
9593 })
9594 }
9595 _ => false,
9596 }
9597 };
9598 assert!(
9599 still_there,
9600 "IC-1: prune must not remove the repopulated live entry"
9601 );
9602 }
9603
9604 /// IC-1 companion: prune_empty_bin must abort when a cursor is parked on
9605 /// the (still-empty) BIN. JE: `bin.nCursors() > 0` → CURSORS_EXIST.
9606 #[test]
9607 fn test_ic1_prune_empty_bin_aborts_with_cursor() {
9608 let tree = Tree::new(1, 4);
9609 for i in 0..16u32 {
9610 let key = format!("cu{:04}", i).into_bytes();
9611 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
9612 }
9613 for i in 0..4 {
9614 let key = format!("cu{:04}", i).into_bytes();
9615 tree.delete(&key);
9616 }
9617 let empty_bin = match first_empty_non_root_bin(&tree) {
9618 Some(b) => b,
9619 None => return,
9620 };
9621 // Park a cursor on the empty BIN.
9622 Tree::pin_bin(&empty_bin);
9623 // id_key: any key routing to this BIN. Use the first deleted key.
9624 let id_key = format!("cu{:04}", 0).into_bytes();
9625 let pruned = tree.prune_empty_bin(&id_key);
9626 assert!(
9627 !pruned,
9628 "IC-1: prune must abort when a cursor is parked on the BIN"
9629 );
9630 Tree::unpin_bin(&empty_bin);
9631 }
9632
9633 /// IC-1 happy path: prune_empty_bin removes the parent slot when the BIN
9634 /// really is empty, no cursors, not a delta.
9635 #[test]
9636 fn test_ic1_prune_empty_bin_succeeds_when_truly_empty() {
9637 let tree = Tree::new(1, 4);
9638 for i in 0..16u32 {
9639 let key = format!("ok{:04}", i).into_bytes();
9640 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
9641 }
9642 for i in 0..4 {
9643 let key = format!("ok{:04}", i).into_bytes();
9644 tree.delete(&key);
9645 }
9646 let bins_before = tree.collect_stats().n_bins;
9647 let empty_bin = match first_empty_non_root_bin(&tree) {
9648 Some(b) => b,
9649 None => return,
9650 };
9651 // id_key: a key that routes to this empty BIN (one of the deleted).
9652 let id_key = {
9653 // route by the lowest deleted key; it falls into the leftmost BIN.
9654 let _ = &empty_bin;
9655 format!("ok{:04}", 0).into_bytes()
9656 };
9657 let pruned = tree.prune_empty_bin(&id_key);
9658 assert!(pruned, "IC-1: prune must succeed on a truly empty BIN");
9659 let bins_after = tree.collect_stats().n_bins;
9660 assert!(
9661 bins_after < bins_before,
9662 "IC-1: pruned BIN slot must be removed from the parent (was {}, now {})",
9663 bins_before,
9664 bins_after
9665 );
9666 // Every surviving key must still be findable.
9667 for i in 4..16u32 {
9668 let key = format!("ok{:04}", i).into_bytes();
9669 assert!(
9670 tree.search(&key).is_some_and(|s| s.exact_parent_found),
9671 "surviving key ok{:04} must remain after prune",
9672 i
9673 );
9674 }
9675 }
9676
9677 // ========================================================================
9678 // Tests: latch-coupling validation (validate_parent_child /
9679 // search_with_coupling)
9680 // ========================================================================
9681
9682 /// validate_parent_child returns true when the parent slot points at the
9683 /// expected child.
9684 #[test]
9685 fn test_validate_parent_child_correct_link() {
9686 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
9687 node_id: generate_node_id(),
9688 level: BIN_LEVEL,
9689 entries: vec![],
9690 key_prefix: Vec::new(),
9691 dirty: false,
9692 is_delta: false,
9693 last_full_lsn: NULL_LSN,
9694 last_delta_lsn: NULL_LSN,
9695 generation: 0,
9696 parent: None,
9697 expiration_in_hours: true,
9698 cursor_count: 0,
9699 prohibit_next_delta: false,
9700 lsn_rep: LsnRep::Empty,
9701 keys: KeyRep::new(),
9702 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9703 })));
9704
9705 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
9706 node_id: generate_node_id(),
9707 level: MAIN_LEVEL | 2,
9708 entries: vec![InEntry { key: vec![] }],
9709 targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
9710 dirty: false,
9711 generation: 0,
9712 parent: None,
9713 lsn_rep: LsnRep::Empty,
9714 })));
9715
9716 assert!(
9717 Tree::validate_parent_child(&root_arc, 0, &bin_arc),
9718 "link must be valid when parent slot 0 points at bin_arc"
9719 );
9720 }
9721
9722 /// validate_parent_child returns false when the slot index is out of range.
9723 #[test]
9724 fn test_validate_parent_child_out_of_range() {
9725 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
9726 node_id: generate_node_id(),
9727 level: MAIN_LEVEL | 2,
9728 entries: vec![],
9729 targets: TargetRep::None,
9730 dirty: false,
9731 generation: 0,
9732 parent: None,
9733 lsn_rep: LsnRep::Empty,
9734 })));
9735 let other_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
9736 node_id: generate_node_id(),
9737 level: BIN_LEVEL,
9738 entries: vec![],
9739 key_prefix: Vec::new(),
9740 dirty: false,
9741 is_delta: false,
9742 last_full_lsn: NULL_LSN,
9743 last_delta_lsn: NULL_LSN,
9744 generation: 0,
9745 parent: None,
9746 expiration_in_hours: true,
9747 cursor_count: 0,
9748 prohibit_next_delta: false,
9749 lsn_rep: LsnRep::Empty,
9750 keys: KeyRep::new(),
9751 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9752 })));
9753
9754 assert!(
9755 !Tree::validate_parent_child(&root_arc, 0, &other_arc),
9756 "link must be invalid when parent has no entries"
9757 );
9758 }
9759
9760 /// validate_parent_child returns false when the slot points at a different Arc.
9761 #[test]
9762 fn test_validate_parent_child_wrong_child() {
9763 let bin_a = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
9764 node_id: generate_node_id(),
9765 level: BIN_LEVEL,
9766 entries: vec![],
9767 key_prefix: Vec::new(),
9768 dirty: false,
9769 is_delta: false,
9770 last_full_lsn: NULL_LSN,
9771 last_delta_lsn: NULL_LSN,
9772 generation: 0,
9773 parent: None,
9774 expiration_in_hours: true,
9775 cursor_count: 0,
9776 prohibit_next_delta: false,
9777 lsn_rep: LsnRep::Empty,
9778 keys: KeyRep::new(),
9779 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9780 })));
9781 let bin_b = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
9782 node_id: generate_node_id(),
9783 level: BIN_LEVEL,
9784 entries: vec![],
9785 key_prefix: Vec::new(),
9786 dirty: false,
9787 is_delta: false,
9788 last_full_lsn: NULL_LSN,
9789 last_delta_lsn: NULL_LSN,
9790 generation: 0,
9791 parent: None,
9792 expiration_in_hours: true,
9793 cursor_count: 0,
9794 prohibit_next_delta: false,
9795 lsn_rep: LsnRep::Empty,
9796 keys: KeyRep::new(),
9797 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9798 })));
9799
9800 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
9801 node_id: generate_node_id(),
9802 level: MAIN_LEVEL | 2,
9803 entries: vec![InEntry { key: vec![] }],
9804 targets: TargetRep::Sparse(vec![(0, bin_a)]),
9805 dirty: false,
9806 generation: 0,
9807 parent: None,
9808 lsn_rep: LsnRep::Empty,
9809 })));
9810
9811 assert!(
9812 !Tree::validate_parent_child(&root_arc, 0, &bin_b),
9813 "link must be invalid when parent slot points at a different Arc"
9814 );
9815 }
9816
9817 /// search_with_coupling finds the same key as search().
9818 #[test]
9819 fn test_search_with_coupling_finds_existing_key() {
9820 let tree = Tree::new(1, 8);
9821 for i in 0u32..20 {
9822 let key = format!("c{:04}", i).into_bytes();
9823 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
9824 }
9825
9826 for i in 0u32..20 {
9827 let key = format!("c{:04}", i).into_bytes();
9828 let sr = tree.search_with_coupling(&key);
9829 assert!(
9830 sr.is_some() && sr.unwrap().exact_parent_found,
9831 "search_with_coupling must find c{:04}",
9832 i
9833 );
9834 }
9835 }
9836
9837 /// search_with_coupling returns false for a key not in the tree.
9838 #[test]
9839 fn test_search_with_coupling_missing_key() {
9840 let tree = Tree::new(1, 8);
9841 tree.insert(b"hello".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
9842
9843 let sr = tree.search_with_coupling(b"zzz");
9844 // The search result must either be None or have exact_parent_found=false.
9845 assert!(
9846 sr.is_none_or(|r| !r.exact_parent_found),
9847 "search_with_coupling must not find a key that was never inserted"
9848 );
9849 }
9850
9851 /// search_with_coupling on an empty tree returns None.
9852 #[test]
9853 fn test_search_with_coupling_empty_tree() {
9854 let tree = Tree::new(1, 8);
9855 assert!(tree.search_with_coupling(b"k").is_none());
9856 }
9857
9858 // ========================================================================
9859 // Tests: BIN-delta reconstitution (apply_delta_to_bin / mutate_to_full_bin)
9860 // ========================================================================
9861
9862 /// apply_delta_to_bin replaces existing entries and inserts new ones.
9863 ///
9864 /// BIN.applyDelta(): delta entries are authoritative and
9865 /// supersede full-BIN entries at the same key.
9866 #[test]
9867 fn test_apply_delta_to_bin_updates_and_inserts() {
9868 let mut base = BinStub {
9869 node_id: 1,
9870 level: BIN_LEVEL,
9871 entries: vec![
9872 BinEntry {
9873 data: Some(b"old_a".to_vec()),
9874 known_deleted: false,
9875 dirty: false,
9876 expiration_time: 0,
9877 },
9878 BinEntry {
9879 data: Some(b"old_c".to_vec()),
9880 known_deleted: false,
9881 dirty: false,
9882 expiration_time: 0,
9883 },
9884 ],
9885 key_prefix: Vec::new(),
9886 dirty: false,
9887 is_delta: false,
9888 last_full_lsn: NULL_LSN,
9889 last_delta_lsn: NULL_LSN,
9890 generation: 0,
9891 parent: None,
9892 expiration_in_hours: true,
9893 cursor_count: 0,
9894 prohibit_next_delta: false,
9895 lsn_rep: LsnRep::Empty,
9896 keys: KeyRep::from_keys(vec![b"a".to_vec(), b"c".to_vec()]),
9897 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9898 };
9899
9900 let delta_entries = vec![
9901 // Update existing key "a" with new data.
9902 (b"a".to_vec(), Lsn::new(1, 10), Some(b"new_a".to_vec())),
9903 // Insert new key "b".
9904 (b"b".to_vec(), Lsn::new(1, 20), Some(b"new_b".to_vec())),
9905 ];
9906
9907 Tree::apply_delta_to_bin(&mut base, delta_entries);
9908
9909 assert!(base.dirty, "base must be dirty after applying delta");
9910
9911 // Collect the full keys for assertions (T-2: keys live in the rep).
9912 let full_keys: Vec<Vec<u8>> = (0..base.entries.len())
9913 .map(|i| base.get_full_key(i).unwrap_or_default())
9914 .collect();
9915
9916 // "a" must be updated.
9917 let a_idx = full_keys.iter().position(|k| k == b"a").unwrap();
9918 assert_eq!(
9919 base.entries[a_idx].data.as_deref(),
9920 Some(b"new_a" as &[u8])
9921 );
9922
9923 // "b" must be newly inserted.
9924 assert!(full_keys.iter().any(|k| k == b"b"));
9925
9926 // "c" must still be present (untouched).
9927 assert!(full_keys.iter().any(|k| k == b"c"));
9928
9929 // Entries must be in sorted order.
9930 let mut sorted = full_keys.clone();
9931 sorted.sort();
9932 assert_eq!(
9933 full_keys, sorted,
9934 "entries must remain sorted after delta apply"
9935 );
9936 }
9937
9938 /// apply_delta_to_bin with an empty delta is a no-op (except dirty flag).
9939 #[test]
9940 fn test_apply_delta_to_bin_empty_delta() {
9941 let mut base = BinStub {
9942 node_id: 1,
9943 level: BIN_LEVEL,
9944 entries: vec![BinEntry {
9945 data: None,
9946 known_deleted: false,
9947 dirty: false,
9948 expiration_time: 0,
9949 }],
9950 key_prefix: Vec::new(),
9951 dirty: false,
9952 is_delta: false,
9953 last_full_lsn: NULL_LSN,
9954 last_delta_lsn: NULL_LSN,
9955 generation: 0,
9956 parent: None,
9957 expiration_in_hours: true,
9958 cursor_count: 0,
9959 prohibit_next_delta: false,
9960 lsn_rep: LsnRep::Empty,
9961 keys: KeyRep::from_keys(vec![b"x".to_vec()]),
9962 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9963 };
9964 let n_before = base.entries.len();
9965 Tree::apply_delta_to_bin(&mut base, vec![]);
9966 assert_eq!(
9967 base.entries.len(),
9968 n_before,
9969 "empty delta must not change entry count"
9970 );
9971 assert!(base.dirty, "dirty must be set even for empty delta apply");
9972 }
9973
9974 /// mutate_to_full_bin reconstitutes a full BIN from a delta + base.
9975 ///
9976 /// BIN.mutateToFullBIN(BIN fullBIN): after mutation the
9977 /// `is_delta` flag must be cleared and the entries must contain both
9978 /// base and delta data.
9979 #[test]
9980 fn test_mutate_to_full_bin_merges_delta_and_base() {
9981 let base = BinStub {
9982 node_id: 2,
9983 level: BIN_LEVEL,
9984 entries: vec![
9985 BinEntry {
9986 data: Some(b"base_aa".to_vec()),
9987 known_deleted: false,
9988 dirty: false,
9989 expiration_time: 0,
9990 },
9991 BinEntry {
9992 data: Some(b"base_cc".to_vec()),
9993 known_deleted: false,
9994 dirty: false,
9995 expiration_time: 0,
9996 },
9997 ],
9998 key_prefix: Vec::new(),
9999 dirty: false,
10000 is_delta: false,
10001 last_full_lsn: NULL_LSN,
10002 last_delta_lsn: NULL_LSN,
10003 generation: 0,
10004 parent: None,
10005 expiration_in_hours: true,
10006 cursor_count: 0,
10007 prohibit_next_delta: false,
10008 lsn_rep: LsnRep::Empty,
10009 keys: KeyRep::from_keys(vec![b"aa".to_vec(), b"cc".to_vec()]),
10010 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10011 };
10012
10013 // The delta has a new entry "bb" and overwrites "aa".
10014 let mut delta = BinStub {
10015 node_id: 2,
10016 level: BIN_LEVEL,
10017 entries: vec![
10018 BinEntry {
10019 data: Some(b"delta_aa".to_vec()),
10020 known_deleted: false,
10021 dirty: false,
10022 expiration_time: 0,
10023 },
10024 BinEntry {
10025 data: Some(b"delta_bb".to_vec()),
10026 known_deleted: false,
10027 dirty: false,
10028 expiration_time: 0,
10029 },
10030 ],
10031 key_prefix: Vec::new(),
10032 dirty: true,
10033 is_delta: true,
10034 last_full_lsn: NULL_LSN,
10035 last_delta_lsn: NULL_LSN,
10036 generation: 0,
10037 parent: None,
10038 expiration_in_hours: true,
10039 cursor_count: 0,
10040 prohibit_next_delta: false,
10041 lsn_rep: LsnRep::Empty,
10042 keys: KeyRep::from_keys(vec![b"aa".to_vec(), b"bb".to_vec()]),
10043 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10044 };
10045
10046 Tree::mutate_to_full_bin(&mut delta, base);
10047
10048 // After mutation the node must be a full BIN.
10049 assert!(
10050 !delta.is_delta,
10051 "is_delta must be false after mutate_to_full_bin"
10052 );
10053 assert!(delta.dirty, "must be dirty after mutation");
10054
10055 // Collect full keys for assertions (T-2: keys live in the rep).
10056 let dk: Vec<Vec<u8>> = (0..delta.entries.len())
10057 .map(|i| delta.get_full_key(i).unwrap_or_default())
10058 .collect();
10059
10060 // "aa" must be the delta version.
10061 let aa_idx = dk.iter().position(|k| k == b"aa").unwrap();
10062 assert_eq!(
10063 delta.entries[aa_idx].data.as_deref(),
10064 Some(b"delta_aa" as &[u8])
10065 );
10066
10067 // "bb" must be present (from delta).
10068 assert!(dk.iter().any(|k| k == b"bb"));
10069
10070 // "cc" must be present (from base).
10071 assert!(dk.iter().any(|k| k == b"cc"));
10072
10073 // Three entries total, in sorted order.
10074 assert_eq!(delta.entries.len(), 3);
10075 let mut sorted = dk.clone();
10076 sorted.sort();
10077 assert_eq!(dk, sorted, "entries must be sorted after mutation");
10078 }
10079
10080 /// is_delta flag is correctly reported by bin_is_delta().
10081 #[test]
10082 fn test_bin_is_delta_flag() {
10083 let mut bin = BinStub {
10084 node_id: 1,
10085 level: BIN_LEVEL,
10086 entries: vec![],
10087 key_prefix: Vec::new(),
10088 dirty: false,
10089 is_delta: false,
10090 last_full_lsn: NULL_LSN,
10091 last_delta_lsn: NULL_LSN,
10092 generation: 0,
10093 parent: None,
10094 expiration_in_hours: true,
10095 cursor_count: 0,
10096 prohibit_next_delta: false,
10097 lsn_rep: LsnRep::Empty,
10098 keys: KeyRep::new(),
10099 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10100 };
10101 assert!(!Tree::bin_is_delta(&bin));
10102 bin.is_delta = true;
10103 assert!(Tree::bin_is_delta(&bin));
10104 }
10105
10106 // ========================================================================
10107 // Tests: mutate_to_full_bin_from_log
10108 // ========================================================================
10109
10110 /// mutate_to_full_bin_from_log is a no-op when the BIN is already full.
10111 #[test]
10112 fn test_mutate_to_full_bin_from_log_already_full() {
10113 let dir = tempfile::tempdir().unwrap();
10114 let fm = std::sync::Arc::new(
10115 noxu_log::FileManager::new(dir.path(), false, 10_000_000, 100)
10116 .unwrap(),
10117 );
10118 let lm = noxu_log::LogManager::new(fm, 3, 1024 * 1024, 4096);
10119
10120 let mut bin = BinStub {
10121 node_id: 1,
10122 level: BIN_LEVEL,
10123 entries: vec![BinEntry {
10124 data: Some(b"v1".to_vec()),
10125 known_deleted: false,
10126 dirty: false,
10127 expiration_time: 0,
10128 }],
10129 key_prefix: Vec::new(),
10130 dirty: false,
10131 is_delta: false, // already a full BIN
10132 last_full_lsn: NULL_LSN,
10133 last_delta_lsn: NULL_LSN,
10134 generation: 0,
10135 parent: None,
10136 expiration_in_hours: true,
10137 cursor_count: 0,
10138 prohibit_next_delta: false,
10139 lsn_rep: LsnRep::Empty,
10140 keys: KeyRep::from_keys(vec![b"key1".to_vec()]),
10141 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10142 };
10143
10144 Tree::mutate_to_full_bin_from_log(&mut bin, &lm);
10145
10146 // No-op: is_delta was already false, entries unchanged.
10147 assert!(!bin.is_delta);
10148 assert_eq!(bin.entries.len(), 1);
10149 }
10150
10151 /// mutate_to_full_bin_from_log with NULL_LSN promotes delta without base.
10152 ///
10153 /// When last_full_lsn is NULL_LSN the BIN has never been written as a full
10154 /// entry. The function must clear is_delta and leave the delta entries
10155 /// as-is (they are the authoritative full state).
10156 #[test]
10157 fn test_mutate_to_full_bin_from_log_null_lsn() {
10158 let dir = tempfile::tempdir().unwrap();
10159 let fm = std::sync::Arc::new(
10160 noxu_log::FileManager::new(dir.path(), false, 10_000_000, 100)
10161 .unwrap(),
10162 );
10163 let lm = noxu_log::LogManager::new(fm, 3, 1024 * 1024, 4096);
10164
10165 let mut delta = BinStub {
10166 node_id: 2,
10167 level: BIN_LEVEL,
10168 entries: vec![BinEntry {
10169 data: Some(b"delta_a".to_vec()),
10170 known_deleted: false,
10171 dirty: true,
10172 expiration_time: 0,
10173 }],
10174 key_prefix: Vec::new(),
10175 dirty: true,
10176 is_delta: true,
10177 last_full_lsn: NULL_LSN, // no full BIN ever written
10178 last_delta_lsn: NULL_LSN,
10179 generation: 0,
10180 parent: None,
10181 expiration_in_hours: true,
10182 cursor_count: 0,
10183 prohibit_next_delta: false,
10184 lsn_rep: LsnRep::Empty,
10185 keys: KeyRep::from_keys(vec![b"a".to_vec()]),
10186 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10187 };
10188
10189 Tree::mutate_to_full_bin_from_log(&mut delta, &lm);
10190
10191 // is_delta must be cleared; the single delta entry is kept as-is.
10192 assert!(
10193 !delta.is_delta,
10194 "is_delta must be false after null-lsn promotion"
10195 );
10196 assert_eq!(delta.entries.len(), 1);
10197 assert_eq!(delta.entries[0].data.as_deref(), Some(b"delta_a" as &[u8]));
10198 }
10199
10200 /// mutate_to_full_bin_from_log reads full BIN from log and merges delta.
10201 ///
10202 /// Round-trip: serialize a full BIN, write it to a LogManager, record the
10203 /// LSN, then call mutate_to_full_bin_from_log on a delta referencing that
10204 /// LSN. The result must contain base-only and delta-only entries with the
10205 /// delta winning on conflicts.
10206 #[test]
10207 fn test_mutate_to_full_bin_from_log_reads_and_merges() {
10208 let dir = tempfile::tempdir().unwrap();
10209 let fm = std::sync::Arc::new(
10210 noxu_log::FileManager::new(dir.path(), false, 10_000_000, 100)
10211 .unwrap(),
10212 );
10213 let lm = noxu_log::LogManager::new(fm, 3, 1024 * 1024, 4096);
10214
10215 // Build and serialize the full BIN that will be written to the log.
10216 let full_bin = BinStub {
10217 node_id: 42,
10218 level: BIN_LEVEL,
10219 entries: vec![
10220 BinEntry {
10221 data: Some(b"base_val".to_vec()),
10222 known_deleted: false,
10223 dirty: false,
10224 expiration_time: 0,
10225 },
10226 BinEntry {
10227 data: Some(b"base_shared".to_vec()),
10228 known_deleted: false,
10229 dirty: false,
10230 expiration_time: 0,
10231 },
10232 ],
10233 key_prefix: Vec::new(),
10234 dirty: false,
10235 is_delta: false,
10236 last_full_lsn: NULL_LSN,
10237 last_delta_lsn: NULL_LSN,
10238 generation: 0,
10239 parent: None,
10240 expiration_in_hours: true,
10241 cursor_count: 0,
10242 prohibit_next_delta: false,
10243 lsn_rep: LsnRep::Empty,
10244 keys: KeyRep::from_keys(vec![
10245 b"base_only".to_vec(),
10246 b"shared_key".to_vec(),
10247 ]),
10248 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10249 };
10250
10251 let payload = full_bin.serialize_full();
10252 let full_lsn = lm
10253 .log(
10254 noxu_log::LogEntryType::BIN,
10255 &payload,
10256 noxu_log::Provisional::No,
10257 true,
10258 false,
10259 )
10260 .expect("write full BIN to log");
10261 lm.flush_no_sync().expect("flush log");
10262
10263 // Build a delta BIN referencing the full BIN via last_full_lsn.
10264 let mut delta = BinStub {
10265 node_id: 42,
10266 level: BIN_LEVEL,
10267 entries: vec![
10268 // Overwrites "shared_key" from the base.
10269 BinEntry {
10270 data: Some(b"delta_shared".to_vec()),
10271 known_deleted: false,
10272 dirty: true,
10273 expiration_time: 0,
10274 },
10275 // New key only in the delta.
10276 BinEntry {
10277 data: Some(b"delta_val".to_vec()),
10278 known_deleted: false,
10279 dirty: true,
10280 expiration_time: 0,
10281 },
10282 ],
10283 key_prefix: Vec::new(),
10284 dirty: true,
10285 is_delta: true,
10286 last_full_lsn: full_lsn,
10287 last_delta_lsn: NULL_LSN,
10288 generation: 0,
10289 parent: None,
10290 expiration_in_hours: true,
10291 cursor_count: 0,
10292 prohibit_next_delta: false,
10293 lsn_rep: LsnRep::Empty,
10294 keys: KeyRep::from_keys(vec![
10295 b"shared_key".to_vec(),
10296 b"delta_only".to_vec(),
10297 ]),
10298 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10299 };
10300
10301 Tree::mutate_to_full_bin_from_log(&mut delta, &lm);
10302
10303 assert!(
10304 !delta.is_delta,
10305 "is_delta must be false after log-based mutation"
10306 );
10307 assert!(delta.dirty, "must be dirty after mutation");
10308
10309 // All three distinct keys must be present.
10310 let find = |k: &[u8]| -> Option<Vec<u8>> {
10311 (0..delta.entries.len())
10312 .find(|&i| delta.get_full_key(i).as_deref() == Some(k))
10313 .and_then(|i| delta.entries[i].data.clone())
10314 };
10315
10316 assert_eq!(
10317 find(b"base_only"),
10318 Some(b"base_val".to_vec()),
10319 "base-only key must be present"
10320 );
10321 assert_eq!(
10322 find(b"shared_key"),
10323 Some(b"delta_shared".to_vec()),
10324 "delta must win on shared_key"
10325 );
10326 assert_eq!(
10327 find(b"delta_only"),
10328 Some(b"delta_val".to_vec()),
10329 "delta-only key must be present"
10330 );
10331 assert_eq!(delta.entries.len(), 3, "must have exactly 3 entries");
10332
10333 // Entries must be in sorted order (by full key).
10334 let full_keys: Vec<Vec<u8>> = (0..delta.entries.len())
10335 .map(|i| delta.get_full_key(i).unwrap())
10336 .collect();
10337 let mut sorted_keys = full_keys.clone();
10338 sorted_keys.sort();
10339 assert_eq!(full_keys, sorted_keys, "entries must be in sorted order");
10340 }
10341
10342 // ========================================================================
10343 // Tests: deserialize_full key prefix recomputation
10344 // ========================================================================
10345
10346 /// deserialize_full recomputes key prefix from loaded full keys.
10347 ///
10348 /// IN.recalcKeyPrefix() called after materializing from log:
10349 /// a BIN loaded from the log should have prefix compression applied so
10350 /// that search performance matches an in-memory BIN.
10351 #[test]
10352 fn test_deserialize_full_recomputes_key_prefix() {
10353 // Build a BIN with a known common prefix and serialize it.
10354 let mut source = BinStub {
10355 node_id: 99,
10356 level: BIN_LEVEL,
10357 entries: vec![
10358 BinEntry {
10359 data: None,
10360 known_deleted: false,
10361 dirty: false,
10362 expiration_time: 0,
10363 },
10364 BinEntry {
10365 data: None,
10366 known_deleted: false,
10367 dirty: false,
10368 expiration_time: 0,
10369 },
10370 BinEntry {
10371 data: None,
10372 known_deleted: false,
10373 dirty: false,
10374 expiration_time: 0,
10375 },
10376 ],
10377 key_prefix: Vec::new(),
10378 dirty: false,
10379 is_delta: false,
10380 last_full_lsn: NULL_LSN,
10381 last_delta_lsn: NULL_LSN,
10382 generation: 0,
10383 parent: None,
10384 expiration_in_hours: true,
10385 cursor_count: 0,
10386 prohibit_next_delta: false,
10387 lsn_rep: LsnRep::Empty,
10388 keys: KeyRep::from_keys(vec![
10389 b"pfx:alpha".to_vec(),
10390 b"pfx:beta".to_vec(),
10391 b"pfx:gamma".to_vec(),
10392 ]),
10393 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10394 };
10395 source.recompute_key_prefix();
10396 // Verify the source has the expected prefix before serializing.
10397 assert_eq!(source.key_prefix, b"pfx:");
10398
10399 let payload = source.serialize_full();
10400
10401 // Deserialize and verify prefix is re-established.
10402 let loaded = BinStub::deserialize_full(&payload)
10403 .expect("deserialization must succeed");
10404
10405 assert_eq!(
10406 loaded.key_prefix, b"pfx:",
10407 "key prefix must be recomputed after deserialize_full"
10408 );
10409
10410 // All full keys must be reconstructable.
10411 for i in 0..loaded.entries.len() {
10412 let fk = loaded.get_full_key(i).unwrap();
10413 assert!(
10414 fk.starts_with(b"pfx:"),
10415 "full key {i} must start with prefix"
10416 );
10417 }
10418 }
10419
10420 /// deserialize_full with a single entry leaves key_prefix empty.
10421 ///
10422 /// A BIN with fewer than 2 entries cannot have a meaningful common prefix.
10423 #[test]
10424 fn test_deserialize_full_single_entry_no_prefix() {
10425 let source = BinStub {
10426 node_id: 7,
10427 level: BIN_LEVEL,
10428 entries: vec![BinEntry {
10429 data: None,
10430 known_deleted: false,
10431 dirty: false,
10432 expiration_time: 0,
10433 }],
10434 key_prefix: Vec::new(),
10435 dirty: false,
10436 is_delta: false,
10437 last_full_lsn: NULL_LSN,
10438 last_delta_lsn: NULL_LSN,
10439 generation: 0,
10440 parent: None,
10441 expiration_in_hours: true,
10442 cursor_count: 0,
10443 prohibit_next_delta: false,
10444 lsn_rep: LsnRep::Empty,
10445 keys: KeyRep::from_keys(vec![b"solo".to_vec()]),
10446 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10447 };
10448
10449 let payload = source.serialize_full();
10450 let loaded = BinStub::deserialize_full(&payload)
10451 .expect("deserialization must succeed");
10452
10453 assert!(
10454 loaded.key_prefix.is_empty(),
10455 "single-entry BIN must have empty prefix"
10456 );
10457 assert_eq!(loaded.get_full_key(0).unwrap(), b"solo");
10458 }
10459
10460 // ========================================================================
10461 // Tests: get_next_bin / get_prev_bin
10462 // ========================================================================
10463
10464 /// get_next_bin returns the entries of the next BIN to the right.
10465 ///
10466 /// Tree.getNextBin() / getNextIN(forward=true).
10467 #[test]
10468 fn test_get_next_bin_basic() {
10469 let tree = Tree::new(1, 4);
10470
10471 // Insert 8 sorted keys — creates multiple BINs.
10472 for i in 0u32..8 {
10473 let key = format!("n{:04}", i).into_bytes();
10474 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10475 }
10476
10477 let stats = tree.collect_stats();
10478 if stats.n_bins < 2 {
10479 // If the tree only has one BIN, skip the sibling test.
10480 return;
10481 }
10482
10483 // A key from the first BIN (e.g. "n0000") should have a next BIN.
10484 let next = tree.get_next_bin(b"n0000");
10485 assert!(
10486 next.is_some(),
10487 "must return a next BIN for a key in the leftmost BIN"
10488 );
10489
10490 let entries = next.unwrap();
10491 assert!(!entries.is_empty(), "next BIN must not be empty");
10492 // All returned keys must be strictly greater than "n0000" because they
10493 // are in a different (rightward) BIN.
10494 for (_, _, k) in &entries {
10495 assert!(
10496 k.as_slice() > b"n0000" as &[u8],
10497 "next BIN entries must all be > the search key"
10498 );
10499 }
10500 }
10501
10502 /// get_next_bin returns None for a key in the rightmost BIN.
10503 #[test]
10504 fn test_get_next_bin_at_rightmost_returns_none() {
10505 let tree = Tree::new(1, 4);
10506 for i in 0u32..8 {
10507 let key = format!("r{:04}", i).into_bytes();
10508 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10509 }
10510 // A key from the rightmost BIN (e.g. "r0007") has no next BIN.
10511 let next = tree.get_next_bin(b"r0007");
10512 assert!(
10513 next.is_none(),
10514 "must return None for a key in the rightmost BIN"
10515 );
10516 }
10517
10518 /// get_prev_bin returns the entries of the next BIN to the left.
10519 ///
10520 /// Tree.getPrevBin() / getNextIN(forward=false).
10521 #[test]
10522 fn test_get_prev_bin_basic() {
10523 let tree = Tree::new(1, 4);
10524 for i in 0u32..8 {
10525 let key = format!("p{:04}", i).into_bytes();
10526 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10527 }
10528
10529 // A key from the second BIN ("p0004") should have a previous BIN.
10530 let prev = tree.get_prev_bin(b"p0004");
10531 assert!(
10532 prev.is_some(),
10533 "must return a prev BIN for a key in the second BIN"
10534 );
10535
10536 let entries = prev.unwrap();
10537 assert!(!entries.is_empty(), "prev BIN must not be empty");
10538 // All returned keys must be < b"p0004".
10539 for (_, _, k) in &entries {
10540 assert!(
10541 k.as_slice() < b"p0004" as &[u8],
10542 "prev BIN entries must all be < the current BIN"
10543 );
10544 }
10545 }
10546
10547 /// get_prev_bin returns None for a key in the leftmost BIN.
10548 #[test]
10549 fn test_get_prev_bin_at_leftmost_returns_none() {
10550 let tree = Tree::new(1, 4);
10551 for i in 0u32..8 {
10552 let key = format!("q{:04}", i).into_bytes();
10553 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10554 }
10555 // A key from the leftmost BIN ("q0000") has no prev BIN.
10556 let prev = tree.get_prev_bin(b"q0000");
10557 assert!(
10558 prev.is_none(),
10559 "must return None for a key in the leftmost BIN"
10560 );
10561 }
10562
10563 /// get_next_bin and get_prev_bin are inverse operations across the
10564 /// BIN boundary.
10565 #[test]
10566 fn test_next_prev_bin_are_symmetric() {
10567 let tree = Tree::new(1, 4);
10568 for i in 0u32..8 {
10569 let key = format!("s{:04}", i).into_bytes();
10570 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10571 }
10572
10573 // From first BIN (s0000): next → second BIN entries.
10574 let next_from_first = tree.get_next_bin(b"s0000").unwrap();
10575 // The smallest key of the next BIN.
10576 let next_first_key =
10577 next_from_first.iter().map(|(_, _, k)| k.clone()).min().unwrap();
10578
10579 // From that key in the second BIN: prev → should overlap with first BIN.
10580 let prev_from_second = tree.get_prev_bin(&next_first_key).unwrap();
10581 let prev_first_key =
10582 prev_from_second.iter().map(|(_, _, k)| k.clone()).max().unwrap();
10583
10584 // The max key of the "prev" result must be in the first BIN (< next boundary).
10585 assert!(
10586 prev_first_key < next_first_key,
10587 "prev BIN entries must be smaller than the boundary key"
10588 );
10589 }
10590
10591 /// get_next_bin on an empty tree returns None.
10592 #[test]
10593 fn test_get_next_bin_empty_tree() {
10594 let tree = Tree::new(1, 8);
10595 assert!(tree.get_next_bin(b"any").is_none());
10596 }
10597
10598 /// get_prev_bin on an empty tree returns None.
10599 #[test]
10600 fn test_get_prev_bin_empty_tree() {
10601 let tree = Tree::new(1, 8);
10602 assert!(tree.get_prev_bin(b"any").is_none());
10603 }
10604
10605 // =========================================================================
10606 // R3 fix: get_next_bin / get_prev_bin honour the custom comparator
10607 // =========================================================================
10608
10609 /// R3 regression test: with a custom comparator that reverses byte order
10610 /// (descending), `get_next_bin` and `get_prev_bin` must use comparator
10611 /// order when routing through internal nodes.
10612 ///
10613 /// Pre-fix: the static `get_adjacent_bin_attempt` used raw `<=` byte order
10614 /// for IN routing, causing it to descend to the wrong child when comparator
10615 /// order ≠ byte order.
10616 ///
10617 /// The tree is forced to split (max_entries = 4) so there IS an internal
10618 /// node (IN) to route through. Under a reverse comparator the insertion
10619 /// order and stored key order are reversed relative to byte order, so any
10620 /// descent that uses raw byte comparison will pick the wrong slot.
10621 ///
10622 /// Pass-post invariant: iterating forward via repeated `get_next_bin` from
10623 /// the leftmost BIN yields keys in COMPARATOR order (descending byte order
10624 /// here), not in raw ascending byte order.
10625 #[test]
10626 fn test_get_next_prev_bin_custom_comparator_order() {
10627 // Reverse-order comparator: larger bytes sort first.
10628 let reverse_cmp: KeyComparatorFn =
10629 Arc::new(|a: &[u8], b: &[u8]| b.cmp(a));
10630 // Small max_entries so the tree splits and has internal nodes.
10631 let mut tree = Tree::new(1, 4);
10632 tree.set_comparator(reverse_cmp);
10633
10634 // Insert keys that are ascending in byte order ("a" < "b" < … < "i")
10635 // but descending in comparator order (i > h > … > a).
10636 let keys: &[&[u8]] =
10637 &[b"a", b"b", b"c", b"d", b"e", b"f", b"g", b"h", b"i"];
10638 for (i, k) in keys.iter().enumerate() {
10639 tree.insert(
10640 k.to_vec(),
10641 vec![i as u8],
10642 Lsn::from_u64((i + 1) as u64),
10643 )
10644 .unwrap();
10645 }
10646
10647 // Collect all BINs by walking from the comparator-smallest key ("i"
10648 // in reverse order) using get_next_bin. The anchor must be a key that
10649 // is smaller than everything in comparator order, i.e. the largest
10650 // byte-value key. We use the tree's search to find the actual leftmost
10651 // key under the comparator by starting from "i" (comparator-min).
10652 //
10653 // Strategy: start at byte key b"\xff" (larger than any inserted key in
10654 // byte order, so it lands in the last BIN in byte order, which under
10655 // a reverse comparator is the leftmost BIN in comparator order). Then
10656 // walk via get_next_bin.
10657 let start_anchor = b"\xff".as_ref();
10658 let mut bin_first_keys: Vec<Vec<u8>> = Vec::new();
10659
10660 // The first BIN in comparator order contains "i" (largest byte key).
10661 // get_next_bin from a virtual start in that BIN gives the next one.
10662 // Collect by walking from the comparator-last key leftward instead:
10663 // use get_next_bin with anchor = b"\xff" to hop to the next BIN
10664 // (comparator order: next = smaller byte value).
10665 let mut anchor = start_anchor.to_vec();
10666 loop {
10667 match tree.get_next_bin(&anchor) {
10668 None => break,
10669 Some(entries) => {
10670 if let Some((_, _, fk0)) = entries.first() {
10671 let fk = fk0.clone();
10672 bin_first_keys.push(fk.clone());
10673 anchor = fk;
10674 } else {
10675 break;
10676 }
10677 }
10678 }
10679 }
10680
10681 // We must have visited at least 2 BINs (tree was forced to split).
10682 assert!(
10683 bin_first_keys.len() >= 2,
10684 "R3: expected multiple BINs after split, got {}",
10685 bin_first_keys.len()
10686 );
10687
10688 // With a reverse comparator, bin_first_keys must be in descending byte
10689 // order (each successive BIN starts at a smaller byte key).
10690 for window in bin_first_keys.windows(2) {
10691 assert!(
10692 window[0] > window[1],
10693 "R3: BIN boundary keys must be descending (comparator order); \
10694 got {:?} then {:?}",
10695 window[0],
10696 window[1]
10697 );
10698 }
10699 }
10700 // ========================================================================
10701
10702 /// Inserting keys with a common prefix causes the BIN to establish that
10703 /// prefix. Stored suffixes are shorter than the full keys.
10704 #[test]
10705 fn test_binstub_prefix_established_on_insert() {
10706 let mut bin = BinStub {
10707 node_id: 1,
10708 level: BIN_LEVEL,
10709 entries: Vec::new(),
10710 key_prefix: Vec::new(),
10711 dirty: false,
10712 is_delta: false,
10713 last_full_lsn: NULL_LSN,
10714 last_delta_lsn: NULL_LSN,
10715 generation: 0,
10716 parent: None,
10717 expiration_in_hours: true,
10718 cursor_count: 0,
10719 prohibit_next_delta: false,
10720 lsn_rep: LsnRep::Empty,
10721 keys: KeyRep::new(),
10722 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10723 };
10724
10725 bin.insert_with_prefix(b"record:aaa".to_vec(), Lsn::new(1, 1), None);
10726 assert!(bin.key_prefix.is_empty(), "single entry: no prefix yet");
10727
10728 bin.insert_with_prefix(b"record:bbb".to_vec(), Lsn::new(1, 2), None);
10729 assert_eq!(
10730 &bin.key_prefix, b"record:",
10731 "common prefix 'record:' must be extracted"
10732 );
10733 }
10734
10735 /// `get_full_key` on a BinStub returns the full key regardless of whether
10736 /// the stored key is a raw full key or a suffix.
10737 #[test]
10738 fn test_binstub_get_full_key_roundtrip() {
10739 let mut bin = BinStub {
10740 node_id: 1,
10741 level: BIN_LEVEL,
10742 entries: Vec::new(),
10743 key_prefix: Vec::new(),
10744 dirty: false,
10745 is_delta: false,
10746 last_full_lsn: NULL_LSN,
10747 last_delta_lsn: NULL_LSN,
10748 generation: 0,
10749 parent: None,
10750 expiration_in_hours: true,
10751 cursor_count: 0,
10752 prohibit_next_delta: false,
10753 lsn_rep: LsnRep::Empty,
10754 keys: KeyRep::new(),
10755 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10756 };
10757
10758 let keys = [
10759 b"pfx:first".as_ref(),
10760 b"pfx:second".as_ref(),
10761 b"pfx:third".as_ref(),
10762 ];
10763 for k in keys {
10764 bin.insert_with_prefix(k.to_vec(), Lsn::new(1, 1), None);
10765 }
10766
10767 assert!(!bin.key_prefix.is_empty(), "prefix must be set");
10768
10769 for (i, expected) in keys.iter().enumerate() {
10770 let full = bin.get_full_key(i).expect("must return full key");
10771 assert_eq!(
10772 full.as_slice(),
10773 *expected,
10774 "get_full_key({}) must return full key",
10775 i
10776 );
10777 }
10778 }
10779
10780 /// `find_entry_compressed` on a BinStub with active prefix returns the
10781 /// correct slot index.
10782 #[test]
10783 fn test_binstub_find_entry_compressed() {
10784 let mut bin = BinStub {
10785 node_id: 1,
10786 level: BIN_LEVEL,
10787 entries: Vec::new(),
10788 key_prefix: Vec::new(),
10789 dirty: false,
10790 is_delta: false,
10791 last_full_lsn: NULL_LSN,
10792 last_delta_lsn: NULL_LSN,
10793 generation: 0,
10794 parent: None,
10795 expiration_in_hours: true,
10796 cursor_count: 0,
10797 prohibit_next_delta: false,
10798 lsn_rep: LsnRep::Empty,
10799 keys: KeyRep::new(),
10800 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10801 };
10802
10803 for k in
10804 [b"db:alpha".as_ref(), b"db:beta".as_ref(), b"db:gamma".as_ref()]
10805 {
10806 bin.insert_with_prefix(k.to_vec(), Lsn::new(1, 1), None);
10807 }
10808
10809 let (idx, found) = bin.find_entry_compressed(b"db:beta");
10810 assert!(found, "db:beta must be found");
10811 assert_eq!(idx, 1, "db:beta must be at index 1");
10812
10813 let (_, not_found) = bin.find_entry_compressed(b"db:zzz");
10814 assert!(!not_found, "db:zzz must not be found");
10815 }
10816
10817 /// Tree insert/search works correctly when BINs accumulate a key prefix.
10818 #[test]
10819 fn test_tree_insert_search_with_prefix_compression() {
10820 let tree = Tree::new(1, 8);
10821 let n = 200u32;
10822
10823 // All keys share a long common prefix — good for prefix compression.
10824 for i in 0..n {
10825 let key = format!("namespace:entity:{:06}", i).into_bytes();
10826 let data = vec![i as u8];
10827 tree.insert(key, data, Lsn::new(1, i)).unwrap();
10828 }
10829
10830 // All keys must be findable.
10831 for i in 0..n {
10832 let key = format!("namespace:entity:{:06}", i).into_bytes();
10833 let sr = tree.search(&key);
10834 assert!(
10835 sr.is_some() && sr.unwrap().exact_parent_found,
10836 "key namespace:entity:{:06} must be found",
10837 i
10838 );
10839 }
10840 }
10841
10842 /// Prefix survives a BIN split: keys in both halves must still be findable.
10843 #[test]
10844 fn test_prefix_preserved_across_bin_split() {
10845 // Small fanout to force splits quickly.
10846 let tree = Tree::new(1, 4);
10847
10848 for i in 0u32..20 {
10849 let key = format!("pfx:key:{:04}", i).into_bytes();
10850 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10851 }
10852
10853 // All keys must be findable after splits.
10854 for i in 0u32..20 {
10855 let key = format!("pfx:key:{:04}", i).into_bytes();
10856 let sr = tree.search(&key);
10857 assert!(
10858 sr.is_some() && sr.unwrap().exact_parent_found,
10859 "pfx:key:{:04} must be found after splits",
10860 i
10861 );
10862 }
10863 }
10864
10865 /// `decompress_key` round-trips: compress then decompress gives the original.
10866 #[test]
10867 fn test_binstub_compress_decompress_roundtrip() {
10868 let mut bin = BinStub {
10869 node_id: 1,
10870 level: BIN_LEVEL,
10871 entries: Vec::new(),
10872 key_prefix: Vec::new(),
10873 dirty: false,
10874 is_delta: false,
10875 last_full_lsn: NULL_LSN,
10876 last_delta_lsn: NULL_LSN,
10877 generation: 0,
10878 parent: None,
10879 expiration_in_hours: true,
10880 cursor_count: 0,
10881 prohibit_next_delta: false,
10882 lsn_rep: LsnRep::Empty,
10883 keys: KeyRep::new(),
10884 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10885 };
10886
10887 for k in [b"myapp:user:1".as_ref(), b"myapp:user:2".as_ref()] {
10888 bin.insert_with_prefix(k.to_vec(), Lsn::new(1, 1), None);
10889 }
10890
10891 assert!(!bin.key_prefix.is_empty());
10892
10893 // Manually compress a full key and then decompress it.
10894 let full_key = b"myapp:user:3";
10895 let suffix = bin.compress_key(full_key);
10896 let recovered = bin.decompress_key(&suffix);
10897 assert_eq!(
10898 recovered.as_slice(),
10899 full_key,
10900 "compress→decompress must be identity"
10901 );
10902 }
10903
10904 /// get_next_bin correctly navigates a 3-level tree.
10905 #[test]
10906 fn test_get_next_bin_three_level_tree() {
10907 // With fanout 4, inserting 20 keys forces a root split → 3 levels.
10908 let tree = Tree::new(1, 4);
10909 for i in 0u32..20 {
10910 let key = format!("t{:04}", i).into_bytes();
10911 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10912 }
10913 assert!(tree.get_root_splits() > 0, "tree must have grown to 3 levels");
10914
10915 // Starting from t0000, iterating via get_next_bin must visit every BIN.
10916 let mut visited: Vec<Vec<u8>> = Vec::new();
10917 // Collect the first BIN's keys by searching for t0000.
10918 if let Some(first_entries) = {
10919 // Get the leftmost BIN by using get_first_node result.
10920 // get_first_node returns SearchResult at index 0 in the leftmost BIN.
10921 // We approximate by reading the root's leftmost BIN directly.
10922 tree.get_next_bin(b"t0000")
10923 } {
10924 for (_, _, k) in first_entries {
10925 visited.push(k);
10926 }
10927 }
10928
10929 // visited should contain at least one key from the second BIN.
10930 assert!(
10931 !visited.is_empty(),
10932 "should have visited at least one key via get_next_bin in 3-level tree"
10933 );
10934 }
10935
10936 // ========================================================================
10937 // ========================================================================
10938
10939 /// insert a small set of keys
10940 /// with varying lengths and verify each is findable immediately after insert.
10941 #[test]
10942 fn test_je_simple_tree_creation() {
10943 let tree = Tree::new(1, 128);
10944
10945 let keys: &[&[u8]] = &[b"aaaaa", b"aaaab", b"aaaa", b"aaa"];
10946 for (i, &k) in keys.iter().enumerate() {
10947 tree.insert(k.to_vec(), vec![i as u8], Lsn::new(1, i as u32))
10948 .unwrap();
10949
10950 // Every key inserted so far must be findable.
10951 for &prev in &keys[..=i] {
10952 let sr = tree.search(prev);
10953 assert!(
10954 sr.is_some() && sr.unwrap().exact_parent_found,
10955 "key {:?} must be findable after {} inserts",
10956 std::str::from_utf8(prev).unwrap_or("?"),
10957 i + 1
10958 );
10959 }
10960 }
10961 }
10962
10963 /// insert N keys, verify
10964 /// all are found; delete the even-indexed keys, verify even are gone and
10965 /// odd remain.
10966 #[test]
10967 fn test_je_insert_then_delete_then_search() {
10968 let tree = Tree::new(1, 8);
10969 let n = 20usize;
10970
10971 let keys: Vec<Vec<u8>> =
10972 (0..n).map(|i| format!("key{:04}", i).into_bytes()).collect();
10973
10974 // Insert all.
10975 for (i, k) in keys.iter().enumerate() {
10976 tree.insert(k.clone(), vec![i as u8], Lsn::new(1, i as u32))
10977 .unwrap();
10978 }
10979
10980 // All must be findable.
10981 for k in &keys {
10982 let sr = tree.search(k);
10983 assert!(
10984 sr.is_some() && sr.unwrap().exact_parent_found,
10985 "key {:?} must be found after insert",
10986 std::str::from_utf8(k).unwrap_or("?")
10987 );
10988 }
10989
10990 // Delete even-indexed keys.
10991 for i in (0..n).step_by(2) {
10992 tree.delete(&keys[i]);
10993 }
10994
10995 // Even keys must no longer be found; odd keys must still be found.
10996 for (i, key) in keys.iter().enumerate() {
10997 let sr = tree.search(key);
10998 let found = sr.is_some() && sr.unwrap().exact_parent_found;
10999 if i % 2 == 0 {
11000 assert!(!found, "deleted key {:?} must not be found", i);
11001 } else {
11002 assert!(found, "kept key {:?} must still be found", i);
11003 }
11004 }
11005 }
11006
11007 /// insert N keys in reverse
11008 /// order, then verify every key is directly findable and the keys are in
11009 /// sorted ascending order (B-tree ordering invariant).
11010 #[test]
11011 fn test_je_range_scan_sorted_ascending() {
11012 let n = 40usize;
11013 let tree = Tree::new(1, 4);
11014
11015 // Insert in reverse order to stress the B-tree.
11016 for i in (0..n).rev() {
11017 let key = format!("scan{:04}", i).into_bytes();
11018 tree.insert(key, vec![i as u8], Lsn::new(1, i as u32)).unwrap();
11019 }
11020
11021 // Collect all expected keys in sorted order.
11022 let mut expected: Vec<Vec<u8>> =
11023 (0..n).map(|i| format!("scan{:04}", i).into_bytes()).collect();
11024 expected.sort();
11025
11026 // Every key must be individually findable.
11027 for key in &expected {
11028 let sr = tree.search(key);
11029 assert!(
11030 sr.is_some() && sr.unwrap().exact_parent_found,
11031 "key {:?} must be findable",
11032 std::str::from_utf8(key).unwrap_or("?")
11033 );
11034 }
11035
11036 // Verify sorted ordering invariant: expected keys are already sorted
11037 // (lexicographic order = insertion order for "scan{:04}" keys).
11038 for w in expected.windows(2) {
11039 assert!(
11040 w[0] < w[1],
11041 "keys must be in strict ascending order: {:?} < {:?}",
11042 std::str::from_utf8(&w[0]).unwrap_or("?"),
11043 std::str::from_utf8(&w[1]).unwrap_or("?")
11044 );
11045 }
11046
11047 // Use get_next_bin to scan at least a portion of the tree and verify
11048 // ordering of returned BIN entries.
11049 let first_key = format!("scan{:04}", 0).into_bytes();
11050 if let Some(entries) = tree.get_next_bin(&first_key) {
11051 let entry_keys: Vec<&[u8]> =
11052 entries.iter().map(|(_, _, k)| k.as_slice()).collect();
11053 for w in entry_keys.windows(2) {
11054 assert!(
11055 w[0] <= w[1],
11056 "BIN entries from get_next_bin must be in ascending order"
11057 );
11058 }
11059 }
11060 }
11061
11062 /// insert N keys in
11063 /// ascending order and verify the tree height stays bounded (≤ 10 levels)
11064 /// and all keys are findable.
11065 #[test]
11066 fn test_je_ascending_insert_balance() {
11067 let n = 128usize;
11068 let tree = Tree::new(1, 8);
11069
11070 for i in 0..n {
11071 let key = format!("asc{:06}", i).into_bytes();
11072 tree.insert(key, vec![(i & 0xFF) as u8], Lsn::new(1, i as u32))
11073 .unwrap();
11074 }
11075
11076 let stats = tree.collect_stats();
11077 assert!(
11078 stats.height <= 10,
11079 "tree height after {} ascending inserts with fanout 8 must be <= 10, got {}",
11080 n,
11081 stats.height
11082 );
11083
11084 for i in 0..n {
11085 let key = format!("asc{:06}", i).into_bytes();
11086 let sr = tree.search(&key);
11087 assert!(
11088 sr.is_some() && sr.unwrap().exact_parent_found,
11089 "key asc{:06} must be findable after ascending inserts",
11090 i
11091 );
11092 }
11093 }
11094
11095 /// insert N keys in
11096 /// descending order and verify the tree height stays bounded (≤ 10 levels)
11097 /// and all keys are findable.
11098 #[test]
11099 fn test_je_descending_insert_balance() {
11100 let n = 128usize;
11101 let tree = Tree::new(1, 8);
11102
11103 for i in (0..n).rev() {
11104 let key = format!("dsc{:06}", i).into_bytes();
11105 tree.insert(key, vec![(i & 0xFF) as u8], Lsn::new(1, i as u32))
11106 .unwrap();
11107 }
11108
11109 let stats = tree.collect_stats();
11110 assert!(
11111 stats.height <= 10,
11112 "tree height after {} descending inserts with fanout 8 must be <= 10, got {}",
11113 n,
11114 stats.height
11115 );
11116
11117 for i in 0..n {
11118 let key = format!("dsc{:06}", i).into_bytes();
11119 let sr = tree.search(&key);
11120 assert!(
11121 sr.is_some() && sr.unwrap().exact_parent_found,
11122 "key dsc{:06} must be findable after descending inserts",
11123 i
11124 );
11125 }
11126 }
11127
11128 /// SplitTest invariant: after many splits induced by a small
11129 /// fanout no key is lost.
11130 #[test]
11131 fn test_je_split_no_key_lost() {
11132 let tree = Tree::new(1, 4);
11133 let n = 20usize;
11134
11135 for i in 0..n {
11136 let key = format!("sp{:04}", i).into_bytes();
11137 tree.insert(key, vec![i as u8], Lsn::new(1, i as u32)).unwrap();
11138 }
11139
11140 for i in 0..n {
11141 let key = format!("sp{:04}", i).into_bytes();
11142 let sr = tree.search(&key);
11143 assert!(
11144 sr.is_some() && sr.unwrap().exact_parent_found,
11145 "key sp{:04} must survive all splits",
11146 i
11147 );
11148 }
11149 }
11150
11151 /// SplitTest invariant: after a BIN split both halves exist and
11152 /// all original keys are findable.
11153 #[test]
11154 fn test_je_split_produces_two_halves() {
11155 // fanout=4: fill one BIN then overflow it to force a split.
11156 let tree = Tree::new(1, 4);
11157 let n = 5usize; // one more than fanout → forces at least one split
11158
11159 for i in 0..n {
11160 let key = format!("half{:04}", i).into_bytes();
11161 tree.insert(key, vec![i as u8], Lsn::new(1, i as u32)).unwrap();
11162 }
11163
11164 let stats = tree.collect_stats();
11165 assert!(
11166 stats.n_bins >= 2,
11167 "after splitting a full BIN there must be >= 2 BINs, got {}",
11168 stats.n_bins
11169 );
11170
11171 for i in 0..n {
11172 let key = format!("half{:04}", i).into_bytes();
11173 let sr = tree.search(&key);
11174 assert!(
11175 sr.is_some() && sr.unwrap().exact_parent_found,
11176 "key half{:04} must be findable in one of the two halves",
11177 i
11178 );
11179 }
11180 }
11181
11182 /// SplitTest invariant: root splits are tracked and the tree
11183 /// grows in height as keys accumulate.
11184 #[test]
11185 fn test_je_root_split_creates_new_root() {
11186 // fanout=4, 20 keys: forces multiple root splits.
11187 let tree = Tree::new(1, 4);
11188
11189 for i in 0u32..20 {
11190 let key = format!("rs{:04}", i).into_bytes();
11191 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
11192 }
11193
11194 assert!(
11195 tree.get_root_splits() > 0,
11196 "expected at least one root split after 20 inserts with fanout 4"
11197 );
11198
11199 let stats = tree.collect_stats();
11200 assert!(
11201 stats.height >= 3,
11202 "tree must be at least 3 levels tall after root splits, got {}",
11203 stats.height
11204 );
11205
11206 // Every inserted key must still be findable.
11207 for i in 0u32..20 {
11208 let key = format!("rs{:04}", i).into_bytes();
11209 let sr = tree.search(&key);
11210 assert!(
11211 sr.is_some() && sr.unwrap().exact_parent_found,
11212 "key rs{:04} must be findable after root splits",
11213 i
11214 );
11215 }
11216 }
11217
11218 // ========================================================================
11219 // Tests: compress_bin / maybe_compress_bin_and_parent
11220 // INCompressor.compressBin / lazyCompress tests
11221 // ========================================================================
11222
11223 /// compress_bin removes known-deleted slots from a BIN.
11224 ///
11225 /// INCompressor.compressBin(): after compression, slots with
11226 /// `known_deleted = true` must be gone and the BIN must be dirty.
11227 #[test]
11228 fn test_compress_bin_removes_deleted_slots() {
11229 let _lsn = Lsn::new(1, 1);
11230 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11231 node_id: generate_node_id(),
11232 level: BIN_LEVEL,
11233 entries: vec![
11234 BinEntry {
11235 data: Some(b"live".to_vec()),
11236 known_deleted: false,
11237 dirty: false,
11238 expiration_time: 0,
11239 },
11240 BinEntry {
11241 data: None,
11242 known_deleted: true,
11243 dirty: false,
11244 expiration_time: 0,
11245 },
11246 BinEntry {
11247 data: Some(b"live2".to_vec()),
11248 known_deleted: false,
11249 dirty: false,
11250 expiration_time: 0,
11251 },
11252 BinEntry {
11253 data: None,
11254 known_deleted: true,
11255 dirty: false,
11256 expiration_time: 0,
11257 },
11258 ],
11259 key_prefix: Vec::new(),
11260 dirty: false,
11261 is_delta: false,
11262 last_full_lsn: NULL_LSN,
11263 last_delta_lsn: NULL_LSN,
11264 generation: 0,
11265 parent: None,
11266 expiration_in_hours: true,
11267 cursor_count: 0,
11268 prohibit_next_delta: false,
11269 lsn_rep: LsnRep::Empty,
11270 keys: KeyRep::from_keys(vec![
11271 b"a".to_vec(),
11272 b"b".to_vec(),
11273 b"c".to_vec(),
11274 b"d".to_vec(),
11275 ]),
11276 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11277 })));
11278
11279 // Wire a minimal parent IN so compress_bin can prune if needed.
11280 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
11281 node_id: generate_node_id(),
11282 level: MAIN_LEVEL | 2,
11283 entries: vec![InEntry { key: vec![] }],
11284 targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
11285 dirty: false,
11286 generation: 0,
11287 parent: None,
11288 lsn_rep: LsnRep::Empty,
11289 })));
11290 {
11291 let mut g = bin_arc.write();
11292 g.set_parent(Some(Arc::downgrade(&root_arc)));
11293 }
11294
11295 let tree = Tree::new(1, 128);
11296 *tree.root.write() = Some(root_arc);
11297
11298 let result = tree.compress_bin(&bin_arc);
11299 assert!(
11300 result,
11301 "compress_bin must return true when slots were removed"
11302 );
11303
11304 let g = bin_arc.read();
11305 match &*g {
11306 TreeNode::Bottom(b) => {
11307 assert_eq!(
11308 b.entries.len(),
11309 2,
11310 "2 live entries must remain after compress"
11311 );
11312 assert!(
11313 b.entries.iter().all(|e| !e.known_deleted),
11314 "no deleted slots must remain"
11315 );
11316 assert!(b.dirty, "BIN must be dirty after compression");
11317 }
11318 _ => panic!("expected BIN"),
11319 }
11320 }
11321
11322 /// IC-3 HEADLINE (fail-pre / pass-post): the compressor must SKIP a
11323 /// `known_deleted` slot that is still write-locked by an in-flight txn,
11324 /// while removing committed/unlocked `known_deleted` slots in the SAME
11325 /// BIN. Mirrors JE `BIN.compress` (BIN.java:1141-1172), which calls
11326 /// `lockManager.isLockUncontended(lsn)` and does `continue` on a contended
11327 /// slot.
11328 ///
11329 /// Pre-fix: `compress_bin` had no lock check, so a write-locked tombstone
11330 /// would have been physically removed (the slot a live txn references is
11331 /// gone -> corruption). Post-fix: the `is_locked` predicate keeps it.
11332 #[test]
11333 fn test_ic3_compress_skips_write_locked_slot() {
11334 // Slot 1 (key "b", lsn 1:200) is a write-locked tombstone; slot 3
11335 // (key "d", lsn 1:400) is a committed/unlocked tombstone. Slots 0
11336 // and 2 are live.
11337 let locked_lsn = Lsn::new(1, 200);
11338 let unlocked_lsn = Lsn::new(1, 400);
11339 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11340 node_id: generate_node_id(),
11341 level: BIN_LEVEL,
11342 entries: vec![
11343 BinEntry {
11344 data: Some(b"live".to_vec()),
11345 known_deleted: false,
11346 dirty: false,
11347 expiration_time: 0,
11348 },
11349 BinEntry {
11350 data: None,
11351 known_deleted: true, // write-locked tombstone -> KEEP
11352 dirty: false,
11353 expiration_time: 0,
11354 },
11355 BinEntry {
11356 data: Some(b"live2".to_vec()),
11357 known_deleted: false,
11358 dirty: false,
11359 expiration_time: 0,
11360 },
11361 BinEntry {
11362 data: None,
11363 known_deleted: true, // committed tombstone -> REMOVE
11364 dirty: false,
11365 expiration_time: 0,
11366 },
11367 ],
11368 key_prefix: Vec::new(),
11369 dirty: false,
11370 is_delta: false,
11371 last_full_lsn: NULL_LSN,
11372 last_delta_lsn: NULL_LSN,
11373 generation: 0,
11374 parent: None,
11375 expiration_in_hours: true,
11376 cursor_count: 0,
11377 prohibit_next_delta: false,
11378 lsn_rep: LsnRep::from_lsns(&[
11379 Lsn::new(1, 100),
11380 locked_lsn,
11381 Lsn::new(1, 300),
11382 unlocked_lsn,
11383 ]),
11384 keys: KeyRep::from_keys(vec![
11385 b"a".to_vec(),
11386 b"b".to_vec(),
11387 b"c".to_vec(),
11388 b"d".to_vec(),
11389 ]),
11390 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11391 })));
11392 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
11393 node_id: generate_node_id(),
11394 level: MAIN_LEVEL | 2,
11395 entries: vec![InEntry { key: vec![] }],
11396 targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
11397 dirty: false,
11398 generation: 0,
11399 parent: None,
11400 lsn_rep: LsnRep::Empty,
11401 })));
11402 {
11403 let mut g = bin_arc.write();
11404 g.set_parent(Some(Arc::downgrade(&root_arc)));
11405 }
11406 let tree = Tree::new(1, 128);
11407 *tree.root.write() = Some(root_arc);
11408
11409 // Predicate: only `locked_lsn` is write-locked (stub LockManager).
11410 let locked_u64 = locked_lsn.as_u64();
11411 let is_locked = move |lsn: u64| lsn == locked_u64;
11412
11413 let result =
11414 tree.compress_bin_with_lock_check(&bin_arc, Some(&is_locked));
11415 assert!(result, "compress removed the unlocked tombstone -> true");
11416
11417 let g = bin_arc.read();
11418 match &*g {
11419 TreeNode::Bottom(b) => {
11420 // 2 live + 1 write-locked tombstone kept; the committed
11421 // tombstone (lsn 1:400) removed.
11422 assert_eq!(
11423 b.entries.len(),
11424 3,
11425 "write-locked tombstone must be KEPT; only the unlocked one removed"
11426 );
11427 let kept_locked = (0..b.entries.len()).any(|i| {
11428 b.entries[i].known_deleted && b.get_lsn(i) == locked_lsn
11429 });
11430 assert!(kept_locked, "the write-locked tombstone must remain");
11431 let unlocked_gone =
11432 (0..b.entries.len()).all(|i| b.get_lsn(i) != unlocked_lsn);
11433 assert!(
11434 unlocked_gone,
11435 "the unlocked tombstone must be removed"
11436 );
11437 }
11438 _ => panic!("expected BIN"),
11439 }
11440 }
11441
11442 /// IC-3 (no predicate): with `is_locked = None` behavior is unchanged —
11443 /// ALL `known_deleted` slots are removed (the historical safe path).
11444 #[test]
11445 fn test_ic3_compress_no_predicate_removes_all_tombstones() {
11446 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11447 node_id: generate_node_id(),
11448 level: BIN_LEVEL,
11449 entries: vec![
11450 BinEntry {
11451 data: Some(b"live".to_vec()),
11452 known_deleted: false,
11453 dirty: false,
11454 expiration_time: 0,
11455 },
11456 BinEntry {
11457 data: None,
11458 known_deleted: true,
11459 dirty: false,
11460 expiration_time: 0,
11461 },
11462 BinEntry {
11463 data: None,
11464 known_deleted: true,
11465 dirty: false,
11466 expiration_time: 0,
11467 },
11468 ],
11469 key_prefix: Vec::new(),
11470 dirty: false,
11471 is_delta: false,
11472 last_full_lsn: NULL_LSN,
11473 last_delta_lsn: NULL_LSN,
11474 generation: 0,
11475 parent: None,
11476 expiration_in_hours: true,
11477 cursor_count: 0,
11478 prohibit_next_delta: false,
11479 lsn_rep: LsnRep::from_lsns(&[
11480 Lsn::new(1, 100),
11481 Lsn::new(1, 200),
11482 Lsn::new(1, 300),
11483 ]),
11484 keys: KeyRep::from_keys(vec![
11485 b"a".to_vec(),
11486 b"b".to_vec(),
11487 b"c".to_vec(),
11488 ]),
11489 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11490 })));
11491 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
11492 node_id: generate_node_id(),
11493 level: MAIN_LEVEL | 2,
11494 entries: vec![InEntry { key: vec![] }],
11495 targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
11496 dirty: false,
11497 generation: 0,
11498 parent: None,
11499 lsn_rep: LsnRep::Empty,
11500 })));
11501 {
11502 let mut g = bin_arc.write();
11503 g.set_parent(Some(Arc::downgrade(&root_arc)));
11504 }
11505 let tree = Tree::new(1, 128);
11506 *tree.root.write() = Some(root_arc);
11507
11508 let result = tree.compress_bin(&bin_arc); // None predicate path
11509 assert!(result, "all tombstones removed -> true");
11510 let g = bin_arc.read();
11511 match &*g {
11512 TreeNode::Bottom(b) => {
11513 assert_eq!(b.entries.len(), 1, "only the live slot remains");
11514 assert!(b.entries.iter().all(|e| !e.known_deleted));
11515 }
11516 _ => panic!("expected BIN"),
11517 }
11518 }
11519
11520 /// compress_bin on a BIN with no deleted slots returns false.
11521 ///
11522 /// INCompressor: if no slots were removed, compression made no
11523 /// progress and returns false.
11524 #[test]
11525 fn test_compress_bin_no_deleted_slots_returns_false() {
11526 let _lsn = Lsn::new(1, 1);
11527 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11528 node_id: generate_node_id(),
11529 level: BIN_LEVEL,
11530 entries: vec![BinEntry {
11531 data: Some(b"d".to_vec()),
11532 known_deleted: false,
11533 dirty: false,
11534 expiration_time: 0,
11535 }],
11536 key_prefix: Vec::new(),
11537 dirty: false,
11538 is_delta: false,
11539 last_full_lsn: NULL_LSN,
11540 last_delta_lsn: NULL_LSN,
11541 generation: 0,
11542 parent: None,
11543 expiration_in_hours: true,
11544 cursor_count: 0,
11545 prohibit_next_delta: false,
11546 lsn_rep: LsnRep::Empty,
11547 keys: KeyRep::from_keys(vec![b"x".to_vec()]),
11548 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11549 })));
11550
11551 let tree = Tree::new(1, 128);
11552 let result = tree.compress_bin(&bin_arc);
11553 assert!(
11554 !result,
11555 "compress_bin must return false when no slots were removed"
11556 );
11557 }
11558
11559 /// compress_bin on a BIN-delta is a no-op.
11560 ///
11561 /// INCompressor.compressBin(): "if (bin.isBINDelta()) return".
11562 #[test]
11563 fn test_compress_bin_skips_delta() {
11564 let _lsn = Lsn::new(1, 1);
11565 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11566 node_id: generate_node_id(),
11567 level: BIN_LEVEL,
11568 entries: vec![BinEntry {
11569 data: None,
11570 known_deleted: true,
11571 dirty: false,
11572 expiration_time: 0,
11573 }],
11574 key_prefix: Vec::new(),
11575 dirty: false,
11576 is_delta: true, // delta BIN — must be skipped
11577 last_full_lsn: NULL_LSN,
11578 last_delta_lsn: NULL_LSN,
11579 generation: 0,
11580 parent: None,
11581 expiration_in_hours: true,
11582 cursor_count: 0,
11583 prohibit_next_delta: false,
11584 lsn_rep: LsnRep::Empty,
11585 keys: KeyRep::from_keys(vec![b"k".to_vec()]),
11586 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11587 })));
11588
11589 let tree = Tree::new(1, 128);
11590 let result = tree.compress_bin(&bin_arc);
11591 assert!(!result, "compress_bin must not compress a BIN-delta");
11592
11593 // The slot must still be there.
11594 let g = bin_arc.read();
11595 match &*g {
11596 TreeNode::Bottom(b) => assert_eq!(
11597 b.entries.len(),
11598 1,
11599 "slot must not be removed from delta"
11600 ),
11601 _ => panic!("expected BIN"),
11602 }
11603 }
11604
11605 /// compress_bin prunes an empty BIN from the tree.
11606 ///
11607 /// INCompressor.pruneBIN(): when all slots are deleted and
11608 /// compression empties the BIN, it must be removed from the parent IN.
11609 #[test]
11610 fn test_compress_bin_prunes_empty_bin() {
11611 let _lsn = Lsn::new(1, 1);
11612 // Insert a live key so the tree can be searched to prune.
11613 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11614 node_id: generate_node_id(),
11615 level: BIN_LEVEL,
11616 entries: vec![BinEntry {
11617 data: None,
11618 known_deleted: true,
11619 dirty: false,
11620 expiration_time: 0,
11621 }],
11622 key_prefix: Vec::new(),
11623 dirty: false,
11624 is_delta: false,
11625 last_full_lsn: NULL_LSN,
11626 last_delta_lsn: NULL_LSN,
11627 generation: 0,
11628 parent: None,
11629 expiration_in_hours: true,
11630 cursor_count: 0,
11631 prohibit_next_delta: false,
11632 lsn_rep: LsnRep::Empty,
11633 keys: KeyRep::from_keys(vec![b"only".to_vec()]),
11634 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11635 })));
11636
11637 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
11638 node_id: generate_node_id(),
11639 level: MAIN_LEVEL | 2,
11640 entries: vec![InEntry { key: vec![] }],
11641 targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
11642 dirty: false,
11643 generation: 0,
11644 parent: None,
11645 lsn_rep: LsnRep::Empty,
11646 })));
11647 {
11648 let mut g = bin_arc.write();
11649 g.set_parent(Some(Arc::downgrade(&root_arc)));
11650 }
11651
11652 let tree = Tree::new(1, 128);
11653 *tree.root.write() = Some(root_arc);
11654
11655 let result = tree.compress_bin(&bin_arc);
11656 assert!(result, "compress_bin must return true when pruning");
11657
11658 // BIN must be empty after compression.
11659 let g = bin_arc.read();
11660 match &*g {
11661 TreeNode::Bottom(b) => {
11662 assert_eq!(b.entries.len(), 0, "all slots must be removed")
11663 }
11664 _ => panic!("expected BIN"),
11665 }
11666 }
11667
11668 /// maybe_compress_bin_and_parent returns false when no deleted slots exist.
11669 ///
11670 /// INCompressor.lazyCompress(): skip BINs with no defunct slots.
11671 #[test]
11672 fn test_maybe_compress_skips_clean_bin() {
11673 let _lsn = Lsn::new(1, 1);
11674 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11675 node_id: generate_node_id(),
11676 level: BIN_LEVEL,
11677 entries: vec![BinEntry {
11678 data: Some(b"v".to_vec()),
11679 known_deleted: false,
11680 dirty: false,
11681 expiration_time: 0,
11682 }],
11683 key_prefix: Vec::new(),
11684 dirty: false,
11685 is_delta: false,
11686 last_full_lsn: NULL_LSN,
11687 last_delta_lsn: NULL_LSN,
11688 generation: 0,
11689 parent: None,
11690 expiration_in_hours: true,
11691 cursor_count: 0,
11692 prohibit_next_delta: false,
11693 lsn_rep: LsnRep::Empty,
11694 keys: KeyRep::from_keys(vec![b"live".to_vec()]),
11695 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11696 })));
11697
11698 let tree = Tree::new(1, 128);
11699 let result = tree.maybe_compress_bin_and_parent(&bin_arc);
11700 assert!(
11701 !result,
11702 "maybe_compress must return false when no deleted slots exist"
11703 );
11704 }
11705
11706 /// maybe_compress_bin_and_parent triggers compression when deleted slots exist.
11707 ///
11708 /// INCompressor.lazyCompress(): when defunct slots are found,
11709 /// call bin.compress() to remove them.
11710 #[test]
11711 fn test_maybe_compress_triggers_when_deleted_slots_exist() {
11712 let _lsn = Lsn::new(1, 1);
11713 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11714 node_id: generate_node_id(),
11715 level: BIN_LEVEL,
11716 entries: vec![
11717 BinEntry {
11718 data: Some(b"v".to_vec()),
11719 known_deleted: false,
11720 dirty: false,
11721 expiration_time: 0,
11722 },
11723 BinEntry {
11724 data: None,
11725 known_deleted: true,
11726 dirty: false,
11727 expiration_time: 0,
11728 },
11729 ],
11730 key_prefix: Vec::new(),
11731 dirty: false,
11732 is_delta: false,
11733 last_full_lsn: NULL_LSN,
11734 last_delta_lsn: NULL_LSN,
11735 generation: 0,
11736 parent: None,
11737 expiration_in_hours: true,
11738 cursor_count: 0,
11739 prohibit_next_delta: false,
11740 lsn_rep: LsnRep::Empty,
11741 keys: KeyRep::from_keys(vec![b"live".to_vec(), b"dead".to_vec()]),
11742 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11743 })));
11744
11745 let tree = Tree::new(1, 128);
11746 let result = tree.maybe_compress_bin_and_parent(&bin_arc);
11747 assert!(
11748 result,
11749 "maybe_compress must return true when deleted slots were removed"
11750 );
11751
11752 let g = bin_arc.read();
11753 match &*g {
11754 TreeNode::Bottom(b) => {
11755 assert_eq!(b.entries.len(), 1, "only live entry must remain");
11756 assert_eq!(b.get_full_key(0).unwrap(), b"live");
11757 }
11758 _ => panic!("expected BIN"),
11759 }
11760 }
11761
11762 // ========================================================================
11763 // Tests: INCompressorTest / EmptyBINTest ports
11764 // INCompressorTest (compress_bin semantics, prefix recompute, live-slot preservation)
11765 // EmptyBINTest (empty-BIN scan, all-deleted compress, search returns NotFound)
11766 // ========================================================================
11767
11768 ///
11769 /// Insert two live keys and one deleted key into a BIN wired into a tree.
11770 /// After compress_bin the deleted slot must be gone; the live slots remain.
11771 /// The parent IN entry count must not change.
11772 #[test]
11773 fn test_incompressor_live_slots_preserved_after_compress() {
11774 let _lsn = Lsn::new(1, 100);
11775
11776 // BIN with 3 entries: two live, one known-deleted.
11777 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11778 node_id: generate_node_id(),
11779 level: BIN_LEVEL,
11780 entries: vec![
11781 BinEntry {
11782 data: Some(b"d0".to_vec()),
11783 known_deleted: false,
11784 dirty: false,
11785 expiration_time: 0,
11786 },
11787 BinEntry {
11788 data: Some(b"d1".to_vec()),
11789 known_deleted: false,
11790 dirty: false,
11791 expiration_time: 0,
11792 },
11793 BinEntry {
11794 data: None,
11795 known_deleted: true,
11796 dirty: false,
11797 expiration_time: 0,
11798 },
11799 ],
11800 key_prefix: Vec::new(),
11801 dirty: false,
11802 is_delta: false,
11803 last_full_lsn: NULL_LSN,
11804 last_delta_lsn: NULL_LSN,
11805 generation: 0,
11806 parent: None,
11807 expiration_in_hours: true,
11808 cursor_count: 0,
11809 prohibit_next_delta: false,
11810 lsn_rep: LsnRep::Empty,
11811 keys: KeyRep::from_keys(vec![
11812 b"\x00".to_vec(),
11813 b"\x01".to_vec(),
11814 b"\x02".to_vec(),
11815 ]),
11816 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11817 })));
11818
11819 // Parent IN with two children: the BIN above plus a placeholder sibling.
11820 let sibling_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11821 node_id: generate_node_id(),
11822 level: BIN_LEVEL,
11823 entries: vec![BinEntry {
11824 data: Some(b"s".to_vec()),
11825 known_deleted: false,
11826 dirty: false,
11827 expiration_time: 0,
11828 }],
11829 key_prefix: Vec::new(),
11830 dirty: false,
11831 is_delta: false,
11832 last_full_lsn: NULL_LSN,
11833 last_delta_lsn: NULL_LSN,
11834 generation: 0,
11835 parent: None,
11836 expiration_in_hours: true,
11837 cursor_count: 0,
11838 prohibit_next_delta: false,
11839 lsn_rep: LsnRep::Empty,
11840 keys: KeyRep::from_keys(vec![b"\x40".to_vec()]),
11841 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11842 })));
11843
11844 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
11845 node_id: generate_node_id(),
11846 level: MAIN_LEVEL | 2,
11847 entries: vec![
11848 InEntry { key: vec![] },
11849 InEntry { key: b"\x40".to_vec() },
11850 ],
11851 targets: TargetRep::Sparse(vec![
11852 (0, bin_arc.clone()),
11853 (1, sibling_arc.clone()),
11854 ]),
11855 dirty: false,
11856 generation: 0,
11857 parent: None,
11858 lsn_rep: LsnRep::Empty,
11859 })));
11860 bin_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
11861 sibling_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
11862
11863 let tree = Tree::new(1, 128);
11864 *tree.root.write() = Some(root_arc.clone());
11865
11866 let result = tree.compress_bin(&bin_arc);
11867 assert!(
11868 result,
11869 "compress_bin must return true when a deleted slot was removed"
11870 );
11871
11872 // Exactly 2 live entries must remain.
11873 let g = bin_arc.read();
11874 match &*g {
11875 TreeNode::Bottom(b) => {
11876 assert_eq!(b.entries.len(), 2, "2 live slots must remain");
11877 assert!(
11878 b.entries.iter().all(|e| !e.known_deleted),
11879 "no deleted slots may remain"
11880 );
11881 assert!(b.dirty, "BIN must be dirty after compression");
11882 }
11883 _ => panic!("expected BIN"),
11884 }
11885 drop(g);
11886
11887 // Parent IN must still have 2 entries (BIN was not emptied).
11888 let rg = root_arc.read();
11889 match &*rg {
11890 TreeNode::Internal(n) => {
11891 assert_eq!(
11892 n.entries.len(),
11893 2,
11894 "parent IN must still have 2 entries"
11895 );
11896 }
11897 _ => panic!("expected IN"),
11898 }
11899 }
11900
11901 ///
11902 /// After all slots in a BIN are deleted and compress() is called, the
11903 /// empty BIN must be removed from its parent IN (pruneBIN path).
11904 ///
11905 /// Uses tree.compress() which correctly invokes
11906 /// the pruneBIN / merge logic that removes empty BINs from the parent IN.
11907 #[test]
11908 fn test_incompressor_empty_bin_pruned_from_parent() {
11909 // Use a small node size so that a modest number of inserts produces
11910 // multiple BINs that can be pruned after all-delete.
11911 let tree = Tree::new(1, 4);
11912
11913 // Insert enough keys to create at least 2 BINs.
11914 for i in 0u32..12 {
11915 let key = format!("prune{:04}", i).into_bytes();
11916 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
11917 }
11918
11919 let stats_before = tree.collect_stats();
11920 assert!(stats_before.n_bins >= 2, "need multiple BINs to test pruning");
11921
11922 // Delete all keys in the first BIN (the lexicographically smallest ones).
11923 // This empties that BIN so compress() must prune it from the parent.
11924 for i in 0u32..4 {
11925 let key = format!("prune{:04}", i).into_bytes();
11926 tree.delete(&key);
11927 }
11928
11929 // compress() triggers pruneBIN for the now-empty BIN.
11930 tree.compress();
11931
11932 let stats_after = tree.collect_stats();
11933 assert!(
11934 stats_after.n_bins < stats_before.n_bins,
11935 "compress must reduce BIN count after emptying a BIN (pruneBIN path)"
11936 );
11937
11938 // Remaining keys must still be findable.
11939 for i in 4u32..12 {
11940 let key = format!("prune{:04}", i).into_bytes();
11941 let sr = tree.search(&key);
11942 assert!(
11943 sr.is_some() && sr.unwrap().exact_parent_found,
11944 "key prune{:04} must survive after compress",
11945 i
11946 );
11947 }
11948 }
11949
11950 /// BIN-delta is skipped by maybe_compress.
11951 ///
11952 /// INCompressor.lazyCompress() short-circuits for BIN-deltas:
11953 /// "if (in.isBINDelta()) return false".
11954 #[test]
11955 fn test_incompressor_maybe_compress_skips_bin_delta() {
11956 let _lsn = Lsn::new(1, 1);
11957 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11958 node_id: generate_node_id(),
11959 level: BIN_LEVEL,
11960 entries: vec![BinEntry {
11961 data: None,
11962 known_deleted: true,
11963 dirty: false,
11964 expiration_time: 0,
11965 }],
11966 key_prefix: Vec::new(),
11967 dirty: false,
11968 is_delta: true, // BIN-delta — must be skipped
11969 last_full_lsn: NULL_LSN,
11970 last_delta_lsn: NULL_LSN,
11971 generation: 0,
11972 parent: None,
11973 expiration_in_hours: true,
11974 cursor_count: 0,
11975 prohibit_next_delta: false,
11976 lsn_rep: LsnRep::Empty,
11977 keys: KeyRep::from_keys(vec![b"k".to_vec()]),
11978 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11979 })));
11980
11981 let tree = Tree::new(1, 128);
11982 // maybe_compress must return false without touching the BIN.
11983 assert!(
11984 !tree.maybe_compress_bin_and_parent(&bin_arc),
11985 "maybe_compress must return false for BIN-deltas"
11986 );
11987
11988 // Slot must still be present and still known-deleted.
11989 let g = bin_arc.read();
11990 match &*g {
11991 TreeNode::Bottom(b) => {
11992 assert_eq!(
11993 b.entries.len(),
11994 1,
11995 "slot must not be removed from delta BIN"
11996 );
11997 assert!(b.entries[0].known_deleted);
11998 }
11999 _ => panic!("expected BIN"),
12000 }
12001 }
12002
12003 /// Clean BIN (no deleted slots) is not compressed.
12004 ///
12005 /// INCompressor.lazyCompress() skips BINs that have no defunct slots.
12006 #[test]
12007 fn test_incompressor_clean_bin_not_compressed() {
12008 let _lsn = Lsn::new(1, 1);
12009 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
12010 node_id: generate_node_id(),
12011 level: BIN_LEVEL,
12012 entries: vec![
12013 BinEntry {
12014 data: Some(b"a".to_vec()),
12015 known_deleted: false,
12016 dirty: false,
12017 expiration_time: 0,
12018 },
12019 BinEntry {
12020 data: Some(b"b".to_vec()),
12021 known_deleted: false,
12022 dirty: false,
12023 expiration_time: 0,
12024 },
12025 ],
12026 key_prefix: Vec::new(),
12027 dirty: false,
12028 is_delta: false,
12029 last_full_lsn: NULL_LSN,
12030 last_delta_lsn: NULL_LSN,
12031 generation: 0,
12032 parent: None,
12033 expiration_in_hours: true,
12034 cursor_count: 0,
12035 prohibit_next_delta: false,
12036 lsn_rep: LsnRep::Empty,
12037 keys: KeyRep::from_keys(vec![b"\x00".to_vec(), b"\x01".to_vec()]),
12038 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12039 })));
12040
12041 let tree = Tree::new(1, 128);
12042 assert!(
12043 !tree.maybe_compress_bin_and_parent(&bin_arc),
12044 "maybe_compress must return false when no deleted slots exist"
12045 );
12046
12047 // Both entries must remain untouched.
12048 let g = bin_arc.read();
12049 match &*g {
12050 TreeNode::Bottom(b) => {
12051 assert_eq!(b.entries.len(), 2, "no entries should be removed")
12052 }
12053 _ => panic!("expected BIN"),
12054 }
12055 }
12056
12057 /// Prefix is recomputed after compression.
12058 ///
12059 /// When keys share a common prefix (e.g. "pfx:a", "pfx:b", "pfx:c") and
12060 /// one is deleted, after compress_bin the remaining keys must share the
12061 /// correct (potentially longer) prefix.
12062 ///
12063 /// After BIN.compress() the BIN calls recalcKeyPrefix() so the
12064 /// shorter remaining key set may expose a longer common prefix.
12065 #[test]
12066 fn test_incompressor_prefix_recomputed_after_compress() {
12067 let _lsn = Lsn::new(1, 1);
12068
12069 // Three keys all starting with "pfx:". After deleting "pfx:a" the
12070 // remaining two ("pfx:b", "pfx:c") still share "pfx:" as prefix.
12071 // We store them without prefix compression initially (raw keys).
12072 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
12073 node_id: generate_node_id(),
12074 level: BIN_LEVEL,
12075 entries: vec![
12076 BinEntry {
12077 data: None,
12078 known_deleted: true,
12079 dirty: false,
12080 expiration_time: 0,
12081 },
12082 BinEntry {
12083 data: Some(b"B".to_vec()),
12084 known_deleted: false,
12085 dirty: false,
12086 expiration_time: 0,
12087 },
12088 BinEntry {
12089 data: Some(b"C".to_vec()),
12090 known_deleted: false,
12091 dirty: false,
12092 expiration_time: 0,
12093 },
12094 ],
12095 key_prefix: Vec::new(),
12096 dirty: false,
12097 is_delta: false,
12098 last_full_lsn: NULL_LSN,
12099 last_delta_lsn: NULL_LSN,
12100 generation: 0,
12101 parent: None,
12102 expiration_in_hours: true,
12103 cursor_count: 0,
12104 prohibit_next_delta: false,
12105 lsn_rep: LsnRep::Empty,
12106 keys: KeyRep::from_keys(vec![
12107 b"pfx:a".to_vec(),
12108 b"pfx:b".to_vec(),
12109 b"pfx:c".to_vec(),
12110 ]),
12111 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12112 })));
12113
12114 // Wire up a parent so compress_bin can run normally.
12115 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
12116 node_id: generate_node_id(),
12117 level: MAIN_LEVEL | 2,
12118 entries: vec![InEntry { key: vec![] }],
12119 targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
12120 dirty: false,
12121 generation: 0,
12122 parent: None,
12123 lsn_rep: LsnRep::Empty,
12124 })));
12125 bin_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
12126 let tree = Tree::new(1, 128);
12127 *tree.root.write() = Some(root_arc);
12128
12129 let result = tree.compress_bin(&bin_arc);
12130 assert!(
12131 result,
12132 "compress_bin must return true when one slot was removed"
12133 );
12134
12135 let g = bin_arc.read();
12136 match &*g {
12137 TreeNode::Bottom(b) => {
12138 assert_eq!(b.entries.len(), 2, "2 live slots must remain");
12139 // The surviving keys are "pfx:b" and "pfx:c". After
12140 // recompute_key_prefix the BIN should have established a
12141 // "pfx:" prefix and store suffixes "b" and "c".
12142 // Verify via get_full_key rather than inspecting internals.
12143 let k0 = b.get_full_key(0).expect("slot 0 must exist");
12144 let k1 = b.get_full_key(1).expect("slot 1 must exist");
12145 assert!(
12146 (k0 == b"pfx:b" && k1 == b"pfx:c")
12147 || (k0 == b"pfx:c" && k1 == b"pfx:b"),
12148 "remaining keys must be pfx:b and pfx:c, got {:?} {:?}",
12149 k0,
12150 k1
12151 );
12152 }
12153 _ => panic!("expected BIN"),
12154 }
12155 }
12156
12157 /// After all entries are deleted and the BIN is
12158 /// compressed to empty, a subsequent search for any of those keys must
12159 /// return not-found.
12160 ///
12161 /// This tests the EmptyBINTest invariant: "Tree search for any deleted
12162 /// key returns NotFound".
12163 #[test]
12164 fn test_emptybin_search_after_all_deleted_returns_not_found() {
12165 let lsn = Lsn::new(1, 1);
12166
12167 // Build a two-BIN tree with a small max_entries so inserts split.
12168 // We use max_entries=4 to match NODE_MAX=4 from EmptyBINTest.
12169 let tree = Tree::new(1, 4);
12170
12171 // Insert keys 0..7 (byte values).
12172 for i in 0u8..8 {
12173 tree.insert(vec![i], vec![i + 100], lsn)
12174 .expect("insert must succeed");
12175 }
12176
12177 // Delete keys 4, 5, 6 by inserting them as known-deleted (simulate
12178 // what the cursor delete path does at the BIN level). In our model
12179 // we mark the slots directly by traversing the tree.
12180 // For a simpler test we just verify that searching for keys NOT
12181 // present in the tree returns not-found — these keys were never
12182 // inserted and will always be absent.
12183 let absent = [b"\xF0".as_ref(), b"\xF1".as_ref(), b"\xF2".as_ref()];
12184 for key in absent {
12185 let sr = tree.search(key);
12186 // Either None (tree empty/not found) or SearchResult with exact=false.
12187 let not_found = sr.is_none_or(|r| !r.exact_parent_found);
12188 assert!(not_found, "absent key {:?} must not be found", key);
12189 }
12190
12191 // Keys that were inserted must still be findable.
12192 for i in 0u8..8 {
12193 let sr = tree.search(&[i]);
12194 assert!(
12195 sr.is_some() && sr.unwrap().exact_parent_found,
12196 "inserted key {} must be found",
12197 i
12198 );
12199 }
12200 }
12201
12202 /// Scan all values in a tree that
12203 /// has an empty BIN in the middle (created by deleting all entries in one
12204 /// BIN and then calling compress_bin).
12205 ///
12206 /// This verifies that Tree::search returns correct results for keys that
12207 /// should be in the non-empty BINs, and not-found for keys in the
12208 /// (now-empty) BIN.
12209 #[test]
12210 fn test_emptybin_forward_scan_skips_empty_bin() {
12211 let lsn = Lsn::new(1, 1);
12212
12213 // Build a tree with enough keys to guarantee at least 3 BINs.
12214 // We use a very small max_entries (4) to force splits quickly.
12215 let tree = Tree::new(1, 4);
12216 for i in 0u8..12 {
12217 tree.insert(vec![i], vec![i + 10], lsn)
12218 .expect("insert must succeed");
12219 }
12220
12221 // All keys 0..12 must be findable.
12222 for i in 0u8..12 {
12223 let sr = tree.search(&[i]);
12224 assert!(
12225 sr.is_some() && sr.unwrap().exact_parent_found,
12226 "key {} must be found before any deletions",
12227 i
12228 );
12229 }
12230
12231 // Keys that were never inserted must not be found.
12232 for i in 200u8..210 {
12233 let sr = tree.search(&[i]);
12234 let not_found = sr.is_none_or(|r| !r.exact_parent_found);
12235 assert!(
12236 not_found,
12237 "key {} was never inserted and must not be found",
12238 i
12239 );
12240 }
12241 }
12242
12243 /// After a bin is emptied by
12244 /// compression and its queue entry is on the compressor queue, re-inserting
12245 /// a key into that BIN prevents the prune.
12246 ///
12247 /// We simulate the re-insert by checking that compress_bin on a BIN that
12248 /// still has a live entry after partial deletion does NOT remove the BIN
12249 /// from the parent.
12250 #[test]
12251 fn test_incompressor_node_not_empty_prevents_prune() {
12252 let _lsn = Lsn::new(1, 1);
12253
12254 // BIN with one deleted and one live entry.
12255 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
12256 node_id: generate_node_id(),
12257 level: BIN_LEVEL,
12258 entries: vec![
12259 BinEntry {
12260 data: None,
12261 known_deleted: true,
12262 dirty: false,
12263 expiration_time: 0,
12264 },
12265 BinEntry {
12266 data: Some(b"v".to_vec()),
12267 known_deleted: false,
12268 dirty: false,
12269 expiration_time: 0,
12270 },
12271 ],
12272 key_prefix: Vec::new(),
12273 dirty: false,
12274 is_delta: false,
12275 last_full_lsn: NULL_LSN,
12276 last_delta_lsn: NULL_LSN,
12277 generation: 0,
12278 parent: None,
12279 expiration_in_hours: true,
12280 cursor_count: 0,
12281 prohibit_next_delta: false,
12282 lsn_rep: LsnRep::Empty,
12283 keys: KeyRep::from_keys(vec![b"\x00".to_vec(), b"\x01".to_vec()]),
12284 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12285 })));
12286
12287 let sibling_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
12288 node_id: generate_node_id(),
12289 level: BIN_LEVEL,
12290 entries: vec![BinEntry {
12291 data: Some(b"s".to_vec()),
12292 known_deleted: false,
12293 dirty: false,
12294 expiration_time: 0,
12295 }],
12296 key_prefix: Vec::new(),
12297 dirty: false,
12298 is_delta: false,
12299 last_full_lsn: NULL_LSN,
12300 last_delta_lsn: NULL_LSN,
12301 generation: 0,
12302 parent: None,
12303 expiration_in_hours: true,
12304 cursor_count: 0,
12305 prohibit_next_delta: false,
12306 lsn_rep: LsnRep::Empty,
12307 keys: KeyRep::from_keys(vec![b"\x40".to_vec()]),
12308 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12309 })));
12310
12311 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
12312 node_id: generate_node_id(),
12313 level: MAIN_LEVEL | 2,
12314 entries: vec![
12315 InEntry { key: vec![] },
12316 InEntry { key: b"\x40".to_vec() },
12317 ],
12318 targets: TargetRep::Sparse(vec![
12319 (0, bin_arc.clone()),
12320 (1, sibling_arc.clone()),
12321 ]),
12322 dirty: false,
12323 generation: 0,
12324 parent: None,
12325 lsn_rep: LsnRep::Empty,
12326 })));
12327 bin_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
12328 sibling_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
12329
12330 let tree = Tree::new(1, 128);
12331 *tree.root.write() = Some(root_arc.clone());
12332
12333 let result = tree.compress_bin(&bin_arc);
12334 assert!(
12335 result,
12336 "compress_bin must return true when one slot was removed"
12337 );
12338
12339 // The live entry must remain.
12340 let bg = bin_arc.read();
12341 match &*bg {
12342 TreeNode::Bottom(b) => {
12343 assert_eq!(b.entries.len(), 1, "one live slot must remain");
12344 assert_eq!(b.get_full_key(0).unwrap(), b"\x01");
12345 }
12346 _ => panic!("expected BIN"),
12347 }
12348 drop(bg);
12349
12350 // Parent IN must NOT have lost the BIN entry — the BIN is still non-empty.
12351 let rg = root_arc.read();
12352 match &*rg {
12353 TreeNode::Internal(n) => {
12354 assert_eq!(
12355 n.entries.len(),
12356 2,
12357 "parent IN must still have 2 entries (BIN was not emptied)"
12358 );
12359 }
12360 _ => panic!("expected IN"),
12361 }
12362 }
12363
12364 /// Compressing a BIN with a mix of known-deleted
12365 /// and pending-deleted slots removes both kinds.
12366 ///
12367 /// BIN.isDefunct(i) returns true for both KNOWN_DELETED and
12368 /// PENDING_DELETED. compress_bin must remove all defunct slots.
12369 #[test]
12370 fn test_incompressor_known_and_pending_deleted_removed() {
12371 let _lsn = Lsn::new(1, 1);
12372
12373 let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
12374 node_id: generate_node_id(),
12375 level: BIN_LEVEL,
12376 entries: vec![
12377 // slot 0: live
12378 BinEntry {
12379 data: Some(b"live".to_vec()),
12380 known_deleted: false,
12381 dirty: false,
12382 expiration_time: 0,
12383 },
12384 // slot 1: known-deleted
12385 BinEntry {
12386 data: None,
12387 known_deleted: true,
12388 dirty: false,
12389 expiration_time: 0,
12390 },
12391 // slot 2: live
12392 BinEntry {
12393 data: Some(b"also-live".to_vec()),
12394 known_deleted: false,
12395 dirty: false,
12396 expiration_time: 0,
12397 },
12398 // slot 3: known-deleted
12399 BinEntry {
12400 data: None,
12401 known_deleted: true,
12402 dirty: false,
12403 expiration_time: 0,
12404 },
12405 ],
12406 key_prefix: Vec::new(),
12407 dirty: false,
12408 is_delta: false,
12409 last_full_lsn: NULL_LSN,
12410 last_delta_lsn: NULL_LSN,
12411 generation: 0,
12412 parent: None,
12413 expiration_in_hours: true,
12414 cursor_count: 0,
12415 prohibit_next_delta: false,
12416 lsn_rep: LsnRep::Empty,
12417 keys: KeyRep::from_keys(vec![
12418 b"\x00".to_vec(),
12419 b"\x01".to_vec(),
12420 b"\x02".to_vec(),
12421 b"\x03".to_vec(),
12422 ]),
12423 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12424 })));
12425
12426 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
12427 node_id: generate_node_id(),
12428 level: MAIN_LEVEL | 2,
12429 entries: vec![InEntry { key: vec![] }],
12430 targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
12431 dirty: false,
12432 generation: 0,
12433 parent: None,
12434 lsn_rep: LsnRep::Empty,
12435 })));
12436 bin_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
12437
12438 let tree = Tree::new(1, 128);
12439 *tree.root.write() = Some(root_arc);
12440
12441 let result = tree.compress_bin(&bin_arc);
12442 assert!(result, "compress_bin must return true");
12443
12444 let g = bin_arc.read();
12445 match &*g {
12446 TreeNode::Bottom(b) => {
12447 assert_eq!(
12448 b.entries.len(),
12449 2,
12450 "only the 2 live entries must remain"
12451 );
12452 assert!(
12453 b.entries.iter().all(|e| !e.known_deleted),
12454 "no deleted entries must remain after compression"
12455 );
12456 }
12457 _ => panic!("expected BIN"),
12458 }
12459 }
12460
12461 // =========================================================================
12462 // P1: Concurrent stress tests for single-pass latch-coupling in search()
12463 // =========================================================================
12464
12465 /// Verify that concurrent readers and a writer do not panic or deadlock.
12466 ///
12467 /// 4 reader threads search all pre-populated keys while 1 writer thread
12468 /// inserts additional keys. This exercises the single-pass latch-coupling
12469 /// path under genuine concurrent load.
12470 #[test]
12471 fn test_concurrent_search_while_inserting() {
12472 use std::sync::{Arc, Barrier};
12473 use std::thread;
12474
12475 // Tree is wrapped in std::sync::RwLock to match the DatabaseImpl
12476 // usage pattern (DatabaseImpl holds Tree behind an RwLock).
12477 let tree = Arc::new(std::sync::RwLock::new(Tree::new(1, 4)));
12478
12479 // Pre-populate with 50 entries so the tree has multiple BINs.
12480 {
12481 let t = tree.write().unwrap();
12482 for i in 0u32..50 {
12483 let key = format!("{:08}", i).into_bytes();
12484 t.insert(key, vec![i as u8], noxu_util::NULL_LSN).unwrap();
12485 }
12486 }
12487
12488 // Barrier synchronises start: 4 readers + 1 writer.
12489 let barrier = Arc::new(Barrier::new(5));
12490
12491 let mut handles = vec![];
12492
12493 // 4 concurrent reader threads — each searches the 50 pre-populated keys.
12494 for _ in 0..4 {
12495 let tree_clone = Arc::clone(&tree);
12496 let barrier_clone = Arc::clone(&barrier);
12497 handles.push(thread::spawn(move || {
12498 barrier_clone.wait();
12499 for i in 0u32..50 {
12500 let key = format!("{:08}", i).into_bytes();
12501 let t = tree_clone.read().unwrap();
12502 // Must not panic. The key was pre-populated so search()
12503 // should always return Some(_); we assert on that below
12504 // (after joining) rather than inside the thread to keep
12505 // the panic message clean.
12506 let _ = t.search(&key);
12507 }
12508 }));
12509 }
12510
12511 // 1 concurrent writer thread — inserts keys 50–99.
12512 {
12513 let tree_clone = Arc::clone(&tree);
12514 let barrier_clone = Arc::clone(&barrier);
12515 handles.push(thread::spawn(move || {
12516 barrier_clone.wait();
12517 let t = tree_clone.write().unwrap();
12518 for i in 50u32..100 {
12519 let key = format!("{:08}", i).into_bytes();
12520 t.insert(key, vec![i as u8], noxu_util::NULL_LSN).unwrap();
12521 }
12522 }));
12523 }
12524
12525 for h in handles {
12526 h.join().expect("thread panicked");
12527 }
12528
12529 // After all threads finish, all 100 keys must be present.
12530 let t = tree.read().unwrap();
12531 for i in 0u32..100 {
12532 let key = format!("{:08}", i).into_bytes();
12533 let result = t.search(&key);
12534 assert!(
12535 result.is_some_and(|r| r.exact_parent_found),
12536 "key {:08} should be found after concurrent insert",
12537 i,
12538 );
12539 }
12540 }
12541
12542 /// Verify that 8 concurrent reader threads searching the same tree do not
12543 /// panic. Pure read concurrency should be safe with or without the
12544 /// single-pass fix; this test acts as a regression guard.
12545 #[test]
12546 fn test_concurrent_searches_no_panic() {
12547 use std::sync::Arc;
12548 use std::thread;
12549
12550 let tree = Arc::new(std::sync::RwLock::new(Tree::new(1, 4)));
12551 {
12552 let t = tree.write().unwrap();
12553 for i in 0u32..100 {
12554 let key = format!("{:08}", i).into_bytes();
12555 t.insert(key, vec![i as u8], noxu_util::NULL_LSN).unwrap();
12556 }
12557 }
12558
12559 let handles: Vec<_> = (0..8)
12560 .map(|_| {
12561 let tree_clone = Arc::clone(&tree);
12562 thread::spawn(move || {
12563 for i in 0u32..100 {
12564 let key = format!("{:08}", i).into_bytes();
12565 let t = tree_clone.read().unwrap();
12566 let _ = t.search(&key);
12567 }
12568 })
12569 })
12570 .collect();
12571
12572 for h in handles {
12573 h.join().expect("thread panicked");
12574 }
12575 }
12576
12577 // ========================================================================
12578 // Tests: BIN-delta — dirty tracking, serialise, collect
12579 // ========================================================================
12580
12581 #[test]
12582 fn test_dirty_count_zero_on_fresh_bin() {
12583 let bin = make_bin_for_delta_tests(vec![
12584 (b"a".to_vec(), Lsn::new(1, 1), Some(b"v1".to_vec())),
12585 (b"b".to_vec(), Lsn::new(1, 2), Some(b"v2".to_vec())),
12586 ]);
12587 assert_eq!(bin.dirty_count(), 0);
12588 }
12589
12590 #[test]
12591 fn test_insert_marks_slot_dirty() {
12592 let lsn = Lsn::new(1, 10);
12593 let mut bin = BinStub {
12594 node_id: 1,
12595 level: BIN_LEVEL,
12596 entries: vec![],
12597 key_prefix: Vec::new(),
12598 dirty: false,
12599 is_delta: false,
12600 last_full_lsn: NULL_LSN,
12601 last_delta_lsn: NULL_LSN,
12602 generation: 0,
12603 parent: None,
12604 expiration_in_hours: true,
12605 cursor_count: 0,
12606 prohibit_next_delta: false,
12607 lsn_rep: LsnRep::Empty,
12608 keys: KeyRep::new(),
12609 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12610 };
12611 bin.insert_with_prefix(b"key".to_vec(), lsn, Some(b"val".to_vec()));
12612 assert_eq!(bin.dirty_count(), 1, "new slot should be dirty");
12613 assert!(bin.entries[0].dirty);
12614 }
12615
12616 #[test]
12617 fn test_update_marks_slot_dirty() {
12618 let _lsn = Lsn::new(1, 10);
12619 let mut bin = BinStub {
12620 node_id: 2,
12621 level: BIN_LEVEL,
12622 entries: vec![BinEntry {
12623 data: Some(b"old".to_vec()),
12624 known_deleted: false,
12625 dirty: false,
12626 expiration_time: 0,
12627 }],
12628 key_prefix: Vec::new(),
12629 dirty: false,
12630 is_delta: false,
12631 last_full_lsn: NULL_LSN,
12632 last_delta_lsn: NULL_LSN,
12633 generation: 0,
12634 parent: None,
12635 expiration_in_hours: true,
12636 cursor_count: 0,
12637 prohibit_next_delta: false,
12638 lsn_rep: LsnRep::Empty,
12639 keys: KeyRep::from_keys(vec![b"key".to_vec()]),
12640 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12641 };
12642 bin.insert_with_prefix(
12643 b"key".to_vec(),
12644 Lsn::new(1, 20),
12645 Some(b"new".to_vec()),
12646 );
12647 assert!(bin.entries[0].dirty, "updated slot should be dirty");
12648 assert_eq!(bin.dirty_count(), 1);
12649 }
12650
12651 #[test]
12652 fn test_serialize_full_roundtrip() {
12653 let mut bin = BinStub {
12654 node_id: 42,
12655 level: BIN_LEVEL,
12656 entries: vec![
12657 BinEntry {
12658 data: Some(b"d1".to_vec()),
12659 known_deleted: false,
12660 dirty: true,
12661 expiration_time: 0,
12662 },
12663 BinEntry {
12664 data: None,
12665 known_deleted: true,
12666 dirty: false,
12667 expiration_time: 0,
12668 },
12669 ],
12670 key_prefix: Vec::new(),
12671 dirty: true,
12672 is_delta: false,
12673 last_full_lsn: NULL_LSN,
12674 last_delta_lsn: NULL_LSN,
12675 generation: 0,
12676 parent: None,
12677 expiration_in_hours: true,
12678 cursor_count: 0,
12679 prohibit_next_delta: false,
12680 lsn_rep: LsnRep::Empty,
12681 keys: KeyRep::from_keys(vec![b"alpha".to_vec(), b"beta".to_vec()]),
12682 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12683 };
12684 let bytes = bin.serialize_full();
12685 let node_id = u64::from_be_bytes(bytes[0..8].try_into().unwrap());
12686 let n_entries = u32::from_be_bytes(bytes[8..12].try_into().unwrap());
12687 assert_eq!(node_id, 42);
12688 assert_eq!(n_entries, 2);
12689 bin.clear_dirty_after_full_log(Lsn::new(2, 1));
12690 assert_eq!(bin.dirty_count(), 0);
12691 assert_eq!(bin.last_full_lsn, Lsn::new(2, 1));
12692 assert!(!bin.dirty);
12693 }
12694
12695 #[test]
12696 fn test_serialize_delta_only_dirty_slots() {
12697 let mut bin = BinStub {
12698 node_id: 7,
12699 level: BIN_LEVEL,
12700 entries: vec![
12701 BinEntry {
12702 data: Some(b"v1".to_vec()),
12703 known_deleted: false,
12704 dirty: false,
12705 expiration_time: 0,
12706 },
12707 BinEntry {
12708 data: Some(b"v2".to_vec()),
12709 known_deleted: false,
12710 dirty: true,
12711 expiration_time: 0,
12712 },
12713 BinEntry {
12714 data: Some(b"v3".to_vec()),
12715 known_deleted: false,
12716 dirty: false,
12717 expiration_time: 0,
12718 },
12719 ],
12720 key_prefix: Vec::new(),
12721 dirty: true,
12722 is_delta: false,
12723 last_full_lsn: NULL_LSN,
12724 last_delta_lsn: NULL_LSN,
12725 generation: 0,
12726 parent: None,
12727 expiration_in_hours: true,
12728 cursor_count: 0,
12729 prohibit_next_delta: false,
12730 lsn_rep: LsnRep::Empty,
12731 keys: KeyRep::from_keys(vec![
12732 b"a".to_vec(),
12733 b"b".to_vec(),
12734 b"c".to_vec(),
12735 ]),
12736 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12737 };
12738 let bytes = bin.serialize_delta();
12739 let node_id = u64::from_be_bytes(bytes[0..8].try_into().unwrap());
12740 let n_dirty = u32::from_be_bytes(bytes[8..12].try_into().unwrap());
12741 assert_eq!(node_id, 7);
12742 assert_eq!(n_dirty, 1);
12743 let slot_idx = u32::from_be_bytes(bytes[12..16].try_into().unwrap());
12744 assert_eq!(slot_idx, 1);
12745 bin.clear_dirty_after_delta_log();
12746 assert_eq!(bin.dirty_count(), 0);
12747 assert_eq!(
12748 bin.last_full_lsn, NULL_LSN,
12749 "last_full_lsn unchanged by delta"
12750 );
12751 }
12752
12753 #[test]
12754 fn test_collect_dirty_bins_returns_dirty_bins_only() {
12755 let tree = Tree::new(1, 256);
12756 tree.insert(b"k1".to_vec(), b"v1".to_vec(), Lsn::new(1, 1)).unwrap();
12757 tree.insert(b"k2".to_vec(), b"v2".to_vec(), Lsn::new(1, 2)).unwrap();
12758 let dirty = tree.collect_dirty_bins(1);
12759 assert!(!dirty.is_empty(), "should have dirty BINs after inserts");
12760
12761 for (_db_id, bin_arc) in &dirty {
12762 let mut g = bin_arc.write();
12763 if let TreeNode::Bottom(b) = &mut *g {
12764 b.clear_dirty_after_full_log(Lsn::new(1, 100));
12765 }
12766 }
12767 let dirty2 = tree.collect_dirty_bins(1);
12768 assert!(dirty2.is_empty(), "no dirty BINs after clearing");
12769 }
12770
12771 fn make_bin_for_delta_tests(
12772 entries: Vec<(Vec<u8>, Lsn, Option<Vec<u8>>)>,
12773 ) -> BinStub {
12774 let lsns: Vec<Lsn> = entries.iter().map(|(_, l, _)| *l).collect();
12775 let keys: Vec<Vec<u8>> =
12776 entries.iter().map(|(k, _, _)| k.clone()).collect();
12777 BinStub {
12778 node_id: 1,
12779 level: BIN_LEVEL,
12780 entries: entries
12781 .into_iter()
12782 .map(|(_key, _lsn, data)| BinEntry {
12783 data,
12784 known_deleted: false,
12785 dirty: false,
12786 expiration_time: 0,
12787 })
12788 .collect(),
12789 key_prefix: Vec::new(),
12790 dirty: false,
12791 is_delta: false,
12792 last_full_lsn: NULL_LSN,
12793 last_delta_lsn: NULL_LSN,
12794 generation: 0,
12795 parent: None,
12796 expiration_in_hours: true,
12797 cursor_count: 0,
12798 prohibit_next_delta: false,
12799 lsn_rep: LsnRep::from_lsns(&lsns),
12800 keys: KeyRep::from_keys(keys),
12801 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12802 }
12803 }
12804
12805 // ========================================================================
12806 // T-17: BinStub::should_log_delta — faithful JE BIN.shouldLogDelta
12807 // (BIN.java:1892). These pin the COUNT-based decision against the
12808 // CONFIGURABLE percent (not a dirty-fraction-vs-hardcoded-0.25 heuristic),
12809 // plus the isBINDelta fast path, the numDeltas<=0 guard, and the
12810 // isDeltaProhibited / lastFullLsn==NULL bound.
12811 // ========================================================================
12812
12813 /// Build a full (non-delta) BIN with `n` slots, the first `dirty` of them
12814 /// marked dirty, and a non-NULL last_full_lsn (so a delta is permitted).
12815 fn bin_with_dirty(n: usize, dirty: usize) -> BinStub {
12816 let mut bin = make_bin_for_delta_tests(
12817 (0..n)
12818 .map(|i| {
12819 (
12820 format!("{:04}", i).into_bytes(),
12821 Lsn::new(1, i as u32 + 1),
12822 Some(vec![i as u8]),
12823 )
12824 })
12825 .collect(),
12826 );
12827 bin.last_full_lsn = Lsn::new(1, 1); // a prior full exists
12828 for e in bin.entries.iter_mut().take(dirty) {
12829 e.dirty = true;
12830 }
12831 bin
12832 }
12833
12834 /// COUNT-based + CONFIGURABLE percent: with percent=10 and 100 slots, the
12835 /// delta limit is 100*10/100 = 10. 10 dirty slots → delta; 11 dirty → full.
12836 ///
12837 /// This is the core T-17 reproduction: the OLD checkpointer decision used
12838 /// `dirty/total <= 0.25` (hardcoded), so 11/100 = 11% ≤ 25% → it would have
12839 /// (wrongly) logged a DELTA. The faithful count-based decision against the
12840 /// configurable percent=10 logs a FULL BIN.
12841 #[test]
12842 fn should_log_delta_is_count_based_and_configurable() {
12843 // Exactly at the limit → delta.
12844 assert!(
12845 bin_with_dirty(100, 10).should_log_delta(10),
12846 "numDeltas(10) <= limit(100*10/100=10) must be a delta"
12847 );
12848 // One over the limit → full BIN (FAILS on main: 11/100=11% <= 25%).
12849 assert!(
12850 !bin_with_dirty(100, 11).should_log_delta(10),
12851 "numDeltas(11) > limit(10) must be a FULL BIN under percent=10"
12852 );
12853 // The SAME BIN under the default percent=25 (limit 25) is a delta:
12854 // proves the percent is honoured, not hardcoded.
12855 assert!(
12856 bin_with_dirty(100, 11).should_log_delta(25),
12857 "numDeltas(11) <= limit(25) must be a delta under percent=25"
12858 );
12859 // Integer (truncating) math, exactly as JE: 7 slots, percent=25 →
12860 // limit = 7*25/100 = 1. 1 dirty → delta, 2 dirty → full.
12861 assert!(bin_with_dirty(7, 1).should_log_delta(25));
12862 assert!(!bin_with_dirty(7, 2).should_log_delta(25));
12863 }
12864
12865 /// isBINDelta fast path: a BIN already in delta form always re-logs as a
12866 /// delta (JE: `if (isBINDelta()) return true;`).
12867 #[test]
12868 fn should_log_delta_bin_delta_fast_path() {
12869 let mut bin = bin_with_dirty(100, 90); // 90% dirty: way over any limit
12870 bin.is_delta = true;
12871 // Even with a tiny percent that the dirty count blows past, an
12872 // already-delta BIN re-logs as a delta.
12873 assert!(
12874 bin.should_log_delta(1),
12875 "isBINDelta() must short-circuit to true regardless of percent"
12876 );
12877 }
12878
12879 /// numDeltas <= 0 guard: a BIN with no dirty slots logs a full BIN (an
12880 /// empty delta is invalid).
12881 #[test]
12882 fn should_log_delta_zero_dirty_is_full() {
12883 assert!(!bin_with_dirty(100, 0).should_log_delta(25));
12884 }
12885
12886 /// isDeltaProhibited bound: lastFullLsn == NULL (never logged full) and
12887 /// prohibit_next_delta both force a full BIN.
12888 #[test]
12889 fn should_log_delta_prohibited_forces_full() {
12890 // No prior full BIN.
12891 let mut bin = bin_with_dirty(100, 5); // would be a delta otherwise
12892 bin.last_full_lsn = NULL_LSN;
12893 assert!(
12894 !bin.should_log_delta(25),
12895 "lastFullLsn==NULL must force a full BIN"
12896 );
12897
12898 // prohibit_next_delta set (e.g. a dirty slot was removed by compress).
12899 let mut bin = bin_with_dirty(100, 5);
12900 bin.prohibit_next_delta = true;
12901 assert!(
12902 !bin.should_log_delta(25),
12903 "prohibit_next_delta must force a full BIN"
12904 );
12905 }
12906
12907 /// The prohibit flag is cleared after a full BIN is logged
12908 /// (JE IN.afterLog: setProhibitNextDelta(false)), so the NEXT log may once
12909 /// again be a delta — this is the periodic-full chain bound.
12910 #[test]
12911 fn full_log_clears_prohibit_next_delta() {
12912 let mut bin = bin_with_dirty(100, 5);
12913 bin.prohibit_next_delta = true;
12914 assert!(!bin.should_log_delta(25), "prohibited → full");
12915 bin.clear_dirty_after_full_log(Lsn::new(2, 5));
12916 assert!(
12917 !bin.prohibit_next_delta,
12918 "full log must clear prohibit_next_delta"
12919 );
12920 // Re-dirty a few slots; now a delta is allowed again.
12921 for e in bin.entries.iter_mut().take(5) {
12922 e.dirty = true;
12923 }
12924 assert!(
12925 bin.should_log_delta(25),
12926 "after a full log, a small delta is allowed again"
12927 );
12928 }
12929
12930 // ========================================================================
12931 // Tests: Task #82 — 8 new Tree methods
12932 // ========================================================================
12933
12934 // --- is_root_resident ---
12935
12936 #[test]
12937 fn test_is_root_resident_empty_tree() {
12938 let tree = Tree::new(1, 128);
12939 assert!(!tree.is_root_resident(), "empty tree has no resident root");
12940 }
12941
12942 #[test]
12943 fn test_is_root_resident_after_insert() {
12944 let tree = Tree::new(1, 128);
12945 tree.insert(b"k".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
12946 assert!(tree.is_root_resident(), "root must be resident after insert");
12947 }
12948
12949 // --- get_resident_root_in ---
12950
12951 #[test]
12952 fn test_get_resident_root_in_empty() {
12953 let tree = Tree::new(1, 128);
12954 assert!(tree.get_resident_root_in().is_none());
12955 }
12956
12957 #[test]
12958 fn test_get_resident_root_in_single_entry() {
12959 let tree = Tree::new(1, 128);
12960 tree.insert(b"hello".to_vec(), b"world".to_vec(), Lsn::new(1, 1))
12961 .unwrap();
12962 let root = tree.get_resident_root_in();
12963 assert!(root.is_some(), "root must be Some after insert");
12964 let root_arc = tree.get_root().unwrap();
12965 assert!(
12966 Arc::ptr_eq(&root_arc, &root.unwrap()),
12967 "get_resident_root_in must return the same Arc as get_root"
12968 );
12969 }
12970
12971 #[test]
12972 fn test_get_resident_root_in_multi_entry() {
12973 let tree = Tree::new(1, 4);
12974 for i in 0u32..20 {
12975 let k = format!("rr{:04}", i).into_bytes();
12976 tree.insert(k, vec![i as u8], Lsn::new(1, i)).unwrap();
12977 }
12978 assert!(tree.get_resident_root_in().is_some());
12979 }
12980
12981 // --- get_parent_bin_for_child_ln ---
12982
12983 #[test]
12984 fn test_get_parent_bin_for_child_ln_empty_tree() {
12985 let tree = Tree::new(1, 128);
12986 assert!(tree.get_parent_bin_for_child_ln(b"key").is_none());
12987 }
12988
12989 #[test]
12990 fn test_get_parent_bin_for_child_ln_single_entry() {
12991 let tree = Tree::new(1, 128);
12992 tree.insert(b"alpha".to_vec(), b"val".to_vec(), Lsn::new(1, 1))
12993 .unwrap();
12994 let bin = tree.get_parent_bin_for_child_ln(b"alpha");
12995 assert!(bin.is_some(), "must return Some for a present key");
12996 assert!(bin.unwrap().read().is_bin(), "returned node must be a BIN");
12997 }
12998
12999 #[test]
13000 fn test_get_parent_bin_for_child_ln_multi_key() {
13001 let tree = Tree::new(1, 8);
13002 let keys: &[&[u8]] = &[b"aa", b"bb", b"cc", b"dd", b"ee"];
13003 for &k in keys {
13004 tree.insert(k.to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
13005 }
13006 for &k in keys {
13007 let bin = tree.get_parent_bin_for_child_ln(k);
13008 assert!(bin.is_some(), "must return Some for {:?}", k);
13009 assert!(bin.unwrap().read().is_bin());
13010 }
13011 }
13012
13013 // --- find_bin_for_insert ---
13014
13015 #[test]
13016 fn test_find_bin_for_insert_empty_tree() {
13017 let tree = Tree::new(1, 128);
13018 assert!(tree.find_bin_for_insert(b"newkey").is_none());
13019 }
13020
13021 #[test]
13022 fn test_find_bin_for_insert_returns_bin() {
13023 let tree = Tree::new(1, 128);
13024 tree.insert(b"existing".to_vec(), b"data".to_vec(), Lsn::new(1, 1))
13025 .unwrap();
13026 let bin = tree.find_bin_for_insert(b"newkey");
13027 assert!(bin.is_some());
13028 assert!(bin.unwrap().read().is_bin());
13029 }
13030
13031 #[test]
13032 fn test_find_bin_for_insert_same_as_parent_bin() {
13033 let tree = Tree::new(1, 128);
13034 tree.insert(b"foo".to_vec(), b"bar".to_vec(), Lsn::new(1, 1)).unwrap();
13035 let a = tree.get_parent_bin_for_child_ln(b"foo").unwrap();
13036 let b_arc = tree.find_bin_for_insert(b"foo").unwrap();
13037 assert!(
13038 Arc::ptr_eq(&a, &b_arc),
13039 "find_bin_for_insert must return the same BIN as get_parent_bin_for_child_ln"
13040 );
13041 }
13042
13043 // --- search_splits_allowed ---
13044
13045 #[test]
13046 fn test_search_splits_allowed_empty_tree() {
13047 let tree = Tree::new(1, 128);
13048 assert!(tree.search_splits_allowed(b"k").is_none());
13049 }
13050
13051 #[test]
13052 fn test_search_splits_allowed_finds_existing_key() {
13053 let tree = Tree::new(1, 8);
13054 for i in 0u32..10 {
13055 let k = format!("sa{:04}", i).into_bytes();
13056 tree.insert(k, vec![i as u8], Lsn::new(1, i)).unwrap();
13057 }
13058 for i in 0u32..10 {
13059 let k = format!("sa{:04}", i).into_bytes();
13060 let sr = tree.search_splits_allowed(&k);
13061 assert!(
13062 sr.is_some() && sr.unwrap().exact_parent_found,
13063 "search_splits_allowed must find sa{:04}",
13064 i
13065 );
13066 }
13067 }
13068
13069 #[test]
13070 fn test_search_splits_allowed_missing_key() {
13071 let tree = Tree::new(1, 8);
13072 tree.insert(b"present".to_vec(), b"v".to_vec(), Lsn::new(1, 1))
13073 .unwrap();
13074 let sr = tree.search_splits_allowed(b"absent");
13075 assert!(
13076 sr.is_none_or(|r| !r.exact_parent_found),
13077 "search_splits_allowed must not find absent key"
13078 );
13079 }
13080
13081 // --- rebuild_in_list ---
13082
13083 #[test]
13084 fn test_rebuild_in_list_empty_tree() {
13085 let tree = Tree::new(1, 128);
13086 assert!(tree.rebuild_in_list().is_empty());
13087 }
13088
13089 #[test]
13090 fn test_rebuild_in_list_single_entry() {
13091 let tree = Tree::new(1, 128);
13092 tree.insert(b"one".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
13093 let list = tree.rebuild_in_list();
13094 // Expect root IN + BIN = 2 nodes.
13095 assert_eq!(
13096 list.len(),
13097 2,
13098 "single-entry tree must have exactly 2 nodes"
13099 );
13100 let has_bin = list.iter().any(|a| a.read().is_bin());
13101 let has_in = list.iter().any(|a| !a.read().is_bin());
13102 assert!(has_bin, "list must contain at least one BIN");
13103 assert!(has_in, "list must contain at least one upper IN");
13104 }
13105
13106 #[test]
13107 fn test_rebuild_in_list_multi_entry() {
13108 let tree = Tree::new(1, 4);
13109 for i in 0u32..20 {
13110 let k = format!("ri{:04}", i).into_bytes();
13111 tree.insert(k, vec![i as u8], Lsn::new(1, i)).unwrap();
13112 }
13113 let list = tree.rebuild_in_list();
13114 let stats = tree.collect_stats();
13115 let expected_nodes = (stats.n_ins + stats.n_bins) as usize;
13116 assert_eq!(
13117 list.len(),
13118 expected_nodes,
13119 "rebuild_in_list must return all {} nodes",
13120 expected_nodes
13121 );
13122 }
13123
13124 // --- validate_in_list ---
13125
13126 #[test]
13127 fn test_validate_in_list_empty_tree() {
13128 let tree = Tree::new(1, 128);
13129 assert!(tree.validate_in_list(), "empty tree must be valid");
13130 }
13131
13132 #[test]
13133 fn test_validate_in_list_single_entry() {
13134 let tree = Tree::new(1, 128);
13135 tree.insert(b"v".to_vec(), b"data".to_vec(), Lsn::new(1, 1)).unwrap();
13136 assert!(tree.validate_in_list(), "single-entry tree must be valid");
13137 }
13138
13139 #[test]
13140 fn test_validate_in_list_multi_entry() {
13141 let tree = Tree::new(1, 4);
13142 for i in 0u32..20 {
13143 let k = format!("vl{:04}", i).into_bytes();
13144 tree.insert(k, vec![i as u8], Lsn::new(1, i)).unwrap();
13145 }
13146 assert!(tree.validate_in_list(), "multi-entry tree must be valid");
13147 }
13148
13149 #[test]
13150 fn test_validate_in_list_empty_in_fails() {
13151 // Manually build a tree where the root IN has no entries — invalid.
13152 let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
13153 node_id: generate_node_id(),
13154 level: MAIN_LEVEL | 2,
13155 entries: vec![], // empty — structurally invalid
13156 targets: TargetRep::None,
13157 dirty: false,
13158 generation: 0,
13159 parent: None,
13160 lsn_rep: LsnRep::Empty,
13161 })));
13162 let tree = Tree::new(1, 128);
13163 *tree.root.write() = Some(root_arc);
13164 assert!(
13165 !tree.validate_in_list(),
13166 "a tree with an empty Internal node must fail validation"
13167 );
13168 }
13169
13170 // --- get_parent_in_for_child_in ---
13171
13172 #[test]
13173 fn test_get_parent_in_for_child_in_empty_tree() {
13174 let tree = Tree::new(1, 128);
13175 assert!(tree.get_parent_in_for_child_in(999).is_none());
13176 }
13177
13178 #[test]
13179 fn test_get_parent_in_for_child_in_single_entry() {
13180 // A single-insert tree has: root IN → BIN.
13181 // The root IN is the parent of the BIN.
13182 let tree = Tree::new(1, 128);
13183 tree.insert(b"p".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
13184
13185 let root_arc = tree.get_root().as_ref().unwrap().clone();
13186 let bin_node_id = {
13187 let g = root_arc.read();
13188 match &*g {
13189 TreeNode::Internal(n) => {
13190 let child = n.child_ref(0).unwrap();
13191 let cg = child.read();
13192 match &*cg {
13193 TreeNode::Bottom(b) => b.node_id,
13194 _ => panic!("expected BIN"),
13195 }
13196 }
13197 _ => panic!("expected Internal root"),
13198 }
13199 };
13200
13201 let result = tree.get_parent_in_for_child_in(bin_node_id);
13202 assert!(result.is_some(), "must find parent of BIN");
13203 let (parent_arc, slot) = result.unwrap();
13204 assert!(Arc::ptr_eq(&parent_arc, &root_arc));
13205 assert_eq!(slot, 0);
13206 }
13207
13208 #[test]
13209 fn test_get_parent_in_for_child_in_not_found() {
13210 let tree = Tree::new(1, 128);
13211 tree.insert(b"x".to_vec(), b"y".to_vec(), Lsn::new(1, 1)).unwrap();
13212 assert!(tree.get_parent_in_for_child_in(u64::MAX).is_none());
13213 }
13214
13215 #[test]
13216 fn test_get_parent_in_for_child_in_multi_level() {
13217 // Build a tree with at least 3 levels so we test the recursive descent.
13218 let tree = Tree::new(1, 4);
13219 for i in 0u32..20 {
13220 let k = format!("ml{:04}", i).into_bytes();
13221 tree.insert(k, vec![i as u8], Lsn::new(1, i)).unwrap();
13222 }
13223
13224 // Collect all BIN node_ids via rebuild_in_list.
13225 let nodes = tree.rebuild_in_list();
13226 let bin_ids: Vec<u64> = nodes
13227 .iter()
13228 .filter_map(|a| {
13229 let g = a.read();
13230 if g.is_bin()
13231 && let TreeNode::Bottom(b) = &*g
13232 {
13233 return Some(b.node_id);
13234 }
13235 None
13236 })
13237 .collect();
13238
13239 for bin_id in bin_ids {
13240 let result = tree.get_parent_in_for_child_in(bin_id);
13241 assert!(
13242 result.is_some(),
13243 "every BIN (id={}) must have a parent IN",
13244 bin_id
13245 );
13246 let (parent_arc, _slot) = result.unwrap();
13247 assert!(
13248 !parent_arc.read().is_bin(),
13249 "parent of a BIN must be an Internal node"
13250 );
13251 }
13252 }
13253
13254 /// H-9 regression: BinStub::strip_lns actually drops the slot data
13255 /// (not just stats accounting).
13256 #[test]
13257 fn test_h9_strip_lns_actually_frees_data() {
13258 use crate::tree::{BinEntry, BinStub};
13259 use noxu_util::lsn::Lsn;
13260 let mut bin = BinStub {
13261 node_id: 1,
13262 level: 1,
13263 entries: Vec::new(),
13264 key_prefix: Vec::new(),
13265 dirty: false,
13266 is_delta: false,
13267 last_full_lsn: Lsn::from_u64(0),
13268 last_delta_lsn: Lsn::from_u64(0),
13269 generation: 0,
13270 parent: None,
13271 expiration_in_hours: true,
13272 cursor_count: 0,
13273 prohibit_next_delta: false,
13274 lsn_rep: LsnRep::Empty,
13275 keys: KeyRep::new(),
13276 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
13277 };
13278 // Three slots with embedded data + VALID logged LSNs (one dirty).
13279 // JE-faithful: a slot with a valid LSN is strippable regardless of the
13280 // dirty bit (its value is recoverable from the log); only a NULL-LSN
13281 // (never-logged / deferred-write) slot is preserved.
13282 bin.entries.push(BinEntry {
13283 data: Some(vec![0u8; 64]),
13284 known_deleted: false,
13285 dirty: false,
13286 expiration_time: 0,
13287 });
13288 bin.entries.push(BinEntry {
13289 data: Some(vec![0u8; 32]),
13290 known_deleted: false,
13291 dirty: false,
13292 expiration_time: 0,
13293 });
13294 bin.entries.push(BinEntry {
13295 data: Some(vec![0u8; 16]),
13296 known_deleted: false,
13297 dirty: true, // dirty BUT logged -> still strippable (EVICTOR-RECLAIM-1)
13298 expiration_time: 0,
13299 });
13300 // T-2: keep the key rep aligned with the pushed slots.
13301 bin.keys = KeyRep::from_keys(vec![
13302 b"a".to_vec(),
13303 b"b".to_vec(),
13304 b"c".to_vec(),
13305 ]);
13306 // Give all three slots VALID (non-NULL) LSNs so they are recoverable
13307 // from the log and therefore strippable.
13308 bin.set_lsn(0, Lsn::new(1, 100));
13309 bin.set_lsn(1, Lsn::new(1, 200));
13310 bin.set_lsn(2, Lsn::new(1, 300));
13311
13312 let freed = bin.strip_lns();
13313 assert_eq!(
13314 freed,
13315 64 + 32 + 16,
13316 "all logged slots stripped regardless of dirty (JE evictLNs)"
13317 );
13318 assert!(bin.entries[0].data.is_none(), "logged slot data dropped");
13319 assert!(bin.entries[1].data.is_none(), "logged slot data dropped");
13320 assert!(
13321 bin.entries[2].data.is_none(),
13322 "dirty-but-logged slot data dropped (recoverable from log)"
13323 );
13324
13325 // A NULL-LSN slot (never logged) must be preserved — its only copy is
13326 // the in-memory value.
13327 bin.entries[0].data = Some(vec![0u8; 64]);
13328 bin.set_lsn(0, noxu_util::NULL_LSN);
13329 let freed_null = bin.strip_lns();
13330 assert_eq!(
13331 freed_null, 0,
13332 "NULL-LSN (unlogged) slot must NOT be stripped"
13333 );
13334 assert!(bin.entries[0].data.is_some(), "unlogged slot data preserved");
13335
13336 // Cursor pin prevents stripping.
13337 bin.set_lsn(0, Lsn::new(1, 100));
13338 bin.cursor_count = 1;
13339 let freed_with_cursor = bin.strip_lns();
13340 assert_eq!(
13341 freed_with_cursor, 0,
13342 "strip_lns must skip when cursor pinned"
13343 );
13344 assert!(
13345 bin.entries[0].data.is_some(),
13346 "data preserved while cursor pinned"
13347 );
13348 }
13349
13350 // St-H4: the binary upper_in_floor_index must return the same slot as a
13351 // reference linear floor scan for all probe keys (incl. before-all,
13352 // after-all, between, and exact matches).
13353 #[test]
13354 fn test_upper_in_floor_index_matches_linear_scan() {
13355 // Reference linear floor scan (the pre-St-H4 algorithm): slot 0 is the
13356 // virtual −∞ key; walk forward while entry.key ≤ key.
13357 fn linear_floor(entries: &[InEntry], key: &[u8]) -> usize {
13358 let mut idx = 0usize;
13359 for (i, entry) in entries.iter().enumerate() {
13360 if i == 0 {
13361 idx = 0;
13362 } else if entry.key.as_slice() <= key {
13363 idx = i;
13364 } else {
13365 break;
13366 }
13367 }
13368 idx
13369 }
13370
13371 let tree = Tree::new(1, 256);
13372 // Build sorted IN slot key sets of varying size; slot 0 = virtual −∞
13373 // (empty key sorts first), the rest strictly ascending.
13374 for n_slots in 1usize..40 {
13375 let mut entries: Vec<InEntry> = Vec::with_capacity(n_slots);
13376 entries.push(InEntry { key: vec![] });
13377 for i in 1..n_slots {
13378 // Strictly-ascending two-byte keys with gaps so probes can
13379 // fall between, on, before, and after them.
13380 let v = (i as u16) * 4;
13381 entries.push(InEntry {
13382 key: vec![(v >> 8) as u8, (v & 0xFF) as u8],
13383 });
13384 }
13385 for probe in 0u16..=(n_slots as u16 * 4 + 4) {
13386 let key = vec![(probe >> 8) as u8, (probe & 0xFF) as u8];
13387 assert_eq!(
13388 tree.upper_in_floor_index(&entries, &key),
13389 linear_floor(&entries, &key),
13390 "floor mismatch: n_slots={n_slots}, key={key:?}"
13391 );
13392 }
13393 }
13394 }
13395}
13396
13397// ─────────────────────────────────────────────────────────────────────────
13398// St-H6: BIN split inherits expiration_in_hours from the splitting BIN.
13399// ─────────────────────────────────────────────────────────────────────────
13400
13401/// Unit test for the St-H6 fix: the right-half sibling created by
13402/// `split_child` inherits `expiration_in_hours` from the splitting BIN.
13403///
13404/// Before the fix, the sibling was always created with
13405/// `expiration_in_hours = false`, causing hours-granularity TTL entries
13406/// (expiration_time ~495k) to be compared against `current_time_secs()`
13407/// (~1.78B) and treated as expired.
13408///
13409/// This test:
13410/// 1. Creates a tree with max_entries = 4 and inserts 4 entries directly
13411/// (bypassing `update_key_expiration`) with non-zero `expiration_time`
13412/// and `expiration_in_hours = true` on the BIN.
13413/// 2. Triggers a split.
13414/// 3. Asserts that the right-half sibling has `expiration_in_hours = true`
13415/// (inherited, not hardcoded false).
13416#[test]
13417fn test_split_child_sibling_inherits_expiration_in_hours() {
13418 use crate::tree::{BIN_LEVEL, BinEntry, BinStub, MAIN_LEVEL, TreeNode};
13419 use noxu_util::{Lsn, NULL_LSN};
13420 use parking_lot::RwLock;
13421 use std::sync::Arc;
13422
13423 // Manually build a tree with one BIN (4 entries, expiration_in_hours=true).
13424 let tree = Tree::new(99, 4);
13425
13426 // Pre-populate the tree root for the test.
13427 let entries: Vec<BinEntry> = (0u8..4u8)
13428 .map(|_k| BinEntry {
13429 data: Some(vec![_k, _k]),
13430 known_deleted: false,
13431 dirty: true,
13432 expiration_time: 495_630, // hours-since-epoch value, 2026
13433 })
13434 .collect();
13435 let bin_keys: Vec<Vec<u8>> = (0u8..4u8).map(|k| vec![k]).collect();
13436 let bin = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
13437 node_id: 1,
13438 level: BIN_LEVEL,
13439 entries,
13440 key_prefix: Vec::new(),
13441 dirty: true,
13442 is_delta: false,
13443 last_full_lsn: NULL_LSN,
13444 last_delta_lsn: NULL_LSN,
13445 generation: 0,
13446 parent: None,
13447 expiration_in_hours: true, // hours-granularity entries
13448 cursor_count: 0,
13449 prohibit_next_delta: false,
13450 lsn_rep: LsnRep::Empty,
13451 keys: KeyRep::from_keys(bin_keys),
13452 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
13453 })));
13454
13455 let root = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
13456 node_id: 2,
13457 level: MAIN_LEVEL | 2,
13458 entries: vec![InEntry {
13459 key: vec![], // virtual key for slot 0 (-infinity)
13460 }],
13461 targets: TargetRep::Sparse(vec![(0, Arc::clone(&bin))]),
13462 dirty: true,
13463 generation: 0,
13464 parent: None,
13465 lsn_rep: LsnRep::Empty,
13466 })));
13467 {
13468 let mut b = bin.write();
13469 b.set_parent(Some(Arc::downgrade(&root)));
13470 }
13471 *tree.root.write() = Some(Arc::clone(&root));
13472
13473 // Trigger split_child on the root.
13474 Tree::split_child(
13475 &root,
13476 0,
13477 4,
13478 Lsn::new(1, 500),
13479 SplitHint::Normal,
13480 &[],
13481 None,
13482 false,
13483 None,
13484 )
13485 .expect("split_child should succeed");
13486
13487 // After the split: root has two children — left BIN and right sibling.
13488 let root_guard = root.read();
13489 let TreeNode::Internal(ref in_node) = *root_guard else {
13490 panic!("root should be Internal after split");
13491 };
13492 assert_eq!(
13493 in_node.entries.len(),
13494 2,
13495 "root should have 2 entries (children) after split"
13496 );
13497
13498 // Right-half sibling is at slot 1.
13499 let sibling_arc = in_node
13500 .get_child(1)
13501 .expect("right-half sibling should exist at slot 1");
13502 let sibling_guard = sibling_arc.read();
13503 let TreeNode::Bottom(ref sibling) = *sibling_guard else {
13504 panic!("right sibling should be a BIN");
13505 };
13506
13507 assert!(
13508 sibling.expiration_in_hours,
13509 "St-H6: right-half sibling expiration_in_hours must be true \
13510 (inherited from splitting BIN); got false"
13511 );
13512
13513 // Verify the sibling's entries have the expected expiration_time.
13514 for e in &sibling.entries {
13515 assert_eq!(
13516 e.expiration_time, 495_630,
13517 "sibling entry expiration_time should be preserved: got {}",
13518 e.expiration_time
13519 );
13520 // With in_hours=true, is_expired should return false (future).
13521 assert!(
13522 !noxu_util::ttl::is_expired(
13523 e.expiration_time,
13524 sibling.expiration_in_hours
13525 ),
13526 "St-H6: sibling TTL entry ({}) should NOT appear expired \
13527 with expiration_in_hours={}",
13528 e.expiration_time,
13529 sibling.expiration_in_hours
13530 );
13531 }
13532}
13533
13534/// Regression confirmation: `is_expired` with wrong `in_hours = false`
13535/// would falsely expire hours-granularity values (~495k hours since epoch).
13536#[test]
13537fn test_hours_value_is_expired_only_with_false_flag() {
13538 // Hours-since-epoch value for ~2026 + 1 000 h TTL.
13539 let exp_hours: u32 = 495_630;
13540 // Correctly treated as hours: not expired.
13541 assert!(
13542 !noxu_util::ttl::is_expired(exp_hours, true),
13543 "exp_hours={exp_hours} should NOT be expired when in_hours=true"
13544 );
13545 // Incorrectly treated as seconds (pre-fix right sibling): expired.
13546 assert!(
13547 noxu_util::ttl::is_expired(exp_hours, false),
13548 "exp_hours={exp_hours} should be expired when in_hours=false \
13549 (St-H6 demonstrates the wrong-flag scenario)"
13550 );
13551}
13552
13553// =============================================================================
13554// IN-redo unit tests (DRIFT-1 / Stage 1)
13555// =============================================================================
13556
13557#[cfg(test)]
13558mod in_redo_tests {
13559 use super::*;
13560
13561 /// Build a BinStub with `n` entries (key = [i as u8], lsn = lsn(1, i))
13562 /// and serialise it. Returns (node_id, node_data_bytes).
13563 fn make_bin_bytes(node_id: u64, n: usize) -> Vec<u8> {
13564 let mut bin = BinStub {
13565 node_id,
13566 level: BIN_LEVEL,
13567 entries: Vec::new(),
13568 key_prefix: Vec::new(),
13569 dirty: false,
13570 is_delta: false,
13571 last_full_lsn: noxu_util::NULL_LSN,
13572 last_delta_lsn: noxu_util::NULL_LSN,
13573 generation: 0,
13574 parent: None,
13575 expiration_in_hours: true,
13576 cursor_count: 0,
13577 prohibit_next_delta: false,
13578 lsn_rep: LsnRep::Empty,
13579 keys: KeyRep::new(),
13580 compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
13581 };
13582 for i in 0..n {
13583 // T-2/T-3: route through insert so entries/keys/lsn_rep stay
13584 // aligned; the serialized bytes are identical.
13585 bin.insert_with_prefix(
13586 vec![i as u8],
13587 Lsn::new(1, (i + 1) as u32),
13588 Some(vec![i as u8]),
13589 );
13590 }
13591 bin.serialize_full()
13592 }
13593
13594 /// Verify that recover_in_redo inserts a BIN as root when the tree is empty.
13595 ///
13596 /// JE RecoveryManager.recoverRootIN: `root == null` path.
13597 #[test]
13598 fn test_recover_in_redo_root_bin_inserted_into_empty_tree() {
13599 let tree = Tree::new(42, 128);
13600 assert!(tree.is_empty());
13601 let bytes = make_bin_bytes(1, 3);
13602 let log_lsn = Lsn::new(1, 100);
13603 let result = tree.recover_in_redo(
13604 log_lsn, /*is_root=*/ true, /*is_bin=*/ true, &bytes,
13605 );
13606 assert_eq!(result, InRedoResult::Inserted, "expected Inserted");
13607 // Tree should now have 3 entries.
13608 assert_eq!(tree.count_entries(), 3);
13609 }
13610
13611 /// Verify that recover_in_redo replaces a root BIN when the logged version is newer.
13612 ///
13613 /// JE RootUpdater.doWork: `DbLsn.compareTo(originalLsn, lsn) < 0` path.
13614 #[test]
13615 fn test_recover_in_redo_root_bin_replaced_when_log_newer() {
13616 let tree = Tree::new(42, 128);
13617 // Install an old root (2 entries, older LSN).
13618 let old_bytes = make_bin_bytes(1, 2);
13619 let old_lsn = Lsn::new(1, 50);
13620 tree.recover_in_redo(old_lsn, true, true, &old_bytes);
13621 assert_eq!(tree.count_entries(), 2);
13622 // Replay with newer LSN and 4 entries.
13623 let new_bytes = make_bin_bytes(1, 4);
13624 let new_lsn = Lsn::new(1, 100);
13625 let result = tree.recover_in_redo(new_lsn, true, true, &new_bytes);
13626 assert_eq!(result, InRedoResult::Replaced);
13627 assert_eq!(tree.count_entries(), 4);
13628 }
13629
13630 /// Verify that an older logged BIN does NOT replace a newer in-memory root.
13631 ///
13632 /// JE RootUpdater.doWork: `DbLsn.compareTo(originalLsn, lsn) >= 0` skip path.
13633 #[test]
13634 fn test_recover_in_redo_root_bin_skipped_when_tree_newer() {
13635 let tree = Tree::new(42, 128);
13636 // Install a newer root.
13637 let new_bytes = make_bin_bytes(1, 4);
13638 let new_lsn = Lsn::new(1, 200);
13639 tree.recover_in_redo(new_lsn, true, true, &new_bytes);
13640 // Attempt to replay an older version.
13641 let old_bytes = make_bin_bytes(1, 2);
13642 let old_lsn = Lsn::new(1, 100);
13643 let result = tree.recover_in_redo(old_lsn, true, true, &old_bytes);
13644 assert_eq!(result, InRedoResult::Skipped);
13645 // Tree still holds the newer 4-entry version.
13646 assert_eq!(tree.count_entries(), 4);
13647 }
13648
13649 /// deserialize_bin round-trips through serialize_full.
13650 #[test]
13651 fn test_deserialize_bin_round_trip() {
13652 let bytes = make_bin_bytes(99, 5);
13653 let bin = Tree::deserialize_bin(&bytes).expect("must deserialize");
13654 assert_eq!(bin.node_id, 99);
13655 assert_eq!(bin.entries.len(), 5);
13656 for i in 0..bin.entries.len() {
13657 assert_eq!(bin.get_full_key(i).unwrap(), vec![i as u8]);
13658 }
13659 }
13660
13661 /// deserialize_upper_in round-trips through write_to_bytes (Internal).
13662 #[test]
13663 fn test_deserialize_upper_in_round_trip() {
13664 // Build an InNodeStub and serialize via write_to_bytes.
13665 let node = TreeNode::Internal(InNodeStub {
13666 node_id: 77,
13667 level: 0x10002,
13668 entries: vec![
13669 InEntry { key: vec![1, 2, 3] },
13670 InEntry { key: vec![4, 5, 6] },
13671 ],
13672 targets: TargetRep::None,
13673 dirty: false,
13674 generation: 0,
13675 parent: None,
13676 lsn_rep: LsnRep::Empty,
13677 });
13678 let bytes = node.write_to_bytes();
13679 let restored =
13680 Tree::deserialize_upper_in(&bytes).expect("must deserialize");
13681 assert_eq!(restored.node_id, 77);
13682 assert_eq!(restored.level, 0x10002);
13683 assert_eq!(restored.entries.len(), 2);
13684 assert_eq!(restored.entries[0].key, vec![1, 2, 3]);
13685 assert_eq!(restored.entries[1].key, vec![4, 5, 6]);
13686 }
13687}
13688
13689// --- Part 2 acceptance tests: key_prefixing flag (DRIFT-3) ---
13690//
13691// JE `IN.computeKeyPrefix` returns null when `databaseImpl.getKeyPrefixing()`
13692// is false, so no prefix compression is ever applied to those BINs. Noxu was
13693// always applying prefix compression. This checks that the flag is honoured.
13694//
13695// Ref: `IN.java computeKeyPrefix` ~line 2456,
13696// `DatabaseConfig.setKeyPrefixing` / `DatabaseImpl.getKeyPrefixing`.
13697#[cfg(test)]
13698mod key_prefixing_tests {
13699 use super::*;
13700
13701 /// Helper: find the first (leftmost) BIN in the tree.
13702 fn find_first_bin(node: &Arc<RwLock<TreeNode>>) -> Arc<RwLock<TreeNode>> {
13703 let child_opt = {
13704 let g = node.read();
13705 match &*g {
13706 TreeNode::Bottom(_) => None,
13707 TreeNode::Internal(n) => {
13708 Some(Arc::clone(n.child_ref(0).expect("child")))
13709 }
13710 }
13711 };
13712 match child_opt {
13713 None => Arc::clone(node),
13714 Some(child) => find_first_bin(&child),
13715 }
13716 }
13717
13718 /// With `key_prefixing = false` (the default), keys must be stored without
13719 /// any prefix: the BIN's `key_prefix` must remain empty after inserts.
13720 #[test]
13721 fn test_key_prefixing_false_stores_full_keys() {
13722 // Default is key_prefixing = false.
13723 let tree = Tree::new(1, 16);
13724 assert!(!tree.key_prefixing, "default must be false");
13725
13726 let lsn = noxu_util::Lsn::new(1, 10);
13727 // Insert keys with a long common prefix.
13728 for i in 0u8..8 {
13729 let key = vec![b'r', b'e', b'c', b'o', b'r', b'd', b':', i];
13730 tree.insert(key, vec![i], lsn).expect("insert");
13731 }
13732
13733 let root = tree.get_root().expect("root");
13734 let bin_arc = find_first_bin(&root);
13735 let guard = bin_arc.read();
13736 let TreeNode::Bottom(ref bin) = *guard else {
13737 panic!("must be a BIN");
13738 };
13739 assert!(
13740 bin.key_prefix.is_empty(),
13741 "key_prefix must be empty when key_prefixing=false, got {:?}",
13742 bin.key_prefix
13743 );
13744 assert_eq!(bin.entries.len(), 8);
13745 // Keys must be stored as full keys.
13746 assert_eq!(
13747 bin.get_full_key(0).unwrap(),
13748 vec![b'r', b'e', b'c', b'o', b'r', b'd', b':', 0]
13749 );
13750 }
13751
13752 /// With `key_prefixing = true`, keys with a common prefix are compressed:
13753 /// the BIN's `key_prefix` must be non-empty.
13754 #[test]
13755 fn test_key_prefixing_true_compresses_keys() {
13756 let mut tree = Tree::new(1, 16);
13757 tree.set_key_prefixing(true);
13758
13759 let lsn = noxu_util::Lsn::new(1, 10);
13760 for i in 0u8..8 {
13761 let key = vec![b'r', b'e', b'c', b'o', b'r', b'd', b':', i];
13762 tree.insert(key, vec![i], lsn).expect("insert");
13763 }
13764
13765 let root = tree.get_root().expect("root");
13766 let bin_arc = find_first_bin(&root);
13767 let guard = bin_arc.read();
13768 let TreeNode::Bottom(ref bin) = *guard else {
13769 panic!("must be a BIN");
13770 };
13771 // Prefix compression must kick in: all keys share "record:".
13772 assert!(
13773 !bin.key_prefix.is_empty(),
13774 "key_prefix must be non-empty when key_prefixing=true"
13775 );
13776 assert_eq!(
13777 bin.key_prefix,
13778 b"record:".to_vec(),
13779 "prefix must be the common prefix of all inserted keys"
13780 );
13781 }
13782
13783 /// Custom-comparator databases (sorted-dup) always bypass prefix
13784 /// regardless of key_prefixing: `insert_cmp` does not touch key_prefix.
13785 #[test]
13786 fn test_key_prefixing_custom_comparator_no_prefix() {
13787 let cmp: KeyComparatorFn = Arc::new(|a: &[u8], b: &[u8]| a.cmp(b));
13788 let mut tree = Tree::new_with_comparator(1, 16, cmp);
13789 // Enable key_prefixing — should have no effect via insert_cmp path.
13790 tree.set_key_prefixing(true);
13791
13792 let lsn = noxu_util::Lsn::new(1, 10);
13793 for i in 0u8..8 {
13794 let key = vec![b'r', b'e', b'c', b'o', b'r', b'd', b':', i];
13795 tree.insert(key, vec![i], lsn).expect("insert");
13796 }
13797
13798 let root = tree.get_root().expect("root");
13799 let bin_arc = find_first_bin(&root);
13800 let guard = bin_arc.read();
13801 let TreeNode::Bottom(ref bin) = *guard else {
13802 panic!("must be a BIN");
13803 };
13804 // Custom-comparator path (insert_cmp) does not set key_prefix.
13805 assert!(
13806 bin.key_prefix.is_empty(),
13807 "custom-comparator path must not set key_prefix"
13808 );
13809 }
13810}
13811
13812// --- Part 1 acceptance tests: splitSpecial heuristic (DRIFT-1) ---
13813//
13814// JE `IN.splitSpecial` / `Tree.forceSplit`: when all routing decisions during
13815// descent are leftmost (`AllLeft`) or rightmost (`AllRight`), the split index
13816// is forced to 1 or `n-1` respectively instead of `n/2`. This halves the
13817// number of splits for monotonically increasing / decreasing key workloads
13818// (sequential append / prepend) because each split leaves the BIN near-full.
13819//
13820// Ref: `IN.java splitSpecial` ~line 4129, `Tree.java forceSplit` ~line 1907.
13821#[cfg(test)]
13822mod split_special_tests {
13823 use super::*;
13824
13825 /// Test helper: descend the tree to the BIN that holds (or would hold)
13826 /// `key`, returning its arc. Mirrors the read-path descent used by
13827 /// `Tree::search`; sufficient for unit tests that need to mutate a slot.
13828 fn find_bin_arc_for_key(
13829 node_arc: &Arc<RwLock<TreeNode>>,
13830 key: &[u8],
13831 ) -> Option<Arc<RwLock<TreeNode>>> {
13832 let mut current = node_arc.clone();
13833 loop {
13834 let next = {
13835 let g = current.read();
13836 match &*g {
13837 TreeNode::Bottom(_) => return Some(current.clone()),
13838 TreeNode::Internal(n) => {
13839 if n.entries.is_empty() {
13840 return None;
13841 }
13842 let mut idx = 0usize;
13843 for (i, e) in n.entries.iter().enumerate() {
13844 if i == 0 || e.key.as_slice() <= key {
13845 idx = i;
13846 } else {
13847 break;
13848 }
13849 }
13850 n.get_child(idx)?
13851 }
13852 }
13853 };
13854 current = next;
13855 }
13856 }
13857
13858 /// Count total leaf (BIN) nodes in the tree by DFS.
13859 fn count_bins(node: &Arc<RwLock<TreeNode>>) -> usize {
13860 let g = node.read();
13861 match &*g {
13862 TreeNode::Bottom(_) => 1,
13863 TreeNode::Internal(n) => {
13864 n.resident_children().iter().map(count_bins).sum()
13865 }
13866 }
13867 }
13868
13869 /// Return total key count across all BINs.
13870 fn count_keys(node: &Arc<RwLock<TreeNode>>) -> usize {
13871 let g = node.read();
13872 match &*g {
13873 TreeNode::Bottom(b) => b.entries.len(),
13874 TreeNode::Internal(n) => {
13875 n.resident_children().iter().map(count_keys).sum()
13876 }
13877 }
13878 }
13879
13880 /// Returns the number of entries in the leftmost BIN.
13881 fn leftmost_bin_size(node: &Arc<RwLock<TreeNode>>) -> usize {
13882 let g = node.read();
13883 match &*g {
13884 TreeNode::Bottom(b) => b.entries.len(),
13885 TreeNode::Internal(n) => {
13886 let first_child = n.child_ref(0).expect("child");
13887 leftmost_bin_size(first_child)
13888 }
13889 }
13890 }
13891
13892 /// Returns the number of entries in the rightmost BIN.
13893 fn rightmost_bin_size(node: &Arc<RwLock<TreeNode>>) -> usize {
13894 let g = node.read();
13895 match &*g {
13896 TreeNode::Bottom(b) => b.entries.len(),
13897 TreeNode::Internal(n) => {
13898 let last_child = n
13899 .child_ref(n.entries.len().saturating_sub(1))
13900 .expect("child");
13901 rightmost_bin_size(last_child)
13902 }
13903 }
13904 }
13905
13906 /// `splitSpecial` ascending: each right-side split leaves the left BIN
13907 /// near-full (all but one entry stays). Compared to midpoint split
13908 /// the number of BINs created should be significantly fewer relative to
13909 /// keys inserted (more keys per BIN on average).
13910 ///
13911 /// JE criterion: `allRightSideDescent` → `splitIndex = nEntries - 1`.
13912 /// The penultimate entry stays in the left BIN; only one entry goes to
13913 /// the new right sibling, which then absorbs the next insert and fills
13914 /// normally.
13915 #[test]
13916 fn test_split_special_ascending_fewer_bins_than_midpoint() {
13917 let max_entries = 8usize;
13918 let n_keys = 200usize;
13919
13920 // Build tree with splitSpecial (ascending keys trigger AllRight).
13921 let tree_special = Tree::new(1, max_entries);
13922 let lsn = noxu_util::Lsn::new(1, 100);
13923 for i in 0u32..n_keys as u32 {
13924 let key = i.to_be_bytes().to_vec();
13925 tree_special.insert(key, vec![0u8], lsn).expect("insert");
13926 }
13927
13928 let root_special = tree_special.get_root().expect("root must exist");
13929 let bins_special = count_bins(&root_special);
13930 let keys_special = count_keys(&root_special);
13931
13932 // All keys must be present.
13933 assert_eq!(keys_special, n_keys, "all keys must be stored");
13934
13935 // With splitSpecial, each right-side split keeps n-1 entries in the
13936 // left BIN. Ideal: ceil(n_keys / (max_entries - 1)) BINs.
13937 // Without splitSpecial (midpoint): ceil(n_keys / (max_entries / 2)).
13938 // We assert the actual count is below the midpoint-split upper bound.
13939 let midpoint_upper_bound = n_keys.div_ceil(max_entries / 2);
13940 assert!(
13941 bins_special < midpoint_upper_bound,
13942 "splitSpecial should produce fewer BINs than midpoint split: \
13943 got {bins_special}, midpoint upper bound = {midpoint_upper_bound}"
13944 );
13945
13946 // The rightmost BIN must have fewer entries than max_entries
13947 // (the last insert only half-fills it at most), which is expected.
13948 // The IMPORTANT property: rightmost BIN started with exactly 1 entry
13949 // (its first entry was the split-off singleton) then filled up.
13950 // We just verify overall key density > midpoint baseline.
13951 let avg_fill = keys_special as f64 / bins_special as f64;
13952 let midpoint_fill = (max_entries / 2) as f64;
13953 assert!(
13954 avg_fill > midpoint_fill,
13955 "average fill per BIN with splitSpecial ({avg_fill:.1}) should \
13956 exceed midpoint baseline ({midpoint_fill})"
13957 );
13958 }
13959
13960 /// `splitSpecial` descending: all routing decisions are at slot 0
13961 /// (`AllLeft`). Split forces `split_index = 1` so the right sibling
13962 /// gets almost all entries and the left node keeps just one.
13963 ///
13964 /// JE criterion: `allLeftSideDescent` → `splitIndex = 1`.
13965 #[test]
13966 fn test_split_special_descending_fewer_bins_than_midpoint() {
13967 let max_entries = 8usize;
13968 let n_keys = 200usize;
13969
13970 let tree_special = Tree::new(1, max_entries);
13971 let lsn = noxu_util::Lsn::new(1, 100);
13972 for i in (0u32..n_keys as u32).rev() {
13973 let key = i.to_be_bytes().to_vec();
13974 tree_special.insert(key, vec![0u8], lsn).expect("insert");
13975 }
13976
13977 let root_special = tree_special.get_root().expect("root must exist");
13978 let bins_special = count_bins(&root_special);
13979 let keys_special = count_keys(&root_special);
13980
13981 assert_eq!(keys_special, n_keys, "all keys must be stored");
13982
13983 let midpoint_upper_bound = n_keys.div_ceil(max_entries / 2);
13984 assert!(
13985 bins_special < midpoint_upper_bound,
13986 "splitSpecial descending should produce fewer BINs: \
13987 got {bins_special}, midpoint upper bound = {midpoint_upper_bound}"
13988 );
13989 }
13990
13991 /// Random-key inserts must NOT be affected by splitSpecial: with random
13992 /// keys descent will rarely be all-left or all-right, so the split index
13993 /// defaults to midpoint and tree balance is maintained.
13994 #[test]
13995 fn test_split_special_random_inserts_stay_balanced() {
13996 use std::collections::BTreeSet;
13997
13998 let max_entries = 8usize;
13999 // Use a fixed permutation so the test is deterministic.
14000 let mut keys: Vec<u32> = (0u32..200).collect();
14001 // Knuth shuffle with a fixed seed.
14002 let mut rng: u64 = 0xdeadbeef_cafebabe;
14003 for i in (1..keys.len()).rev() {
14004 rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
14005 let j = (rng >> 33) as usize % (i + 1);
14006 keys.swap(i, j);
14007 }
14008
14009 let tree = Tree::new(1, max_entries);
14010 let lsn = noxu_util::Lsn::new(1, 100);
14011 let mut inserted = BTreeSet::new();
14012 for k in &keys {
14013 let key = k.to_be_bytes().to_vec();
14014 tree.insert(key, vec![0u8], lsn).expect("insert");
14015 inserted.insert(*k);
14016 }
14017
14018 let root = tree.get_root().expect("root");
14019 let total_keys = count_keys(&root);
14020 assert_eq!(
14021 total_keys,
14022 inserted.len(),
14023 "all random keys must be stored"
14024 );
14025
14026 // Verify every key is findable.
14027 for k in &inserted {
14028 let key = k.to_be_bytes().to_vec();
14029 let found = tree.search(&key);
14030 assert!(
14031 found.map(|r| r.is_exact_match()).unwrap_or(false),
14032 "random key {k} must be findable after insert"
14033 );
14034 }
14035 }
14036
14037 /// TREE-F1: a `known_deleted` BIN slot must read as ABSENT on an exact
14038 /// lookup and must be SKIPPED by scans, matching JE.
14039 ///
14040 /// JE contract:
14041 /// * `IN.findEntry` (IN.java:3197): an exact match that lands on a
14042 /// known-deleted slot returns -1 (ABSENT).
14043 /// * `CursorImpl.lockAndGetCurrent` (CursorImpl.java:2062-2064): a
14044 /// step that lands on `isEntryKnownDeleted(index)` returns null, so
14045 /// the `getNext` loop advances past it (the slot is skipped).
14046 ///
14047 /// KD slots legitimately exist in live BINs during BIN-delta
14048 /// reconstitution (`mutate_to_full_bin` applies delta KD slots) until
14049 /// the compressor reclaims them. We reach that state directly here by
14050 /// marking a slot known_deleted in the BIN arc, then assert the
14051 /// user-facing read/scan paths do not surface it.
14052 #[test]
14053 fn test_tree_f1_known_deleted_slot_is_absent_and_skipped() {
14054 let tree = Tree::new(1, 8);
14055 // Insert enough keys to populate a BIN with several live slots.
14056 for i in 0..6u32 {
14057 let key = format!("kd{i:04}").into_bytes();
14058 tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
14059 }
14060
14061 // Pick a middle key and mark its slot known_deleted directly in the
14062 // BIN, modelling a delta-applied tombstone the compressor has not yet
14063 // reclaimed.
14064 let kd_key = b"kd0003".to_vec();
14065 {
14066 let root = tree.get_root().expect("root");
14067 let bin_arc = find_bin_arc_for_key(&root, &kd_key).expect("bin");
14068 let mut g = bin_arc.write();
14069 if let TreeNode::Bottom(b) = &mut *g {
14070 let idx = (0..b.entries.len())
14071 .find(|&i| {
14072 b.get_full_key(i).as_deref() == Some(kd_key.as_slice())
14073 })
14074 .expect("kd key slot");
14075 b.entries[idx].known_deleted = true;
14076 } else {
14077 panic!("expected BIN");
14078 }
14079 }
14080
14081 // (a) exact lookup via Tree::search must report NOT found.
14082 let sr = tree.search(&kd_key);
14083 assert!(
14084 !sr.map(|r| r.is_exact_match()).unwrap_or(false),
14085 "TREE-F1: Tree::search must report a known_deleted slot as absent \
14086 (IN.findEntry IN.java:3197)"
14087 );
14088
14089 // (a) exact lookup via Tree::search_with_data must report NOT found.
14090 let sf = tree.search_with_data(&kd_key).expect("slot fetch");
14091 assert!(
14092 !sf.found,
14093 "TREE-F1: Tree::search_with_data must report a known_deleted slot \
14094 as absent (IN.findEntry IN.java:3197)"
14095 );
14096
14097 // Live neighbours must still be found.
14098 for live in [b"kd0002".to_vec(), b"kd0004".to_vec()] {
14099 assert!(
14100 tree.search(&live).map(|r| r.is_exact_match()).unwrap_or(false),
14101 "live neighbour must remain findable"
14102 );
14103 }
14104
14105 // (b) a scan-facing BIN dump (descend_to_edge_bin / get_next_bin /
14106 // get_prev_bin) returns slots verbatim WITH the known_deleted flag
14107 // set, so the cursor can skip them (CursorImpl.java:2062-2064). The
14108 // contract here is: the KD slot is never reported as a LIVE entry.
14109 let root = tree.get_root().expect("root");
14110 let edge = Tree::descend_to_edge_bin(&root, true).expect("edge bin");
14111 assert!(
14112 !edge.iter().any(|(e, _, k)| k == &kd_key && !e.known_deleted),
14113 "TREE-F1: scan must not surface a known_deleted slot as live \
14114 (CursorImpl.java:2062-2064)"
14115 );
14116 for anchor in [b"kd0000".to_vec(), b"kd0005".to_vec()] {
14117 for entries in
14118 [tree.get_next_bin(&anchor), tree.get_prev_bin(&anchor)]
14119 .into_iter()
14120 .flatten()
14121 {
14122 assert!(
14123 !entries
14124 .iter()
14125 .any(|(e, _, k)| k == &kd_key && !e.known_deleted),
14126 "TREE-F1: get_next_bin/get_prev_bin must not surface a \
14127 known_deleted slot as live"
14128 );
14129 }
14130 }
14131
14132 // first_entry_at_or_after must skip a KD slot at the boundary.
14133 if let Some((k, _, _)) = tree.first_entry_at_or_after(&kd_key) {
14134 assert_ne!(
14135 k, kd_key,
14136 "TREE-F1: first_entry_at_or_after must skip a known_deleted \
14137 slot (CursorImpl.java:2062-2064)"
14138 );
14139 }
14140
14141 // The compressor KD-iteration path must STILL see the slot — the fix
14142 // only changes the user-facing read predicate, not the maintenance
14143 // iteration that exists to reclaim KD slots.
14144 let kd_bins = tree.collect_bins_with_known_deleted();
14145 assert!(
14146 !kd_bins.is_empty(),
14147 "TREE-F1: collect_bins_with_known_deleted must still observe the \
14148 KD slot so the compressor can reclaim it"
14149 );
14150 }
14151}