Skip to main content

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}