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        // EVICTOR-LOG-1 safety: a BIN may only be detached once it has a
6056        // durable full-BIN version on disk (`last_full_lsn != NULL`).  The
6057        // parent slot LSN is stamped from `last_full_lsn` below and drives the
6058        // re-fetch (`fetch_node_from_log`, which parses the entry as an
6059        // InLogEntry/BIN).  If we detached a never-logged BIN the slot would
6060        // keep its prior value -- an *LN* LSN -- and the re-fetch would try to
6061        // parse an LN entry as a BIN and fail, silently losing the whole
6062        // BIN's keys.  Callers are expected to `flush_dirty_node_to_log`
6063        // first, but that can no-op (evictor without a LogManager wired / a
6064        // failed log write), so enforce the invariant here at the single
6065        // shared detach site rather than trusting every caller.  Peek without
6066        // removing the child so it is left resident on refusal.
6067        // JE: `Evictor.evict` only detaches after `target.log(...)` returns a
6068        // valid LSN (Evictor.java:3027-3035).
6069        if let Some(c) = p.child_ref(child_index)
6070            && matches!(&*c.read(), TreeNode::Bottom(b) if b.last_full_lsn == NULL_LSN)
6071        {
6072            return 0; // never-logged BIN -- keep resident, do not corrupt slot
6073        }
6074        // T-4: detach the cached child via the node-level INTargetRep, leaving
6075        // the slot's key/LSN intact for re-fetch (JE IN.setTarget(idx, null)).
6076        let child = match p.take_child(child_index) {
6077            Some(c) => c,     // child Arc removed from the slot
6078            None => return 0, // already detached
6079        };
6080
6081        // Measure the child's real heap footprint while we still hold it.
6082        // JE: long evictedBytes = target.getBudgetedMemorySize().
6083        let freed = child.read().budgeted_memory_size();
6084
6085        // EV-14 re-fetch correctness: the parent slot LSN must point at the
6086        // child's CURRENT on-disk version so `child_at_or_fetch` re-reads the
6087        // right bytes (JE `IN.updateEntry(idx, newLsn)` is called whenever a
6088        // child is logged; the parent slot LSN tracks the child's LSN).  The
6089        // evictor only fully evicts/detaches a CLEAN BIN (it logs+clears dirty
6090        // BINs via flush_dirty_node_to_log first, which sets `last_full_lsn`),
6091        // so the child's authoritative LSN is its `last_full_lsn`.  Stamp it
6092        // into the parent slot before dropping the child; if it is null (the
6093        // child was never logged) leave the existing slot LSN intact rather
6094        // than writing a null — a never-logged clean child cannot occur on
6095        // the evict path, but be conservative.
6096        let child_full_lsn = match &*child.read() {
6097            TreeNode::Bottom(b) => b.last_full_lsn,
6098            TreeNode::Internal(_) => NULL_LSN,
6099        };
6100        if child_full_lsn != NULL_LSN {
6101            p.set_lsn(child_index, child_full_lsn);
6102        }
6103
6104        // Mark the parent dirty: the slot's in-memory target changed (JE
6105        // detachNode sets dirty when updateLsn; we conservatively mark dirty
6106        // so the parent is re-logged with the now-non-resident slot).
6107        p.dirty = true;
6108
6109        // Drop the strong Arc explicitly so the node is freed now (the slot's
6110        // `child` is already None).  If any other resident path still held a
6111        // strong reference this would not free — but the tree is the sole
6112        // strong owner of a cached child, so this drops the last strong ref.
6113        drop(parent_guard);
6114        drop(child);
6115
6116        // JE: getInMemoryINs().remove(child) — drop it from the evictor LRU.
6117        self.note_removed(node_id);
6118
6119        // NOTE: the live tree-memory counter (`memory_counter`) is the SAME
6120        // `Arc<AtomicI64>` the evictor's Arbiter uses as `cache_usage`.  The
6121        // evictor decrements it once via `Arbiter::release_memory(bytes)` for
6122        // the full eviction batch, so detach must NOT decrement here too —
6123        // that would double-credit and drive `cache_usage` below reality
6124        // (the very drift EV-13 fixes, in the other direction).  We only
6125        // measure-and-free; the caller does the single counter update.
6126        freed
6127    }
6128
6129    /// Evict the root IN of this tree (EV-14).
6130    ///
6131    /// Faithful port of JE `Evictor.evictRoot` (Evictor.java:3050-3110) plus
6132    /// the `RootEvictor.doWork` + `Tree.withRootLatchedExclusive` framing
6133    /// (Evictor.java:2529-2576, Tree.java:508-517).  Unlike a normal IN, the
6134    /// root has no parent slot to detach from; instead the *tree's* root
6135    /// reference is the equivalent of the `RootChildReference`, so eviction:
6136    ///
6137    ///   1. Latches the root reference exclusively (`rootLatch.acquireExclusive`
6138    ///      via `withRootLatchedExclusive`).
6139    ///   2. Re-checks that the root is still resident and still evictable
6140    ///      (no resident children, no pinned BIN — JE `RootEvictor.doWork`
6141    ///      re-latches and re-checks `rootIN == target && rootIN.isRoot()`).
6142    ///   3. If the root is dirty, LOGS it first so the on-disk version is
6143    ///      current and updates `root_log_lsn` to the new LSN (JE
6144    ///      `evictRoot`: `long newLsn = target.log(...); rootRef.setLsn(newLsn)`).
6145    ///   4. Clears the in-memory root (`rootRef.clearTarget()` — JE leaves the
6146    ///      `ChildReference` LSN intact; here `root_log_lsn` is that LSN) and
6147    ///      `note_removed`s it from the evictor LRU (JE `inList.remove(target)`).
6148    ///
6149    /// On the next access `fetch_root_from_log` re-materializes the root from
6150    /// `root_log_lsn` (JE `Tree.getRootINRootAlreadyLatched` →
6151    /// `root.fetchTarget`).
6152    ///
6153    /// # Conditions (eviction is REFUSED, returning `None`, when)
6154    ///
6155    /// * there is no log manager wired (the root could never be re-fetched),
6156    /// * the tree has no resident root (already evicted),
6157    /// * the root has any resident child (JE only evicts a childless root —
6158    ///   the `hasCachedChildren` skip in `processTarget`; a root with cached
6159    ///   children would orphan them, the EV-6 invariant),
6160    /// * the root is a BIN pinned by a cursor (`cursor_count > 0`),
6161    /// * the root is dirty but we have no clean persisted version AND logging
6162    ///   it fails, or
6163    /// * the root is clean but `root_log_lsn` is null (never logged — cannot
6164    ///   be re-fetched; happens only for a brand-new unlogged tree).
6165    ///
6166    /// Returns `Some((freed_bytes, was_dirty))` on success, where `freed_bytes`
6167    /// is the root's measured heap footprint (JE
6168    /// `target.getBudgetedMemorySize()`) and `was_dirty` reports whether the
6169    /// root had to be logged (JE `rootEvictor.flushed`, which drives
6170    /// `nDirtyNodesEvicted` and `modifyDbRoot`).
6171    pub fn evict_root(&self, db_id: u64) -> Option<(u64, bool)> {
6172        // A root with no re-fetch path must never be made non-resident.
6173        self.log_manager.as_ref()?;
6174
6175        // JE `Tree.withRootLatchedExclusive(rootEvictor)`: hold the root latch
6176        // exclusively across the whole evict so no descender or splitter can
6177        // observe/install a half-evicted root.  Acquiring `self.root.write()`
6178        // is the Noxu equivalent (it is the lock guarding the root pointer).
6179        let mut root_slot = self.root.write();
6180        let root_arc = root_slot.as_ref()?.clone();
6181
6182        // JE `RootEvictor.doWork`: re-latch the target and re-check the
6183        // conditions.  We hold the node guard for the duration.
6184        let node_guard = root_arc.write();
6185
6186        // EV-6 / JE `processTarget` hasCachedChildren skip: a root with any
6187        // resident child must NOT be evicted (it would orphan the child).
6188        // EV-14 only evicts an *idle* root whose children are already
6189        // non-resident (or which is itself a leaf BIN).
6190        let (node_id, was_dirty, freed) = match &*node_guard {
6191            TreeNode::Internal(n) => {
6192                if !n.resident_children().is_empty() {
6193                    return None; // has cached children — keep resident
6194                }
6195                (n.node_id, n.dirty, node_guard.budgeted_memory_size())
6196            }
6197            TreeNode::Bottom(b) => {
6198                if b.cursor_count > 0 {
6199                    return None; // pinned by a cursor — keep resident
6200                }
6201                (
6202                    b.node_id,
6203                    b.dirty || b.dirty_count() > 0,
6204                    node_guard.budgeted_memory_size(),
6205                )
6206            }
6207        };
6208
6209        // If dirty, log the root first so the on-disk version is current,
6210        // then record the new LSN as the root's re-fetch point (JE
6211        // `evictRoot`: target.log(...) + rootRef.setLsn(newLsn)).
6212        if was_dirty {
6213            let lm = self.log_manager.as_ref()?; // checked above; re-borrow
6214            let node_bytes = node_guard.write_to_bytes();
6215            let is_bin = node_guard.is_bin();
6216            let entry = noxu_log::entry::in_log_entry::InLogEntry::new(
6217                db_id, NULL_LSN, // prev_full_lsn
6218                NULL_LSN, // prev_delta_lsn
6219                node_bytes,
6220            );
6221            let mut buf = bytes::BytesMut::with_capacity(entry.log_size());
6222            entry.write_to_log(&mut buf);
6223            let entry_type = if is_bin {
6224                noxu_log::LogEntryType::BIN
6225            } else {
6226                noxu_log::LogEntryType::IN
6227            };
6228            // flush_required = true so the root's bytes are durable before we
6229            // drop the in-memory copy (JE logs synchronously in evictRoot).
6230            let new_lsn = match lm.log(
6231                entry_type,
6232                &buf,
6233                noxu_log::Provisional::No,
6234                true,  // flush_required
6235                false, // fsync at next checkpoint
6236            ) {
6237                Ok(l) => l,
6238                Err(_) => return None, // could not log — keep the root resident
6239            };
6240            *self.root_log_lsn.write() = new_lsn;
6241        } else {
6242            // Clean root: it must already be re-fetchable.  If it was never
6243            // logged (root_log_lsn null) we cannot evict it safely.
6244            if *self.root_log_lsn.read() == NULL_LSN {
6245                return None;
6246            }
6247        }
6248
6249        // JE `rootRef.clearTarget()` + `inList.remove(target)`: drop the
6250        // in-memory root and remove it from the evictor LRU.  The root_log_lsn
6251        // is the surviving `ChildReference` LSN used to re-fetch it.
6252        drop(node_guard);
6253        *root_slot = None;
6254        drop(root_slot);
6255        self.note_removed(node_id);
6256
6257        Some((freed, was_dirty))
6258    }
6259
6260    /// Re-materialize an evicted root IN from its persisted `root_log_lsn`
6261    /// (EV-14, piece B).
6262    /// Faithful to JE `Tree.getRootINRootAlreadyLatched` (Tree.java:477-516)
6263    /// which calls `root.fetchTarget(database, null)` when the in-memory
6264    /// target is null.  Idempotent and cheap when the root is already
6265    /// resident: returns the resident root without touching the log.
6266    ///
6267    /// Returns `None` only when the tree is genuinely empty (no resident root
6268    /// AND `root_log_lsn` is null) or when the re-fetch fails (no log manager,
6269    /// log read error, deserialize failure) — callers then see an empty tree,
6270    /// never wrong data.
6271    pub fn fetch_root_from_log(&self) -> Option<Arc<RwLock<TreeNode>>> {
6272        // Fast path: root already resident.
6273        if let Some(r) = self.root.read().clone() {
6274            return Some(r);
6275        }
6276        // Take the write lock and re-check (another thread may have re-fetched
6277        // it while we waited — JE upgrades the root latch the same way).
6278        let mut root_slot = self.root.write();
6279        if let Some(r) = root_slot.as_ref() {
6280            return Some(r.clone());
6281        }
6282        let log_lsn = *self.root_log_lsn.read();
6283        let node = self.fetch_node_from_log(log_lsn)?;
6284        let node_id = node.node_id();
6285        let arc = Arc::new(RwLock::new(node));
6286        *root_slot = Some(arc.clone());
6287        drop(root_slot);
6288        // JE: a fetched IN is added back to the INList (Evictor LRU).
6289        self.note_added(node_id);
6290        Some(arc)
6291    }
6292
6293    /// Return the resident child Arc for slot `idx` of `parent_arc`, fetching
6294    /// it from its slot LSN and installing it if it is not resident (EV-14 /
6295    /// EV-13 re-fetch on descent).
6296    ///
6297    /// Faithful to JE `ChildReference.fetchTarget` (and `IN.fetchTarget`):
6298    /// when a slot's in-memory target is null but its LSN is valid, the node
6299    /// is read back from the log and cached in the slot.  Installing the
6300    /// fetched child requires the parent EX-latch, so this takes the parent
6301    /// write lock; the fast path (child already resident) takes only a read
6302    /// lock.
6303    ///
6304    /// Returns `None` only when the slot index is out of range, the slot has
6305    /// no valid LSN, or the log read/deserialize fails — callers then treat
6306    /// the descent as terminating in an empty subtree, never wrong data.
6307    fn child_at_or_fetch(
6308        &self,
6309        parent_arc: &Arc<RwLock<TreeNode>>,
6310        idx: usize,
6311    ) -> Option<ChildArc> {
6312        // Fast path: child already cached (read lock only).
6313        {
6314            let g = parent_arc.read();
6315            if let TreeNode::Internal(n) = &*g {
6316                if let Some(c) = n.get_child(idx) {
6317                    return Some(c);
6318                }
6319            } else {
6320                return None; // BINs have no IN children
6321            }
6322        }
6323        // Slow path: fetch the child from its slot LSN under the parent
6324        // EX-latch (JE installs the fetched target under the IN latch).
6325        let mut g = parent_arc.write();
6326        let TreeNode::Internal(n) = &mut *g else {
6327            return None;
6328        };
6329        // Re-check: another thread may have fetched it while we upgraded.
6330        if let Some(c) = n.get_child(idx) {
6331            return Some(c);
6332        }
6333        if idx >= n.entries.len() {
6334            return None;
6335        }
6336        let child_lsn = n.get_lsn(idx);
6337        let node = self.fetch_node_from_log(child_lsn)?;
6338        let node_id = node.node_id();
6339        let arc: ChildArc = Arc::new(RwLock::new(node));
6340        n.set_child(idx, Some(arc.clone()));
6341        drop(g);
6342        // JE: a fetched IN is added back to the INList (Evictor LRU).
6343        self.note_added(node_id);
6344        Some(arc)
6345    }
6346
6347    /// Check whether a BIN node is a candidate for slot compression and,
6348    /// if so, trigger `compress_bin`.
6349    ///
6350    /// from (the opportunistic / lazy compression path).
6351    ///
6352    /// # Algorithm
6353    ///
6354    /// 1. Skip the BIN if it is a delta or has no defunct (known-deleted) slots.
6355    /// 2. If compression succeeds and the BIN becomes empty, it is pruned.
6356    ///
6357    /// # Returns
6358    ///
6359    /// `true` if compression was triggered (regardless of whether any slots
6360    /// were actually removed), `false` if the BIN does not need compression.
6361    pub fn maybe_compress_bin_and_parent(
6362        &self,
6363        bin_arc: &Arc<RwLock<TreeNode>>,
6364    ) -> bool {
6365        // Check whether the BIN has any deleted slots worth compressing.
6366        // lazyCompress: skip deltas and BINs with no defunct slots.
6367        let should_compress = {
6368            {
6369                let g = bin_arc.read();
6370                match &*g {
6371                    TreeNode::Bottom(b) => {
6372                        // Skip deltas (the: !in.isBIN() || in.isBINDelta()).
6373                        if b.is_delta {
6374                            false
6375                        } else {
6376                            // Check for any known-deleted slot
6377                            // (the: for (int i=0; i < bin.getNEntries(); i++) {
6378                            //        if (bin.isDefunct(i)) { ... break; }
6379                            //      }).
6380                            b.entries.iter().any(|e| e.known_deleted)
6381                        }
6382                    }
6383                    _ => false,
6384                }
6385            }
6386        };
6387
6388        if !should_compress {
6389            return false;
6390        }
6391
6392        self.compress_bin(bin_arc)
6393    }
6394
6395    // ========================================================================
6396    // Latch-coupling validation
6397    // ========================================================================
6398
6399    /// Validate that `parent.entries[child_index].child` still points at
6400    /// `child_arc` after acquiring the child's latch.
6401    ///
6402    /// Re-latch validation step inside the
6403    /// `Tree.searchSplitsAllowed`: after a concurrent split the parent
6404    /// slot that previously held the child may have changed.  Callers that
6405    /// plan to mutate the child must verify the parent-child link is still
6406    /// intact before proceeding.
6407    ///
6408    /// Returns `true` if the parent-child link is intact.
6409    pub fn validate_parent_child(
6410        parent: &Arc<RwLock<TreeNode>>,
6411        child_index: usize,
6412        child_arc: &Arc<RwLock<TreeNode>>,
6413    ) -> bool {
6414        let g = parent.read();
6415        match &*g {
6416            TreeNode::Internal(p) => match p.child_ref(child_index) {
6417                Some(stored) => Arc::ptr_eq(stored, child_arc),
6418                None => false,
6419            },
6420            TreeNode::Bottom(_) => false,
6421        }
6422    }
6423
6424    /// Search for the BIN that should contain `key`, with latch-coupling
6425    /// validation at every level of descent.
6426    ///
6427    /// .
6428    ///
6429    /// The difference from `search()` is that after obtaining the child
6430    /// arc we call `validate_parent_child` to confirm the parent still
6431    /// holds the expected Arc.  If the link has been broken (e.g. by a
6432    /// concurrent split that relocated the child) the traversal restarts
6433    /// from the root.
6434    ///
6435    /// Returns a `SearchResult` if the key is (or should be) in the tree,
6436    /// `None` if the tree is empty.
6437    ///
6438    /// Same as [`Tree::search`] but exposes the hand-over-hand latch
6439    /// coupling explicitly. Kept as a public, equivalent API for
6440    /// callers (today only tests) that want to verify the
6441    /// latch-coupling behaviour against `search()` itself.
6442    ///
6443    /// Both `search()` and this method use the same `read_arc()`
6444    /// hand-over-hand: take the child read guard *before* dropping
6445    /// the parent guard, so a concurrent `split_child(parent, ..)`
6446    /// (which takes `parent.write()`) cannot run between when we
6447    /// captured the child Arc and when we entered the child. There
6448    /// is no validate-and-restart loop because the coupling makes
6449    /// the race unreachable.
6450    pub fn search_with_coupling(&self, key: &[u8]) -> Option<SearchResult> {
6451        let root = self.get_root()?;
6452        let mut guard: NodeArcReadGuard = root.read_arc();
6453
6454        loop {
6455            if guard.is_bin() {
6456                let index = guard.find_entry(key, true, true);
6457                let found = index >= 0 && (index & EXACT_MATCH != 0);
6458                return Some(SearchResult::with_values(
6459                    found,
6460                    index & 0xFFFF,
6461                    false,
6462                ));
6463            }
6464
6465            let parent_arc = NodeArcReadGuard::rwlock(&guard).clone();
6466            let next_idx = match &*guard {
6467                TreeNode::Internal(n) => {
6468                    if n.entries.is_empty() {
6469                        return None;
6470                    }
6471                    let idx = self.upper_in_floor_index(&n.entries, key);
6472                    match n.get_child(idx) {
6473                        Some(c) => {
6474                            let next_guard = c.read_arc();
6475                            drop(guard);
6476                            guard = next_guard;
6477                            continue;
6478                        }
6479                        None => idx, // EV-14/EV-13: re-fetch below.
6480                    }
6481                }
6482                TreeNode::Bottom(_) => {
6483                    unreachable!("is_bin() returned false above")
6484                }
6485            };
6486            // Hand-over-hand: take the child read guard before
6487            // releasing the parent guard. Closes the
6488            // descender-vs-splitter window: a concurrent
6489            // split_child(parent, ..) takes parent.write(), which
6490            // blocks while we still hold parent.read().
6491            drop(guard);
6492            let child = self.child_at_or_fetch(&parent_arc, next_idx)?;
6493            guard = child.read_arc();
6494        }
6495    }
6496
6497    // ========================================================================
6498    // BIN-Delta reconstitution
6499    // ========================================================================
6500
6501    /// Increments the cursor-pin count on a BIN node.
6502    ///
6503    /// Called by `CursorImpl` when it positions on (or enters) a BIN.
6504    /// The evictor will not select a BIN with `cursor_count > 0` for eviction
6505    /// (`RealNodeInfo.pin_count`), matching `BIN.incrementCursorCount()`.
6506    pub fn pin_bin(bin_arc: &Arc<RwLock<TreeNode>>) {
6507        let mut guard = bin_arc.write();
6508        if let TreeNode::Bottom(ref mut stub) = *guard {
6509            stub.cursor_count += 1;
6510        }
6511    }
6512
6513    /// Decrements the cursor-pin count on a BIN node.
6514    ///
6515    /// Called by `CursorImpl` when it moves away from or closes on a BIN.
6516    /// Uses `saturating_sub` to guard against an accidental double-unpin.
6517    /// Matching `BIN.decrementCursorCount()`.
6518    pub fn unpin_bin(bin_arc: &Arc<RwLock<TreeNode>>) {
6519        let mut guard = bin_arc.write();
6520        if let TreeNode::Bottom(ref mut stub) = *guard {
6521            stub.cursor_count = stub.cursor_count.saturating_sub(1);
6522        }
6523    }
6524
6525    /// Returns `true` if the given `BinStub` is a BIN-delta (not a full BIN).
6526    ///
6527    /// `IN.isBINDelta()`.
6528    pub fn bin_is_delta(bin: &BinStub) -> bool {
6529        bin.is_delta
6530    }
6531
6532    /// Merge delta entries into a full BIN's entry list.
6533    ///
6534    /// - For each delta entry: if a matching key already exists in `bin`,
6535    ///   replace it (delta is authoritative).
6536    /// - Otherwise insert the delta entry in sorted position.
6537    ///
6538    /// Delta entries carry **full** keys (prefix already prepended by the
6539    /// caller).  After applying all delta entries the BIN's prefix is
6540    /// recomputed so the final state is consistent.
6541    ///
6542    /// All delta entries are considered to be the most-recently-dirtied
6543    /// state, exactly as in where delta slots supersede full-BIN slots.
6544    pub fn apply_delta_to_bin(
6545        bin: &mut BinStub,
6546        delta_entries: Vec<(Vec<u8>, Lsn, Option<Vec<u8>>)>,
6547    ) {
6548        for (full_key, lsn, data) in delta_entries {
6549            // `full_key` is a full (uncompressed) key here.
6550            bin.insert_with_prefix(full_key, lsn, data);
6551        }
6552        bin.dirty = true;
6553    }
6554
6555    /// Reconstitute a BIN-delta into a full BIN.
6556    ///
6557    /// from the:
6558    ///
6559    /// 1. Extract the delta entries from `self` (this BIN-delta), decompressing
6560    ///    them to full keys.
6561    /// 2. Apply them onto `base` (the previously logged full BIN) via
6562    ///    `apply_delta_to_bin`.
6563    /// 3. Copy `base`'s merged entries and prefix back into `self`.
6564    /// 4. Clear the `is_delta` flag so subsequent code treats `self` as
6565    ///    a full BIN.
6566    ///
6567    /// After this call `self` is a full BIN; `base` should be discarded.
6568    pub fn mutate_to_full_bin(delta: &mut BinStub, mut base: BinStub) {
6569        // Decompress delta entries to full keys before applying.
6570        let delta_full_entries: Vec<(Vec<u8>, Lsn, Option<Vec<u8>>)> = (0
6571            ..delta.entries.len())
6572            .map(|i| {
6573                (
6574                    delta.get_full_key(i).unwrap_or_default(),
6575                    delta.get_lsn(i),
6576                    delta.entries[i].data.clone(),
6577                )
6578            })
6579            .collect();
6580        // reconstituteBIN + resetContent + setBINDelta(false).
6581        Self::apply_delta_to_bin(&mut base, delta_full_entries);
6582        delta.entries = base.entries;
6583        delta.lsn_rep = base.lsn_rep; // T-3
6584        delta.keys = base.keys; // T-2
6585        delta.key_prefix = base.key_prefix;
6586        delta.is_delta = false;
6587        delta.dirty = true;
6588    }
6589
6590    /// Read an IN/BIN log entry at `log_lsn` and deserialise it into a
6591    /// `TreeNode`, ready to be installed as a (re-fetched) resident node.
6592    ///
6593    /// JE `LogManager.getLogEntry(lsn)` + `IN.readFromLog` as used by
6594    /// `ChildReference.fetchTarget` (the path that re-materializes a
6595    /// non-resident node from its persisted LSN on descent) and by
6596    /// `Tree.getRootINRootAlreadyLatched` for the root.  The freshly-fetched
6597    /// node has no resident children (`TargetRep::None`); its own children, if
6598    /// any, are re-fetched on demand the same way when the descent reaches
6599    /// them.
6600    ///
6601    /// Returns `None` if the LSN is null, the log read fails, the entry is not
6602    /// an IN/BIN, or deserialisation fails (the caller treats this as "node
6603    /// unavailable" rather than panicking, matching the graceful-degradation
6604    /// policy of `mutate_to_full_bin_from_log`).
6605    fn fetch_node_from_log(&self, log_lsn: Lsn) -> Option<TreeNode> {
6606        if log_lsn == NULL_LSN {
6607            return None;
6608        }
6609        let lm = self.log_manager.as_ref()?;
6610        let (entry_type, payload) = lm.read_entry(log_lsn).ok()?;
6611        // The on-disk payload is an `InLogEntry` body (db_id | prev_full_lsn
6612        // | prev_delta_lsn | len | node_data).  The recovery scanner strips
6613        // this header before calling `recover_in_redo`; re-fetch must do the
6614        // same so `deserialize_*` sees the bare node bytes.  JE
6615        // `INLogEntry.readEntry` parses the same wrapper.
6616        let in_entry =
6617            noxu_log::entry::in_log_entry::InLogEntry::read_from_log(&payload)
6618                .ok()?;
6619        let node_data = &in_entry.node_data;
6620        use noxu_log::LogEntryType;
6621        match entry_type {
6622            LogEntryType::BIN => {
6623                Self::deserialize_bin(node_data).map(TreeNode::Bottom)
6624            }
6625            LogEntryType::IN => {
6626                Self::deserialize_upper_in(node_data).map(TreeNode::Internal)
6627            }
6628            // BIN-deltas are never logged as the *root* version and are
6629            // reconstituted by the BIN-delta path, not here.
6630            _ => {
6631                log::warn!(
6632                    "fetch_node_from_log: expected IN/BIN entry at LSN {:?}, \
6633                     got {:?}",
6634                    log_lsn,
6635                    entry_type
6636                );
6637                None
6638            }
6639        }
6640    }
6641
6642    /// Reconstitute a BIN-delta into a full BIN by reading the base from log.
6643    ///
6644    /// — the
6645    /// single-argument overload that calls `fetchFullBIN(databaseImpl)` to
6646    /// read the last full BIN from the log manager automatically.
6647    ///
6648    /// Algorithm:
6649    /// 1. If `delta.last_full_lsn == NULL_LSN`, the BIN was never written as a
6650    ///    full entry; there is no base to merge so the delta IS the full BIN.
6651    ///    Clear `is_delta` and return.
6652    /// 2. Read the full-BIN log entry at `delta.last_full_lsn` using
6653    ///    `log_manager.read_entry(lsn)`.
6654    /// 3. Deserialize the payload with `BinStub::deserialize_full()`.
6655    /// 4. Delegate to `Self::mutate_to_full_bin(delta, base)` to merge and
6656    ///    replace `delta`'s contents.
6657    ///
6658    /// On any read / parse failure the function falls back to clearing the
6659    /// `is_delta` flag without merging, so the caller always gets a non-delta
6660    /// BIN (possibly missing some old slots).  This mirrors the
6661    /// `EnvironmentFailureException` path but gracefully degrades instead of
6662    /// panicking.
6663    ///
6664    /// `BIN.fetchFullBIN(dbImpl)` + `BIN.mutateToFullBIN(boolean)`.
6665    pub fn mutate_to_full_bin_from_log(
6666        delta: &mut BinStub,
6667        log_manager: &noxu_log::LogManager,
6668    ) {
6669        if !delta.is_delta {
6670            // Already a full BIN; nothing to do.
6671            return;
6672        }
6673
6674        if delta.last_full_lsn == NULL_LSN {
6675            // BIN has never been logged as a full entry — the in-memory delta
6676            // is effectively the full state. During recovery this path is
6677            // harmless.
6678            delta.is_delta = false;
6679            return;
6680        }
6681
6682        // Read the full-BIN log entry at last_full_lsn.
6683        // `envImpl.getLogManager().getEntryHandleFileNotFound(lsn)`.
6684        match log_manager.read_entry(delta.last_full_lsn) {
6685            Ok((entry_type, payload)) => {
6686                use noxu_log::LogEntryType;
6687                if entry_type == LogEntryType::BIN {
6688                    if let Some(mut base) = BinStub::deserialize_full(&payload)
6689                    {
6690                        // Set the base's last_full_lsn so it is preserved
6691                        // into the merged result.
6692                        base.last_full_lsn = delta.last_full_lsn;
6693                        Self::mutate_to_full_bin(delta, base);
6694                        return;
6695                    }
6696                    // Deserialization failed — fall through to graceful degradation.
6697                    log::warn!(
6698                        "mutate_to_full_bin_from_log: failed to deserialize \
6699                         full BIN at LSN {:?}; keeping delta as-is",
6700                        delta.last_full_lsn
6701                    );
6702                } else {
6703                    log::warn!(
6704                        "mutate_to_full_bin_from_log: expected BIN entry at \
6705                         LSN {:?}, got {:?}",
6706                        delta.last_full_lsn,
6707                        entry_type
6708                    );
6709                }
6710            }
6711            Err(e) => {
6712                log::warn!(
6713                    "mutate_to_full_bin_from_log: failed to read log at \
6714                     LSN {:?}: {}",
6715                    delta.last_full_lsn,
6716                    e
6717                );
6718            }
6719        }
6720
6721        // Graceful degradation: promote the delta to a "full" BIN without
6722        // the base slots.  The BIN will be re-logged as a full BIN at the
6723        // next checkpoint.
6724        delta.is_delta = false;
6725        delta.dirty = true;
6726    }
6727
6728    // ========================================================================
6729    // getNextBin / getPrevBin
6730    // ========================================================================
6731
6732    /// Return the entries of the BIN immediately to the right of the BIN
6733    /// that contains (or would contain) `current_key`.
6734    ///
6735    /// → `Tree.getNextIN(forward=true)`.
6736    ///
6737    /// # Algorithm
6738    /// 1. Build a root-to-BIN path for `current_key`.
6739    /// 2. Walk the path back up looking for a parent that has a slot to the
6740    ///    right of the slot we descended through.
6741    /// 3. When found, descend to the leftmost BIN of that sibling subtree.
6742    /// 4. If no such parent exists, return `None` (no next BIN).
6743    pub fn get_next_bin(
6744        &self,
6745        current_key: &[u8],
6746    ) -> Option<Vec<(BinEntry, Lsn, Vec<u8>)>> {
6747        let root = self.get_root()?;
6748        self.get_adjacent_bin(&root, current_key, true)
6749    }
6750
6751    /// Return the entries of the BIN immediately to the left of the BIN
6752    /// that contains (or would contain) `current_key`.
6753    ///
6754    /// → `Tree.getNextIN(forward=false)`.
6755    pub fn get_prev_bin(
6756        &self,
6757        current_key: &[u8],
6758    ) -> Option<Vec<(BinEntry, Lsn, Vec<u8>)>> {
6759        let root = self.get_root()?;
6760        self.get_adjacent_bin(&root, current_key, false)
6761    }
6762
6763    /// Core implementation shared by `get_next_bin` and `get_prev_bin`.
6764    ///
6765    /// Builds the path from `root` down to the BIN for `current_key`
6766    /// (each element records the parent arc, the slot index taken,
6767    /// and the child Arc reached) using `read_arc()` hand-over-hand
6768    /// latch coupling.
6769    ///
6770    /// The ascent re-acquires the parent's read lock one level at a
6771    /// time. To handle a concurrent split that completes between
6772    /// path capture and ascent, we validate that the slot still
6773    /// holds the child Arc we descended through. If the slot
6774    /// mismatches we retry the whole operation from root with a
6775    /// short pause between attempts. The retry budget is generous
6776    /// (`MAX_ASCENT_ATTEMPTS`) so that the typical case of a few
6777    /// cascading splits between two BIN-level cursor steps is
6778    /// absorbed without surfacing as a false end-of-iteration.
6779    /// After exhausting the budget we conservatively return `None`,
6780    /// signalling "no adjacent BIN found"; the cursor will then
6781    /// either restart its scan or report end-of-iteration. The
6782    /// budget is finite so a pathological workload (a thread
6783    /// permanently splitting under us) cannot livelock the lookup.
6784    /// JE `Tree.getNextIN` / `Tree.getPrevIN`.
6785    ///
6786    /// R3 fix (2026-06-16): converted from `static fn` to `&self` so that the
6787    /// IN-level descent uses `self.upper_in_floor_index` (comparator-aware)
6788    /// instead of a raw byte `<=`. Without this, databases with a custom
6789    /// comparator (secondary indexes, sorted-dup) could descend to the wrong
6790    /// child → wrong adjacent BIN → incorrect cursor iteration across BIN
6791    /// boundaries. Mirrors `Tree.getNextIN`/`Tree.getPrevIN` using the
6792    /// comparator-aware `IN.findEntry`.
6793    fn get_adjacent_bin(
6794        &self,
6795        root: &Arc<RwLock<TreeNode>>,
6796        current_key: &[u8],
6797        forward: bool,
6798    ) -> Option<Vec<(BinEntry, Lsn, Vec<u8>)>> {
6799        const MAX_ASCENT_ATTEMPTS: u32 = 8;
6800        for attempt in 0..MAX_ASCENT_ATTEMPTS {
6801            match self.get_adjacent_bin_attempt(root, current_key, forward) {
6802                AdjacentBinOutcome::Found(v) => return Some(v),
6803                AdjacentBinOutcome::NoAdjacent => return None,
6804                AdjacentBinOutcome::SplitRaceRetry => {
6805                    // Brief pause to let the splitter finish.
6806                    if attempt + 1 < MAX_ASCENT_ATTEMPTS {
6807                        std::thread::yield_now();
6808                    }
6809                }
6810            }
6811        }
6812        // Exhausted retry budget. Signal "no adjacent" so the
6813        // cursor can fall back to its end-of-iteration path.
6814        None
6815    }
6816
6817    /// One attempt at `get_adjacent_bin`. The tri-state return
6818    /// value distinguishes "no adjacent BIN exists" (which the
6819    /// caller should propagate as end-of-iteration) from "a
6820    /// concurrent split invalidated our path" (which the caller
6821    /// should retry from root).
6822    fn get_adjacent_bin_attempt(
6823        &self,
6824        root: &Arc<RwLock<TreeNode>>,
6825        current_key: &[u8],
6826        forward: bool,
6827    ) -> AdjacentBinOutcome {
6828        // Path entry: (parent_arc, slot_idx_taken, child_arc_reached).
6829        // The child Arc lets the ascent validate that the slot still
6830        // points to the same node we descended through.
6831        let mut path: Vec<(
6832            Arc<RwLock<TreeNode>>,
6833            usize,
6834            Arc<RwLock<TreeNode>>,
6835        )> = Vec::new();
6836
6837        let mut guard: NodeArcReadGuard = root.read_arc();
6838        loop {
6839            if guard.is_bin() {
6840                break;
6841            }
6842
6843            let (next_arc, slot_idx) = match &*guard {
6844                TreeNode::Internal(n) => {
6845                    if n.entries.is_empty() {
6846                        return AdjacentBinOutcome::NoAdjacent;
6847                    }
6848                    // R3 fix: use comparator-aware upper_in_floor_index so
6849                    // that custom-comparator / sorted-dup databases descend
6850                    // to the correct child. Mirrors JE Tree.getNextIN which
6851                    // uses IN.findEntry (comparator-aware) not raw byte order.
6852                    let idx =
6853                        self.upper_in_floor_index(&n.entries, current_key);
6854                    let child = match n.get_child(idx) {
6855                        Some(c) => c,
6856                        None => return AdjacentBinOutcome::NoAdjacent,
6857                    };
6858                    (child, idx)
6859                }
6860                TreeNode::Bottom(_) => unreachable!(),
6861            };
6862
6863            // Record the parent and the child we are about to enter
6864            // — the child Arc lets the ascent validate the slot.
6865            let parent_arc = NodeArcReadGuard::rwlock(&guard).clone();
6866            path.push((parent_arc, slot_idx, Arc::clone(&next_arc)));
6867
6868            // Hand-over-hand: take child read lock BEFORE releasing parent.
6869            let next_guard = next_arc.read_arc();
6870            drop(guard);
6871            guard = next_guard;
6872        }
6873        drop(guard);
6874
6875        // Ascend the path. At each level, validate that
6876        // `parent.entries[taken_idx].child == descended_child` before
6877        // trusting `taken_idx` as a coordinate. If not, return
6878        // `SplitRaceRetry` so the caller restarts from root.
6879        while let Some((parent_arc, taken_idx, descended_child)) = path.pop() {
6880            let parent_guard = parent_arc.read();
6881            let (n_entries, slot_still_valid) = match &*parent_guard {
6882                TreeNode::Internal(p) => {
6883                    let n = p.entries.len();
6884                    let valid = p
6885                        .child_ref(taken_idx)
6886                        .is_some_and(|c| Arc::ptr_eq(c, &descended_child));
6887                    (n, valid)
6888                }
6889                _ => return AdjacentBinOutcome::NoAdjacent,
6890            };
6891            drop(parent_guard);
6892
6893            if !slot_still_valid {
6894                return AdjacentBinOutcome::SplitRaceRetry;
6895            }
6896
6897            let sibling_idx = if forward {
6898                taken_idx + 1
6899            } else if taken_idx == 0 {
6900                // No left sibling at this level — ascend further.
6901                continue;
6902            } else {
6903                taken_idx - 1
6904            };
6905
6906            if forward && sibling_idx >= n_entries {
6907                // No right sibling at this level — ascend further.
6908                continue;
6909            }
6910
6911            // Found a sibling slot — fetch the sibling child arc.
6912            let sibling_arc = {
6913                let g = parent_arc.read();
6914                match &*g {
6915                    TreeNode::Internal(p) => match p.get_child(sibling_idx) {
6916                        Some(c) => c,
6917                        None => return AdjacentBinOutcome::NoAdjacent,
6918                    },
6919                    _ => return AdjacentBinOutcome::NoAdjacent,
6920                }
6921            };
6922
6923            // Descend to the leftmost (forward) or rightmost (!forward) BIN.
6924            return match Self::descend_to_edge_bin(&sibling_arc, forward) {
6925                Some(v) => AdjacentBinOutcome::Found(v),
6926                None => AdjacentBinOutcome::NoAdjacent,
6927            };
6928        }
6929
6930        // Exhausted path without finding a sibling → no adjacent BIN.
6931        AdjacentBinOutcome::NoAdjacent
6932    }
6933
6934    /// Descend to the leftmost BIN (`forward = true`) or rightmost BIN
6935    /// (`forward = false`) in the sub-tree rooted at `node_arc`.
6936    ///
6937    /// `Tree.searchSubTree(SearchType.LEFT / RIGHT, targetLevel)`.
6938    fn descend_to_edge_bin(
6939        node_arc: &Arc<RwLock<TreeNode>>,
6940        forward: bool,
6941    ) -> Option<Vec<(BinEntry, Lsn, Vec<u8>)>> {
6942        // Hand-over-hand latch coupling — see Tree::search.
6943        let mut guard: NodeArcReadGuard = node_arc.read_arc();
6944
6945        loop {
6946            if guard.is_bin() {
6947                return match &*guard {
6948                    TreeNode::Bottom(b) => {
6949                        // Return entries with full (decompressed) keys so that
6950                        // callers always work with complete keys.
6951                        //
6952                        // TREE-F1: KD slots are NOT filtered here — the BIN's
6953                        // slot indices are returned verbatim so the cursor can
6954                        // skip KD slots itself (CursorImpl getNext loop;
6955                        // CursorImpl.java:2062-2064) and continue to the next
6956                        // BIN when an edge BIN is entirely KD during the
6957                        // BIN-delta reconstitution window.
6958                        let full_entries: Vec<(BinEntry, Lsn, Vec<u8>)> = (0
6959                            ..b.entries.len())
6960                            .map(|i| {
6961                                (
6962                                    BinEntry {
6963                                        data: b.entries[i].data.clone(),
6964                                        known_deleted: b.entries[i]
6965                                            .known_deleted,
6966                                        dirty: b.entries[i].dirty,
6967                                        expiration_time: b.entries[i]
6968                                            .expiration_time,
6969                                    },
6970                                    b.get_lsn(i),
6971                                    b.get_full_key(i).unwrap_or_default(),
6972                                )
6973                            })
6974                            .collect();
6975                        Some(full_entries)
6976                    }
6977                    _ => None,
6978                };
6979            }
6980
6981            let next = match &*guard {
6982                TreeNode::Internal(n) => {
6983                    if forward {
6984                        n.get_child(0)?
6985                    } else {
6986                        n.get_child(n.entries.len().saturating_sub(1))?
6987                    }
6988                }
6989                _ => return None,
6990            };
6991            // Take child read lock BEFORE releasing parent's.
6992            let next_guard = next.read_arc();
6993            drop(guard);
6994            guard = next_guard;
6995        }
6996    }
6997}
6998
6999// ============================================================================
7000// Tree statistics
7001// ============================================================================
7002
7003/// Statistics collected by a full tree walk.
7004///
7005/// `TreeWalkerStatsAccumulator`.
7006#[derive(Debug, Default, Clone, PartialEq, Eq)]
7007pub struct TreeStats {
7008    /// Number of BINs (bottom internal nodes).
7009    pub n_bins: u64,
7010    /// Number of upper INs.
7011    pub n_ins: u64,
7012    /// Total number of entries across all nodes.
7013    pub n_entries: u64,
7014    /// Height of the tree (1 = root is a BIN, 2 = one level above BINs, …).
7015    pub height: u32,
7016}
7017
7018impl Tree {
7019    /// Walks the entire tree and collects structural statistics.
7020    ///
7021    /// `TreeWalkerStatsAccumulator` pattern — performs a simple
7022    /// recursive DFS and counts INs, BINs, entries, and tree height.
7023    pub fn collect_stats(&self) -> TreeStats {
7024        let mut stats = TreeStats::default();
7025        if let Some(root) = self.get_root() {
7026            Self::collect_stats_recursive(&root, &mut stats, 0);
7027        }
7028        stats
7029    }
7030
7031    fn collect_stats_recursive(
7032        node_arc: &Arc<RwLock<TreeNode>>,
7033        stats: &mut TreeStats,
7034        depth: u32,
7035    ) {
7036        let guard = node_arc.read();
7037
7038        let current_height = depth + 1;
7039        if current_height > stats.height {
7040            stats.height = current_height;
7041        }
7042
7043        match &*guard {
7044            TreeNode::Bottom(b) => {
7045                stats.n_bins += 1;
7046                stats.n_entries += b.entries.len() as u64;
7047            }
7048            TreeNode::Internal(n) => {
7049                stats.n_ins += 1;
7050                stats.n_entries += n.entries.len() as u64;
7051                // Collect child arcs before releasing the guard.
7052                let children: Vec<Arc<RwLock<TreeNode>>> =
7053                    n.resident_children();
7054                // Release guard before recursing to avoid lock ordering issues.
7055                drop(guard);
7056                for child in children {
7057                    Self::collect_stats_recursive(&child, stats, depth + 1);
7058                }
7059            }
7060        }
7061    }
7062
7063    /// Collects all dirty BINs as (Arc to node, db_id) pairs.
7064    ///
7065    /// The checkpoint path calls this to enumerate BINs that need to be
7066    /// logged.  For each dirty BIN the checkpoint decides — based on the
7067    /// BIN-delta threshold — whether to write a full `BIN` entry or a
7068    /// `BINDelta` entry.
7069    ///
7070    /// `Checkpointer.processINList()` which iterates the dirty
7071    /// IN list accumulated during normal operation.
7072    pub fn collect_dirty_bins(
7073        &self,
7074        db_id: u64,
7075    ) -> Vec<(u64, Arc<RwLock<TreeNode>>)> {
7076        let mut result = Vec::new();
7077        if let Some(root) = self.get_root() {
7078            Self::collect_dirty_bins_recursive(&root, db_id, &mut result);
7079        }
7080        result
7081    }
7082
7083    fn collect_dirty_bins_recursive(
7084        node_arc: &Arc<RwLock<TreeNode>>,
7085        db_id: u64,
7086        out: &mut Vec<(u64, Arc<RwLock<TreeNode>>)>,
7087    ) {
7088        let guard = node_arc.read();
7089        match &*guard {
7090            TreeNode::Bottom(b) => {
7091                // Include this BIN if it is dirty or has any dirty slots.
7092                if b.dirty || b.dirty_count() > 0 {
7093                    out.push((db_id, Arc::clone(node_arc)));
7094                }
7095            }
7096            TreeNode::Internal(n) => {
7097                let children: Vec<Arc<RwLock<TreeNode>>> =
7098                    n.resident_children();
7099                drop(guard);
7100                for child in children {
7101                    Self::collect_dirty_bins_recursive(&child, db_id, out);
7102                } // guard already dropped
7103            }
7104        }
7105    }
7106
7107    /// Collect all BINs that have at least one `known_deleted` slot.
7108    ///
7109    /// INCompressor queue-drain scan in the: the daemon iterates
7110    /// the in-memory IN list and identifies BINs that still hold zombie deleted
7111    /// slots.  Each returned `Arc` can be passed directly to `compress_bin()`.
7112    pub fn collect_bins_with_known_deleted(
7113        &self,
7114    ) -> Vec<Arc<RwLock<TreeNode>>> {
7115        let mut result = Vec::new();
7116        if let Some(root) = self.get_root() {
7117            Self::collect_bins_with_known_deleted_recursive(&root, &mut result);
7118        }
7119        result
7120    }
7121
7122    fn collect_bins_with_known_deleted_recursive(
7123        node_arc: &Arc<RwLock<TreeNode>>,
7124        out: &mut Vec<Arc<RwLock<TreeNode>>>,
7125    ) {
7126        let guard = node_arc.read();
7127        match &*guard {
7128            TreeNode::Bottom(b) => {
7129                if b.entries.iter().any(|e| e.known_deleted) {
7130                    out.push(Arc::clone(node_arc));
7131                }
7132            }
7133            TreeNode::Internal(n) => {
7134                let children: Vec<Arc<RwLock<TreeNode>>> =
7135                    n.resident_children();
7136                drop(guard);
7137                for child in children {
7138                    Self::collect_bins_with_known_deleted_recursive(
7139                        &child, out,
7140                    );
7141                }
7142            }
7143        }
7144    }
7145
7146    /// Collect all dirty upper (non-BIN) internal nodes, sorted ascending by
7147    /// level (bottom-up order, BIN level excluded).
7148    ///
7149    /// Serialise an upper-IN node (level > 1) by node_id for off-heap storage.
7150    ///
7151    /// Traverses the tree to find the internal node whose  matches,
7152    /// then calls  to produce a compact byte
7153    /// representation.  Returns  if the node is not found or is a BIN
7154    /// (BINs are not upper INs).
7155    ///
7156    /// Mirrors `OffHeapAllocator` serialises the same bytes that would be written
7157    /// to the log, allowing the evictor to store upper-INs off-heap and avoid
7158    /// log-file reads on the next traversal.
7159    pub fn serialize_upper_in(&self, node_id: u64) -> Option<Vec<u8>> {
7160        let root = self.get_root()?;
7161        Self::find_and_serialize_upper_in(&root, node_id)
7162    }
7163
7164    fn find_and_serialize_upper_in(
7165        node_arc: &Arc<RwLock<TreeNode>>,
7166        target_id: u64,
7167    ) -> Option<Vec<u8>> {
7168        let guard = node_arc.read();
7169        match &*guard {
7170            TreeNode::Bottom(_) => None, // BINs are not upper INs
7171            TreeNode::Internal(n) => {
7172                if n.node_id == target_id {
7173                    // Serialise InNodeStub for off-heap storage.
7174                    // Format: node_id(u64BE) | level(i32BE) | n_entries(u32BE)
7175                    //   then per-entry: key_len(u32BE) | key | lsn(u64BE)
7176                    let mut buf = Vec::new();
7177                    buf.extend_from_slice(&n.node_id.to_be_bytes());
7178                    buf.extend_from_slice(&n.level.to_be_bytes());
7179                    buf.extend_from_slice(
7180                        &(n.entries.len() as u32).to_be_bytes(),
7181                    );
7182                    for (i, e) in n.entries.iter().enumerate() {
7183                        buf.extend_from_slice(
7184                            &(e.key.len() as u32).to_be_bytes(),
7185                        );
7186                        buf.extend_from_slice(&e.key);
7187                        buf.extend_from_slice(
7188                            &n.get_lsn(i).as_u64().to_be_bytes(),
7189                        );
7190                    }
7191                    return Some(buf);
7192                }
7193                // Recurse into children before releasing the guard so we
7194                // hold the minimum read-lock duration.
7195                let children: Vec<Arc<RwLock<TreeNode>>> =
7196                    n.resident_children();
7197                drop(guard);
7198                for child in &children {
7199                    if let Some(bytes) =
7200                        Self::find_and_serialize_upper_in(child, target_id)
7201                    {
7202                        return Some(bytes);
7203                    }
7204                }
7205                None
7206            }
7207        }
7208    }
7209
7210    /// Upper-IN traversal in `Checkpointer.processINList()` from
7211    /// — visits all `TreeNode::Internal` nodes whose `dirty` flag is set
7212    /// and returns them together with their level, sorted lowest-level-first
7213    /// so the checkpointer can log them bottom-up.  The root is always the
7214    /// last entry (highest level), which must be logged `Provisional::No`.
7215    pub fn collect_dirty_upper_ins(
7216        &self,
7217        _db_id: u64,
7218    ) -> Vec<(i32, Arc<RwLock<TreeNode>>)> {
7219        let mut result: Vec<(i32, Arc<RwLock<TreeNode>>)> = Vec::new();
7220        if let Some(root) = self.get_root() {
7221            Self::collect_dirty_upper_ins_recursive(&root, &mut result);
7222        }
7223        result.sort_by_key(|(level, _)| *level);
7224        result
7225    }
7226
7227    fn collect_dirty_upper_ins_recursive(
7228        node_arc: &Arc<RwLock<TreeNode>>,
7229        out: &mut Vec<(i32, Arc<RwLock<TreeNode>>)>,
7230    ) {
7231        let guard = node_arc.read();
7232        match &*guard {
7233            TreeNode::Bottom(_) => {
7234                // BINs are handled by flush_dirty_bins_internal; skip here.
7235            }
7236            TreeNode::Internal(n) => {
7237                let is_dirty = n.dirty;
7238                // REC-AA: return the node's ACTUAL tree level (n.level, in
7239                // MAIN_LEVEL|n units), not a root-relative depth.  The level
7240                // must be on the same scale as a BIN's `level` (BIN_LEVEL =
7241                // MAIN_LEVEL|1) so that the checkpointer's flush-level
7242                // computation and the evictor's `node_level < flush_level`
7243                // comparison are meaningful.  With a root-relative depth the
7244                // root had the SMALLEST value (0) and the IN above the BINs
7245                // the LARGEST, inverting the provisional/non-provisional
7246                // boundary; with n.level the root has the largest level, as JE
7247                // expects.
7248                let level = n.level;
7249                let children: Vec<Arc<RwLock<TreeNode>>> =
7250                    n.resident_children();
7251                drop(guard);
7252                // Recurse into children first (bottom-up ordering).
7253                for child in &children {
7254                    Self::collect_dirty_upper_ins_recursive(child, out);
7255                }
7256                // Add this node after children (so parent comes after all descendants).
7257                if is_dirty {
7258                    out.push((level, Arc::clone(node_arc)));
7259                }
7260            }
7261        }
7262    }
7263
7264    // ========================================================================
7265    // Tree.java ports: 8 additional tree methods (Task #82)
7266    // ========================================================================
7267
7268    /// Returns `true` if the root node is currently loaded in memory.
7269    ///
7270    /// .
7271    pub fn is_root_resident(&self) -> bool {
7272        self.root.read().is_some()
7273    }
7274
7275    /// Returns the root node `Arc` if present, or `None`.
7276    ///
7277    /// .
7278    pub fn get_resident_root_in(&self) -> Option<Arc<RwLock<TreeNode>>> {
7279        self.root.read().clone()
7280    }
7281
7282    /// Returns the BIN that should contain a slot for `key` (the "parent" of
7283    /// LN slots).
7284    ///
7285    /// .  Descends the tree
7286    /// exactly like `search()` and returns the leaf-level BIN arc, or `None`
7287    /// if the tree is empty.
7288    ///
7289    /// Uses `read_arc()` hand-over-hand on the descent — the child
7290    /// guard is taken before the parent guard is dropped, matching
7291    /// `search()`. Returns the BIN Arc with no read lock held; the
7292    /// caller must take whatever lock it needs to operate on the
7293    /// returned BIN.
7294    pub fn get_parent_bin_for_child_ln(
7295        &self,
7296        key: &[u8],
7297    ) -> Option<Arc<RwLock<TreeNode>>> {
7298        let root = self.get_root()?;
7299        let mut current_arc: Arc<RwLock<TreeNode>> = root.clone();
7300        let mut guard: NodeArcReadGuard = root.read_arc();
7301
7302        loop {
7303            if guard.is_bin() {
7304                drop(guard);
7305                return Some(current_arc);
7306            }
7307
7308            let parent_arc = current_arc.clone();
7309            let next_idx = match &*guard {
7310                TreeNode::Internal(n) => {
7311                    if n.entries.is_empty() {
7312                        return None;
7313                    }
7314                    let idx = self.upper_in_floor_index(&n.entries, key);
7315                    match n.get_child(idx) {
7316                        Some(c) => {
7317                            let next_guard = c.read_arc();
7318                            drop(guard);
7319                            current_arc = c;
7320                            guard = next_guard;
7321                            continue;
7322                        }
7323                        None => idx, // EV-14/EV-13: re-fetch below.
7324                    }
7325                }
7326                TreeNode::Bottom(_) => {
7327                    unreachable!("is_bin() returned false above")
7328                }
7329            };
7330            // Hand-over-hand: take child guard before dropping parent.
7331            drop(guard);
7332            let child = self.child_at_or_fetch(&parent_arc, next_idx)?;
7333            let next_guard = child.read_arc();
7334            current_arc = child;
7335            guard = next_guard;
7336        }
7337    }
7338
7339    /// Returns the BIN where `key` should be inserted.
7340    ///
7341    /// .  Semantically identical to
7342    /// `get_parent_bin_for_child_ln` — expressed as a separate method to match
7343    /// API surface.
7344    ///
7345    /// Implemented as a delegation to `get_parent_bin_for_child_ln`,
7346    /// which uses `read_arc()` hand-over-hand on the descent.
7347    pub fn find_bin_for_insert(
7348        &self,
7349        key: &[u8],
7350    ) -> Option<Arc<RwLock<TreeNode>>> {
7351        self.get_parent_bin_for_child_ln(key)
7352    }
7353
7354    /// Search for a BIN, allowing splits during descent (preemptive splitting).
7355    ///
7356    /// .  This thin wrapper
7357    /// delegates to `search()` and returns the result wrapped in `Some`.
7358    /// The full split-allowed descent is performed by `insert()` internally;
7359    /// this method exposes the same result type for callers that only need to
7360    /// locate the BIN.
7361    ///
7362    /// Returns `None` if the tree is empty.
7363    pub fn search_splits_allowed(&self, key: &[u8]) -> Option<SearchResult> {
7364        self.search(key)
7365    }
7366
7367    /// Traverses the entire tree and returns every IN and BIN node as a flat
7368    /// list.
7369    ///
7370    /// .  Used by recovery to rebuild
7371    /// the in-memory IN list after log replay.  The walk is a BFS from the
7372    /// root; every `Arc<RwLock<TreeNode>>` encountered (both Internal and
7373    /// Bottom variants) is included in the result.
7374    pub fn rebuild_in_list(&self) -> Vec<Arc<RwLock<TreeNode>>> {
7375        let mut result = Vec::new();
7376        if let Some(root) = self.get_root() {
7377            Self::rebuild_in_list_recursive(&root, &mut result);
7378        }
7379        result
7380    }
7381
7382    fn rebuild_in_list_recursive(
7383        node_arc: &Arc<RwLock<TreeNode>>,
7384        out: &mut Vec<Arc<RwLock<TreeNode>>>,
7385    ) {
7386        // Push this node unconditionally — both INs and BINs belong in the list.
7387        out.push(Arc::clone(node_arc));
7388
7389        let guard = node_arc.read();
7390
7391        if let TreeNode::Internal(n) = &*guard {
7392            // Collect child arcs while holding the guard, then drop it before
7393            // recursing to avoid holding multiple locks simultaneously.
7394            let children: Vec<Arc<RwLock<TreeNode>>> = n.resident_children();
7395            drop(guard);
7396            for child in children {
7397                Self::rebuild_in_list_recursive(&child, out);
7398            }
7399        }
7400        // BIN nodes are leaves — no children to recurse into.
7401    }
7402
7403    /// Validates internal tree consistency.
7404    ///
7405    /// .  Primarily a debug/test tool.
7406    ///
7407    /// Rules checked:
7408    /// - An empty tree (no root) is trivially valid → returns `true`.
7409    /// - A non-empty tree must have a non-null root.
7410    /// - Every Internal node must have at least one entry.
7411    /// - Every child pointer that is `Some` must be readable (lock must be
7412    ///   acquirable — i.e., no poisoned locks).
7413    ///
7414    /// Returns `true` if no inconsistencies are detected, `false` otherwise.
7415    pub fn validate_in_list(&self) -> bool {
7416        match self.get_root() {
7417            None => true, // empty tree is always valid
7418            Some(root) => Self::validate_node(&root),
7419        }
7420    }
7421
7422    fn validate_node(node_arc: &Arc<RwLock<TreeNode>>) -> bool {
7423        let guard = node_arc.read();
7424
7425        match &*guard {
7426            TreeNode::Bottom(_bin) => {
7427                // BIN nodes are always structurally valid at this level.
7428                true
7429            }
7430            TreeNode::Internal(n) => {
7431                // An Internal node must have at least one entry.
7432                if n.entries.is_empty() {
7433                    return false;
7434                }
7435                // Collect child arcs before dropping the guard.
7436                let children: Vec<Arc<RwLock<TreeNode>>> =
7437                    n.resident_children();
7438                drop(guard);
7439                // Recursively validate every resident child.
7440                for child in children {
7441                    if !Self::validate_node(&child) {
7442                        return false;
7443                    }
7444                }
7445                true
7446            }
7447        }
7448    }
7449
7450    /// Traverses the tree to find the parent IN that contains `child_node_id`
7451    /// as one of its child slots.
7452    ///
7453    /// .  Used by the cleaner
7454    /// migration path to re-insert migrated INs after eviction/fetch.
7455    ///
7456    /// Returns `(parent_arc, slot_index)` where `slot_index` is the position
7457    /// in the parent's `entries` vector whose child matches `child_node_id`,
7458    /// or `None` if no such parent is found.
7459    pub fn get_parent_in_for_child_in(
7460        &self,
7461        child_node_id: u64,
7462    ) -> Option<(Arc<RwLock<TreeNode>>, usize)> {
7463        let root = self.get_root()?;
7464        Self::find_parent_of_node_id(&root, child_node_id)
7465    }
7466
7467    /// Recursive DFS helper for `get_parent_in_for_child_in`.
7468    ///
7469    /// Scans every entry in each Internal node.  When a child's node_id
7470    /// matches `target_id` the parent arc and slot index are returned.
7471    fn find_parent_of_node_id(
7472        node_arc: &Arc<RwLock<TreeNode>>,
7473        target_id: u64,
7474    ) -> Option<(Arc<RwLock<TreeNode>>, usize)> {
7475        let guard = node_arc.read();
7476
7477        let TreeNode::Internal(n) = &*guard else {
7478            // BIN nodes have no IN children — cannot be a parent of another IN.
7479            return None;
7480        };
7481
7482        // Check whether any child of this IN has the target node_id.
7483        let mut children: Vec<(usize, Arc<RwLock<TreeNode>>)> = Vec::new();
7484        for slot in 0..n.entries.len() {
7485            if let Some(child_arc) = n.child_ref(slot) {
7486                // Read the child's node_id under a separate lock (acquire child
7487                // while parent guard is still held — this is intentional for
7488                // the ID comparison only; we release both immediately after).
7489                let child_id = {
7490                    let cg = child_arc.read();
7491                    match &*cg {
7492                        TreeNode::Internal(cn) => cn.node_id,
7493                        TreeNode::Bottom(cb) => cb.node_id,
7494                    }
7495                };
7496
7497                if child_id == target_id {
7498                    // Found — return a clone of this node as parent.
7499                    let parent_clone = Arc::clone(node_arc);
7500                    return Some((parent_clone, slot));
7501                }
7502
7503                // Not found at this slot; schedule this child for recursion.
7504                children.push((slot, Arc::clone(child_arc)));
7505            }
7506        }
7507        // Release parent guard before recursing.
7508        drop(guard);
7509
7510        // Recurse into each Internal child.
7511        for (_slot, child_arc) in children {
7512            if let Some(result) =
7513                Self::find_parent_of_node_id(&child_arc, target_id)
7514            {
7515                return Some(result);
7516            }
7517        }
7518
7519        None
7520    }
7521
7522    /// Propagates the dirty flag upward from `node_arc` to the root.
7523    ///
7524    /// Implicit dirty propagation: after modifying any node,
7525    /// all ancestors on the path to the root must also be marked dirty so
7526    /// the checkpointer logs them.
7527    ///
7528    /// In this happens through `IN.setDirty(true)` calls at each level
7529    /// during split/insert callbacks.  Here we walk the weak parent chain.
7530    /// Reconstitute a BIN-delta by merging it onto a base full BIN.
7531    ///
7532    /// Implements JE `BINDelta.reconstituteBIN(databaseImpl)` for the recovery
7533    /// path where the log manager is not available as a `LogManager` but as
7534    /// raw serialized bytes.
7535    ///
7536    /// Algorithm:
7537    /// 1. Deserialise `base_bytes` as a full `BinStub`.
7538    /// 2. Apply `delta_bytes` slots onto the base using `BinStub::apply_delta`
7539    ///    (raw slot overlay).
7540    /// 3. Recompute key prefix so prefix-compressed entries are consistent.
7541    ///
7542    /// Returns `None` if either byte slice is malformed.
7543    ///
7544    /// JE `BINDelta.reconstituteBIN` / `BINDelta.applyDelta`
7545    /// (DRIFT-10 / Stage 3).
7546    pub fn reconstitute_bin_delta(
7547        base_bytes: &[u8],
7548        delta_bytes: &[u8],
7549    ) -> Option<BinStub> {
7550        let mut base = BinStub::deserialize_full(base_bytes)?;
7551        // Apply the delta slots onto the base.
7552        // Note: BinStub::apply_delta uses slot-index addressing into base.entries,
7553        // extending with new entries when the slot_idx >= base.entries.len().
7554        // After apply_delta we recompute the key prefix to fix prefix compression.
7555        BinStub::apply_delta(&mut base, delta_bytes)?;
7556        // Recompute prefix so prefix-compressed BINs are consistent after merge.
7557        base.recompute_key_prefix();
7558        base.is_delta = false;
7559        base.dirty = false;
7560        Some(base)
7561    }
7562
7563    pub fn propagate_dirty_to_root(node_arc: &Arc<RwLock<TreeNode>>) {
7564        let parent_weak = { node_arc.read().get_parent() };
7565
7566        if let Some(parent_arc) = parent_weak.and_then(|w| w.upgrade()) {
7567            {
7568                let mut g = parent_arc.write();
7569                g.set_dirty(true);
7570            }
7571            // Recurse further up.
7572            Self::propagate_dirty_to_root(&parent_arc);
7573        }
7574    }
7575
7576    // ========================================================================
7577    // IN-redo: JE RecoveryManager.recoverIN / recoverRootIN / recoverChildIN
7578    // ========================================================================
7579
7580    /// Deserialise an upper-IN node from bytes produced by
7581    /// `TreeNode::write_to_bytes()` / `flush_one_tree_upper_ins`.
7582    ///
7583    /// Format: node_id(u64BE) | level(i32BE) | n_entries(u32BE) | dirty(u8)
7584    ///   | per-entry: key_len(u16BE) | key | lsn(u64BE)
7585    ///
7586    /// JE `INFileReader.getIN(db)` / `IN.readFromLog`.
7587    pub fn deserialize_upper_in(bytes: &[u8]) -> Option<InNodeStub> {
7588        if bytes.len() < 13 {
7589            return None;
7590        }
7591        let node_id = u64::from_be_bytes(bytes[0..8].try_into().ok()?);
7592        let level = i32::from_be_bytes(bytes[8..12].try_into().ok()?);
7593        let n_entries =
7594            u32::from_be_bytes(bytes[12..16].try_into().ok()?) as usize;
7595        // dirty byte (1 byte after n_entries)
7596        if bytes.len() < 17 {
7597            return None;
7598        }
7599        let mut pos = 17usize; // skip node_id(8) + level(4) + n_entries(4) + dirty(1)
7600        let mut entries = Vec::with_capacity(n_entries);
7601        let mut lsns: Vec<Lsn> = Vec::with_capacity(n_entries);
7602        for _ in 0..n_entries {
7603            if pos + 2 > bytes.len() {
7604                return None;
7605            }
7606            let key_len =
7607                u16::from_be_bytes(bytes[pos..pos + 2].try_into().ok()?)
7608                    as usize;
7609            pos += 2;
7610            if pos + key_len > bytes.len() {
7611                return None;
7612            }
7613            let key = bytes[pos..pos + key_len].to_vec();
7614            pos += key_len;
7615            if pos + 8 > bytes.len() {
7616                return None;
7617            }
7618            let lsn = noxu_util::Lsn::from_u64(u64::from_be_bytes(
7619                bytes[pos..pos + 8].try_into().ok()?,
7620            ));
7621            pos += 8;
7622            entries.push(InEntry { key });
7623            lsns.push(lsn); // T-3
7624        }
7625        Some(InNodeStub {
7626            node_id,
7627            level,
7628            entries,
7629            // T-4: a freshly deserialized IN has no resident children.
7630            targets: TargetRep::None,
7631            dirty: false,
7632            generation: 0,
7633            parent: None,
7634            lsn_rep: LsnRep::from_lsns(&lsns), // T-3
7635        })
7636    }
7637
7638    /// Deserialise a BIN from bytes produced by `BinStub::serialize_full()`.
7639    ///
7640    /// Thin wrapper so the recovery path does not need to import `BinStub`
7641    /// directly from callers that only have the raw bytes.
7642    ///
7643    /// JE `INFileReader.getIN(db)` for a BIN entry.
7644    pub fn deserialize_bin(bytes: &[u8]) -> Option<BinStub> {
7645        let mut bin = BinStub::deserialize_full(bytes)?;
7646        bin.dirty = false; // freshly loaded from log — clean for now
7647        Some(bin)
7648    }
7649
7650    /// Apply a logged IN/BIN to the in-memory tree during the recovery redo pass.
7651    ///
7652    /// Implements JE `RecoveryManager.recoverIN`:
7653    /// - `is_root` nodes are handled by `recover_root_in`.
7654    /// - non-root nodes are handled by `recover_child_in`.
7655    ///
7656    /// `log_lsn` is the LSN at which this IN/BIN was logged.  The currency
7657    /// check in `recover_child_in` uses this to decide whether to replace the
7658    /// in-memory slot (tree slot LSN < log_lsn → replace; equal → noop;
7659    /// greater → skip).
7660    ///
7661    /// JE `RecoveryManager.recoverIN` / `replayOneIN`
7662    /// (RecoveryManager.java ~lines 1200–1280).
7663    pub fn recover_in_redo(
7664        &self,
7665        log_lsn: noxu_util::Lsn,
7666        is_root: bool,
7667        is_bin: bool,
7668        node_data: &[u8],
7669    ) -> InRedoResult {
7670        if is_bin {
7671            let Some(bin) = Self::deserialize_bin(node_data) else {
7672                return InRedoResult::DeserializeFailed;
7673            };
7674            if is_root {
7675                self.recover_root_bin(log_lsn, bin)
7676            } else {
7677                self.recover_child_bin(log_lsn, bin)
7678            }
7679        } else {
7680            let Some(upper) = Self::deserialize_upper_in(node_data) else {
7681                return InRedoResult::DeserializeFailed;
7682            };
7683            if is_root {
7684                self.recover_root_upper_in(log_lsn, upper)
7685            } else {
7686                self.recover_child_upper_in(log_lsn, upper)
7687            }
7688        }
7689    }
7690
7691    /// Recover a root BIN.
7692    ///
7693    /// If no root exists or the existing root is older (lower LSN), install
7694    /// this BIN as the new root.
7695    ///
7696    /// JE `RecoveryManager.recoverRootIN` / `RootUpdater.doWork`
7697    /// (RecoveryManager.java ~lines 1293–1410).
7698    fn recover_root_bin(
7699        &self,
7700        log_lsn: noxu_util::Lsn,
7701        bin: BinStub,
7702    ) -> InRedoResult {
7703        let mut root_guard = self.root.write();
7704        let existing_lsn = *self.root_log_lsn.read();
7705        match &*root_guard {
7706            None => {
7707                // No root — install this BIN as the root.
7708                // JE: `root == null` case in `RootUpdater.doWork`.
7709                let node = TreeNode::Bottom(bin);
7710                *root_guard = Some(Arc::new(RwLock::new(node)));
7711                *self.root_log_lsn.write() = log_lsn;
7712                InRedoResult::Inserted
7713            }
7714            Some(_) => {
7715                // JE: `originalLsn = root.getLsn()`; replace if logLsn > originalLsn.
7716                if log_lsn > existing_lsn {
7717                    let node = TreeNode::Bottom(bin);
7718                    *root_guard = Some(Arc::new(RwLock::new(node)));
7719                    *self.root_log_lsn.write() = log_lsn;
7720                    InRedoResult::Replaced
7721                } else {
7722                    InRedoResult::Skipped
7723                }
7724            }
7725        }
7726    }
7727
7728    /// Recover a root upper IN.
7729    ///
7730    /// JE `RecoveryManager.recoverRootIN` for a non-BIN root.
7731    fn recover_root_upper_in(
7732        &self,
7733        log_lsn: noxu_util::Lsn,
7734        upper: InNodeStub,
7735    ) -> InRedoResult {
7736        let mut root_guard = self.root.write();
7737        let existing_lsn = *self.root_log_lsn.read();
7738        match &*root_guard {
7739            None => {
7740                let node = TreeNode::Internal(upper);
7741                *root_guard = Some(Arc::new(RwLock::new(node)));
7742                *self.root_log_lsn.write() = log_lsn;
7743                InRedoResult::Inserted
7744            }
7745            Some(_) => {
7746                if log_lsn > existing_lsn {
7747                    let node = TreeNode::Internal(upper);
7748                    *root_guard = Some(Arc::new(RwLock::new(node)));
7749                    *self.root_log_lsn.write() = log_lsn;
7750                    InRedoResult::Replaced
7751                } else {
7752                    InRedoResult::Skipped
7753                }
7754            }
7755        }
7756    }
7757
7758    /// Recover a non-root BIN.
7759    ///
7760    /// Implements the three-case currency check from JE
7761    /// `RecoveryManager.recoverChildIN`
7762    /// (RecoveryManager.java lines 1412–1500):
7763    ///
7764    /// 1. Node not in tree: skip (parent logged a later structure that already
7765    ///    omits this node, or node was deleted).
7766    /// 2. Physical match (slot LSN == log_lsn): noop — already current.
7767    /// 3. Logical match: another version of the node is in the slot.
7768    ///    Replace if tree slot LSN < log_lsn (tree is older), skip otherwise.
7769    fn recover_child_bin(
7770        &self,
7771        log_lsn: noxu_util::Lsn,
7772        bin: BinStub,
7773    ) -> InRedoResult {
7774        let node_id = bin.node_id;
7775        let Some((parent_arc, slot)) = self.get_parent_in_for_child_in(node_id)
7776        else {
7777            // Case 1: not in tree.
7778            return InRedoResult::NotInTree;
7779        };
7780        let mut parent = parent_arc.write();
7781        let TreeNode::Internal(ref mut p) = *parent else {
7782            return InRedoResult::NotInTree;
7783        };
7784        let tree_lsn = p.get_lsn(slot); // T-3
7785        if tree_lsn == log_lsn {
7786            // Case 2: physical match — noop.
7787            InRedoResult::Skipped
7788        } else if tree_lsn < log_lsn {
7789            // Case 3: logical match, tree is older — replace.
7790            // JE `parent.recoverIN(idx, inFromLog, logLsn, lastLoggedSize)`.
7791            let new_arc = Arc::new(RwLock::new(TreeNode::Bottom(bin)));
7792            // Set parent back-pointer on the new node.
7793            {
7794                let mut ng = new_arc.write();
7795                if let TreeNode::Bottom(ref mut b) = *ng {
7796                    b.parent = Some(Arc::downgrade(&parent_arc));
7797                }
7798            }
7799            p.set_child(slot, Some(new_arc));
7800            p.set_lsn(slot, log_lsn); // T-3
7801            InRedoResult::Replaced
7802        } else {
7803            // tree_lsn > log_lsn: tree already holds a newer version.
7804            InRedoResult::Skipped
7805        }
7806    }
7807
7808    /// Recover a non-root upper IN.
7809    ///
7810    /// JE `RecoveryManager.recoverChildIN` for a non-BIN node.
7811    fn recover_child_upper_in(
7812        &self,
7813        log_lsn: noxu_util::Lsn,
7814        upper: InNodeStub,
7815    ) -> InRedoResult {
7816        let node_id = upper.node_id;
7817        let Some((parent_arc, slot)) = self.get_parent_in_for_child_in(node_id)
7818        else {
7819            return InRedoResult::NotInTree;
7820        };
7821        let mut parent = parent_arc.write();
7822        let TreeNode::Internal(ref mut p) = *parent else {
7823            return InRedoResult::NotInTree;
7824        };
7825        let tree_lsn = p.get_lsn(slot); // T-3
7826        if tree_lsn == log_lsn {
7827            InRedoResult::Skipped
7828        } else if tree_lsn < log_lsn {
7829            let new_arc = Arc::new(RwLock::new(TreeNode::Internal(upper)));
7830            {
7831                let mut ng = new_arc.write();
7832                if let TreeNode::Internal(ref mut n) = *ng {
7833                    n.parent = Some(Arc::downgrade(&parent_arc));
7834                }
7835            }
7836            p.set_child(slot, Some(new_arc));
7837            p.set_lsn(slot, log_lsn); // T-3
7838            InRedoResult::Replaced
7839        } else {
7840            InRedoResult::Skipped
7841        }
7842    }
7843}
7844
7845/// Result of a single `recover_in_redo` call.
7846///
7847/// JE traces the same outcomes in `RecoveryManager` debug logging.
7848#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7849pub enum InRedoResult {
7850    /// Node was inserted as the new root.
7851    Inserted,
7852    /// Node replaced an older version in the tree.
7853    Replaced,
7854    /// Node not applied: tree already holds an equal or newer version.
7855    Skipped,
7856    /// Node not found in tree (parent logged later structure that excludes it).
7857    NotInTree,
7858    /// Deserialisation of `node_data` bytes failed.
7859    DeserializeFailed,
7860}
7861
7862/// Global node ID counter for generating unique node IDs.
7863///
7864/// This is the SINGLE source of node-ids for the whole tree subsystem.  The
7865/// BIN constructor (`bin.rs`) and `node.rs` route through `generate_node_id`
7866/// so that, after crash recovery, a freshly allocated node-id is always
7867/// strictly greater than every node-id present in the recovered log.
7868///
7869/// JE ref: `NodeSequence.getNextLocalNodeId` (a single per-env counter) and
7870/// `IN.nodeId` allocation; `NodeSequence.initRealNodeId` seeds the counter
7871/// from the recovered `CheckpointEnd.lastLocalNodeId`.  The env seeds this
7872/// counter post-recovery via `seed_node_id_counter`.
7873static NODE_ID_COUNTER: std::sync::atomic::AtomicU64 =
7874    std::sync::atomic::AtomicU64::new(1);
7875
7876/// Generates a unique node ID.
7877pub fn generate_node_id() -> u64 {
7878    NODE_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
7879}
7880
7881/// Returns the node-id that would be generated next (without allocating it).
7882///
7883/// Used by recovery seeding and by tests to assert no node-id reuse after a
7884/// restart.
7885pub fn peek_next_node_id_counter() -> u64 {
7886    NODE_ID_COUNTER.load(std::sync::atomic::Ordering::SeqCst)
7887}
7888
7889/// Seeds the node-id counter so the next generated id is `> last_node_id`.
7890///
7891/// Called by `EnvironmentImpl` after recovery with the recovered
7892/// `use_max_node_id`, mirroring `NodeSequence.initRealNodeId` /
7893/// `setLastNodeId`: post-restart allocation must never reuse a node-id that
7894/// is already in the log.  Monotonic: never lowers the counter.
7895pub fn seed_node_id_counter(last_node_id: u64) {
7896    let want_next = last_node_id.saturating_add(1);
7897    // Bump only if our current next is below the recovered floor.
7898    let mut cur = NODE_ID_COUNTER.load(std::sync::atomic::Ordering::SeqCst);
7899    while cur < want_next {
7900        match NODE_ID_COUNTER.compare_exchange_weak(
7901            cur,
7902            want_next,
7903            std::sync::atomic::Ordering::SeqCst,
7904            std::sync::atomic::Ordering::SeqCst,
7905        ) {
7906            Ok(_) => break,
7907            Err(observed) => cur = observed,
7908        }
7909    }
7910}
7911
7912#[cfg(test)]
7913mod tests {
7914    use super::*;
7915
7916    // ====================================================================
7917    // T-3: LsnRep packed-LSN encoding (IN.entryLsnByteArray / getLsn /
7918    // setLsnInternal, IN.java:1752-1935).
7919    // ====================================================================
7920
7921    /// All-NULL node uses the 0-byte Empty rep; reads return NULL_LSN.
7922    #[test]
7923    fn lsnrep_empty_is_zero_bytes() {
7924        let rep = LsnRep::new(64);
7925        assert!(matches!(rep, LsnRep::Empty));
7926        assert_eq!(rep.memory_size(), 0);
7927        assert_eq!(rep.get(0), NULL_LSN);
7928        assert_eq!(rep.get(63), NULL_LSN);
7929    }
7930
7931    /// LSNs sharing a file number pack to the Compact rep (4 bytes/slot,
7932    /// base_file_number-relative) and round-trip exactly.
7933    #[test]
7934    fn lsnrep_compact_roundtrip_same_file() {
7935        let mut rep = LsnRep::new(8);
7936        for i in 0..8u32 {
7937            rep.set(i as usize, Lsn::new(7, 1000 + i), 8);
7938        }
7939        assert!(matches!(rep, LsnRep::Compact { .. }));
7940        for i in 0..8u32 {
7941            assert_eq!(rep.get(i as usize), Lsn::new(7, 1000 + i));
7942        }
7943        // 8 slots * 4 bytes = 32 bytes, far below 8 * 8 = 64 for raw u64.
7944        assert_eq!(rep.memory_size(), 8 * 4);
7945    }
7946
7947    /// NULL_LSN is stored via the 0xffffff file-offset sentinel, NOT u64::MAX,
7948    /// so a node with NULL slots still packs Compact (the blocker JE solves).
7949    #[test]
7950    fn lsnrep_null_does_not_force_long() {
7951        let mut rep = LsnRep::new(4);
7952        rep.set(0, Lsn::new(3, 50), 4);
7953        rep.set(1, NULL_LSN, 4);
7954        rep.set(2, Lsn::new(3, 60), 4);
7955        rep.set(3, NULL_LSN, 4);
7956        assert!(
7957            matches!(rep, LsnRep::Compact { .. }),
7958            "NULL slots must NOT force the Long rep"
7959        );
7960        assert_eq!(rep.get(0), Lsn::new(3, 50));
7961        assert_eq!(rep.get(1), NULL_LSN);
7962        assert_eq!(rep.get(2), Lsn::new(3, 60));
7963        assert_eq!(rep.get(3), NULL_LSN);
7964    }
7965
7966    /// base_file_number tracks the minimum; setting a lower file number
7967    /// re-bases the whole array (adjustFileNumbers) while staying Compact.
7968    #[test]
7969    fn lsnrep_rebase_on_lower_file_number() {
7970        let mut rep = LsnRep::new(3);
7971        rep.set(0, Lsn::new(10, 5), 3);
7972        rep.set(1, Lsn::new(12, 6), 3);
7973        // A lower file number re-bases base_file_number to 8.
7974        rep.set(2, Lsn::new(8, 7), 3);
7975        assert!(matches!(rep, LsnRep::Compact { .. }));
7976        assert_eq!(rep.get(0), Lsn::new(10, 5));
7977        assert_eq!(rep.get(1), Lsn::new(12, 6));
7978        assert_eq!(rep.get(2), Lsn::new(8, 7));
7979    }
7980
7981    /// A file-number spread > 127 forces the Long fallback (mutateToLongArray),
7982    /// still round-tripping every slot.
7983    #[test]
7984    fn lsnrep_mutates_to_long_on_wide_file_range() {
7985        let mut rep = LsnRep::new(2);
7986        rep.set(0, Lsn::new(1, 5), 2);
7987        rep.set(1, Lsn::new(1000, 6), 2); // diff 999 > 127 -> Long
7988        assert!(matches!(rep, LsnRep::Long(_)));
7989        assert_eq!(rep.get(0), Lsn::new(1, 5));
7990        assert_eq!(rep.get(1), Lsn::new(1000, 6));
7991    }
7992
7993    /// A file offset > MAX_FILE_OFFSET (0xfffffe) forces the Long fallback.
7994    #[test]
7995    fn lsnrep_mutates_to_long_on_large_offset() {
7996        let mut rep = LsnRep::new(2);
7997        rep.set(0, Lsn::new(1, 10), 2);
7998        rep.set(1, Lsn::new(1, 0x00ff_ffff), 2); // > MAX_FILE_OFFSET -> Long
7999        assert!(matches!(rep, LsnRep::Long(_)));
8000        assert_eq!(rep.get(1), Lsn::new(1, 0x00ff_ffff));
8001    }
8002
8003    /// insert_shift / remove_shift keep slots aligned (INArrayRep.copy).
8004    #[test]
8005    fn lsnrep_insert_and_remove_shift() {
8006        let mut rep = LsnRep::from_lsns(&[
8007            Lsn::new(2, 1),
8008            Lsn::new(2, 2),
8009            Lsn::new(2, 3),
8010        ]);
8011        // Insert a new slot at index 1.
8012        rep.insert_shift(1, 4);
8013        rep.set(1, Lsn::new(2, 99), 4);
8014        assert_eq!(rep.get(0), Lsn::new(2, 1));
8015        assert_eq!(rep.get(1), Lsn::new(2, 99));
8016        assert_eq!(rep.get(2), Lsn::new(2, 2));
8017        assert_eq!(rep.get(3), Lsn::new(2, 3));
8018        // Remove slot 1.
8019        rep.remove_shift(1);
8020        assert_eq!(rep.get(0), Lsn::new(2, 1));
8021        assert_eq!(rep.get(1), Lsn::new(2, 2));
8022        assert_eq!(rep.get(2), Lsn::new(2, 3));
8023    }
8024
8025    #[test]
8026    fn test_empty_tree() {
8027        let tree = Tree::new(1, 128);
8028        assert!(tree.is_empty());
8029        assert_eq!(tree.get_database_id(), 1);
8030        assert_eq!(tree.get_root_splits(), 0);
8031    }
8032
8033    #[test]
8034    fn test_redo_insert_older_lsn_does_not_overwrite_newer_slot() {
8035        // REC-F2 reproduce-first: redo() must be idempotent w.r.t. slot
8036        // currency.  JE RecoveryManager.redo() (line ~2512/2544) only
8037        // replaces a slot when logrecLsn > treeLsn.  A later redo of an
8038        // OLDER committed LN for the same key must NOT revert the slot to
8039        // the older value or reset the slot LSN backward.
8040        let tree = Tree::new(1, 128);
8041        let key = b"k".to_vec();
8042
8043        // Install the newer version at LSN X (e.g. the BIN-logged value).
8044        let newer = Lsn::new(5, 500);
8045        tree.redo_insert(&key, b"new", newer).unwrap();
8046
8047        // Replay an OLDER committed LN at Y < X for the same key.
8048        let older = Lsn::new(2, 200);
8049        tree.redo_insert(&key, b"old", older).unwrap();
8050
8051        // The newer value and LSN must survive.
8052        let got = tree.search_with_data(&key).expect("key present");
8053        assert!(got.found);
8054        assert_eq!(
8055            got.data.as_deref(),
8056            Some(&b"new"[..]),
8057            "older-LSN redo reverted committed data"
8058        );
8059        assert_eq!(
8060            got.lsn,
8061            newer.as_u64(),
8062            "older-LSN redo reset slot LSN backward"
8063        );
8064
8065        // A redo at a strictly NEWER LSN must still replace (replace-only
8066        // when log_lsn > slot_lsn, matching JE lsnCmp > 0).
8067        let newest = Lsn::new(9, 900);
8068        tree.redo_insert(&key, b"newest", newest).unwrap();
8069        let got = tree.search_with_data(&key).expect("key present");
8070        assert_eq!(got.data.as_deref(), Some(&b"newest"[..]));
8071        assert_eq!(got.lsn, newest.as_u64());
8072    }
8073
8074    #[test]
8075    fn test_insert_single() {
8076        let tree = Tree::new(1, 128);
8077        let key = b"testkey".to_vec();
8078        let data = b"testdata".to_vec();
8079        let lsn = Lsn::new(1, 100);
8080
8081        let result = tree.insert(key.clone(), data, lsn);
8082        assert!(result.is_ok());
8083        assert!(result.unwrap()); // Should be a new insert
8084
8085        assert!(!tree.is_empty());
8086
8087        // Verify we can search for it
8088        let search_result = tree.search(&key);
8089        assert!(search_result.is_some());
8090        let sr = search_result.unwrap();
8091        assert!(sr.exact_parent_found || !sr.child_not_resident);
8092    }
8093
8094    #[test]
8095    fn test_insert_multiple() {
8096        let tree = Tree::new(1, 128);
8097
8098        let keys = vec![
8099            b"apple".to_vec(),
8100            b"banana".to_vec(),
8101            b"cherry".to_vec(),
8102            b"date".to_vec(),
8103        ];
8104
8105        for (i, key) in keys.iter().enumerate() {
8106            let data = format!("data{}", i).into_bytes();
8107            let lsn = Lsn::new(1, 100 + (i as u32) * 10);
8108            let result = tree.insert(key.clone(), data, lsn);
8109            assert!(result.is_ok());
8110            assert!(result.unwrap()); // All should be new inserts
8111        }
8112
8113        // Verify we can search for each
8114        for key in &keys {
8115            let search_result = tree.search(key);
8116            assert!(search_result.is_some());
8117        }
8118    }
8119
8120    #[test]
8121    fn test_insert_duplicate_key() {
8122        let tree = Tree::new(1, 128);
8123        let key = b"duplicate".to_vec();
8124        let data1 = b"first".to_vec();
8125        let data2 = b"second".to_vec();
8126        let lsn1 = Lsn::new(1, 100);
8127        let lsn2 = Lsn::new(1, 200);
8128
8129        // First insert
8130        let result1 = tree.insert(key.clone(), data1, lsn1);
8131        assert!(result1.is_ok());
8132        assert!(result1.unwrap()); // New insert
8133
8134        // Second insert with same key - should be update
8135        let result2 = tree.insert(key, data2, lsn2);
8136        assert!(result2.is_ok());
8137        assert!(!result2.unwrap()); // Update, not new insert
8138    }
8139
8140    #[test]
8141    fn test_search_empty_tree() {
8142        let tree = Tree::new(1, 128);
8143        let key = b"noexist".to_vec();
8144
8145        let result = tree.search(&key);
8146        assert!(result.is_none());
8147    }
8148
8149    #[test]
8150    fn test_first_and_last_node() {
8151        let tree = Tree::new(1, 128);
8152
8153        // Empty tree
8154        assert!(tree.get_first_node().is_none());
8155        assert!(tree.get_last_node().is_none());
8156
8157        // Insert some keys
8158        let keys = [b"a".to_vec(), b"b".to_vec(), b"c".to_vec()];
8159        for (i, key) in keys.iter().enumerate() {
8160            let data = format!("data{}", i).into_bytes();
8161            let lsn = Lsn::new(1, 100 + (i as u32) * 10);
8162            tree.insert(key.clone(), data, lsn).unwrap();
8163        }
8164
8165        // Now should have first and last
8166        let first = tree.get_first_node();
8167        assert!(first.is_some());
8168        assert_eq!(first.unwrap().index, 0);
8169
8170        let last = tree.get_last_node();
8171        assert!(last.is_some());
8172        assert_eq!(last.unwrap().index, 2);
8173    }
8174
8175    #[test]
8176    fn test_node_id_generation() {
8177        let id1 = generate_node_id();
8178        let id2 = generate_node_id();
8179        let id3 = generate_node_id();
8180
8181        assert!(id2 > id1);
8182        assert!(id3 > id2);
8183    }
8184
8185    #[test]
8186    fn test_tree_node_is_bin() {
8187        let bin = TreeNode::Bottom(BinStub {
8188            node_id: 1,
8189            level: BIN_LEVEL,
8190            entries: vec![],
8191            key_prefix: Vec::new(),
8192            dirty: false,
8193            is_delta: false,
8194            last_full_lsn: NULL_LSN,
8195            last_delta_lsn: NULL_LSN,
8196            generation: 0,
8197            parent: None,
8198            expiration_in_hours: true,
8199            cursor_count: 0,
8200            prohibit_next_delta: false,
8201            lsn_rep: LsnRep::Empty,
8202            keys: KeyRep::new(),
8203            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
8204        });
8205        assert!(bin.is_bin());
8206        assert_eq!(bin.level(), BIN_LEVEL);
8207
8208        let internal = TreeNode::Internal(InNodeStub {
8209            node_id: 2,
8210            level: MAIN_LEVEL + 2,
8211            entries: vec![],
8212            targets: TargetRep::None,
8213            dirty: false,
8214            generation: 0,
8215            parent: None,
8216            lsn_rep: LsnRep::Empty,
8217        });
8218        assert!(!internal.is_bin());
8219        assert_eq!(internal.level(), MAIN_LEVEL + 2);
8220    }
8221
8222    #[test]
8223    fn test_find_entry() {
8224        let mut entries = vec![];
8225        let mut keys = vec![];
8226        for i in 0..5 {
8227            entries.push(BinEntry {
8228                data: Some(vec![]),
8229                known_deleted: false,
8230                dirty: false,
8231                expiration_time: 0,
8232            });
8233            keys.push(format!("key{}", i).into_bytes());
8234        }
8235
8236        let bin = TreeNode::Bottom(BinStub {
8237            node_id: 1,
8238            level: BIN_LEVEL,
8239            entries,
8240            key_prefix: Vec::new(),
8241            dirty: false,
8242            is_delta: false,
8243            last_full_lsn: NULL_LSN,
8244            last_delta_lsn: NULL_LSN,
8245            generation: 0,
8246            parent: None,
8247            expiration_in_hours: true,
8248            cursor_count: 0,
8249            prohibit_next_delta: false,
8250            lsn_rep: LsnRep::Empty,
8251            keys: KeyRep::from_keys(keys),
8252            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
8253        });
8254
8255        // Search for existing key
8256        let result = bin.find_entry(b"key2", false, true);
8257        assert_eq!(result & 0xFFFF, 2);
8258        assert_ne!(result & EXACT_MATCH, 0);
8259
8260        // Search for non-existing key with exact=false
8261        let result = bin.find_entry(b"key15", false, false);
8262        assert_eq!(result & 0xFFFF, 2); // Would go between key1 and key2
8263        assert_eq!(result & EXACT_MATCH, 0);
8264    }
8265
8266    #[test]
8267    fn test_insert_until_full() {
8268        // With splits implemented, inserting beyond max_entries_per_node must
8269        // succeed (the tree splits proactively rather than returning an error).
8270        let tree = Tree::new(1, 3); // Small max to exercise splits
8271
8272        // Insert up to max
8273        for i in 0..3 {
8274            let key = format!("key{}", i).into_bytes();
8275            let data = format!("data{}", i).into_bytes();
8276            let lsn = Lsn::new(1, 100 + i);
8277            let result = tree.insert(key, data, lsn);
8278            assert!(result.is_ok(), "insert {} should succeed", i);
8279        }
8280
8281        // The 4th insert triggers a split and must also succeed.
8282        let key = b"key3".to_vec();
8283        let data = b"data3".to_vec();
8284        let lsn = Lsn::new(1, 103);
8285        let result = tree.insert(key.clone(), data, lsn);
8286        assert!(
8287            result.is_ok(),
8288            "insert after full should trigger split and succeed"
8289        );
8290        assert!(result.unwrap(), "should be a new insert");
8291
8292        // The inserted key must be findable after the split.
8293        let sr = tree.search(&key);
8294        assert!(sr.is_some(), "key3 must be searchable after split");
8295        assert!(sr.unwrap().exact_parent_found, "key3 must be found exactly");
8296    }
8297
8298    #[test]
8299    fn test_memory_counter_balanced_on_insert_delete_f8() {
8300        use std::sync::Arc;
8301        use std::sync::atomic::{AtomicI64, Ordering};
8302        // F8 regression: insert accounts key+data+48; delete must subtract the
8303        // SAME, so an insert+delete of the same record returns the counter to
8304        // its starting value (previously delete omitted data_len -> the counter
8305        // leaked data_len per delete, biasing the evictor over-budget view).
8306        let mut tree = Tree::new(1, 16);
8307        let counter = Arc::new(AtomicI64::new(0));
8308        tree.set_memory_counter(Arc::clone(&counter));
8309
8310        let key = b"a-key".to_vec();
8311        let data = vec![0u8; 200]; // non-trivial data length
8312        tree.insert(key.clone(), data.clone(), Lsn::new(0, 10)).unwrap();
8313        let after_insert = counter.load(Ordering::Relaxed);
8314        assert!(after_insert > 0, "insert must increase the counter");
8315        assert_eq!(
8316            after_insert,
8317            (key.len() + data.len() + BIN_ENTRY_OVERHEAD) as i64,
8318            "insert accounts key + data + per-slot BinEntry overhead"
8319        );
8320
8321        let deleted = tree.delete(&key);
8322        assert!(deleted);
8323        assert_eq!(
8324            counter.load(Ordering::Relaxed),
8325            0,
8326            "F8: delete must subtract key + data + BIN_ENTRY_OVERHEAD, returning the counter              to its pre-insert value (no data_len leak)"
8327        );
8328    }
8329
8330    /// EV-13 (pass-post): a full-node detach must ACTUALLY drop the child
8331    /// `Arc` from the parent IN, not merely credit bytes.  Before the fix the
8332    /// evictor credited `node_size_fn(node_id)` and removed the node from the
8333    /// LRU list, but the parent's `InEntry.child` still held a strong `Arc`,
8334    /// so the node was never freed (phantom free) and the budget over-credited.
8335    ///
8336    /// This test proves: after `detach_node_by_id` the held child `Arc` is the
8337    /// LAST strong reference (strong_count == 1), the parent slot's `child` is
8338    /// `None`, and the returned bytes equal the node's measured heap size.
8339    ///
8340    /// JE ref: `IN.detachNode` (`setTarget(idx, null)`) / `Evictor.evict`.
8341    #[test]
8342    fn test_ev13_detach_actually_frees_child() {
8343        // Tiny fanout forces a root split so we get a real IN parent with BIN
8344        // children that the evictor would target.
8345        let tree = Tree::new(7, 4);
8346        for i in 0u8..12 {
8347            tree.insert(
8348                vec![b'a' + i],
8349                vec![i; 8],
8350                Lsn::new(1, u32::from(i) + 1),
8351            )
8352            .unwrap();
8353        }
8354
8355        // Find a BIN child of the root IN (the eviction target) + its parent.
8356        let root = tree.get_root().expect("tree must have a root");
8357        let (parent_arc, child_idx, bin_id, expected_bytes) = {
8358            let rg = root.read();
8359            let TreeNode::Internal(n) = &*rg else {
8360                panic!("root must be an IN after split");
8361            };
8362            // Pick the first slot whose child is a resident BIN.
8363            let (idx, child) = n
8364                .first_resident_child()
8365                .expect("root must have a resident child");
8366            let (id, bytes) = {
8367                let cg = child.read();
8368                (
8369                    match &*cg {
8370                        TreeNode::Bottom(b) => b.node_id,
8371                        TreeNode::Internal(n2) => n2.node_id,
8372                    },
8373                    cg.budgeted_memory_size(),
8374                )
8375            };
8376            (Arc::clone(&root), idx, id, bytes)
8377        };
8378
8379        // Hold an external strong reference to the child so we can observe its
8380        // strong_count drop when detach releases the parent's reference.
8381        let child_arc = {
8382            let pg = parent_arc.read();
8383            let TreeNode::Internal(n) = &*pg else { unreachable!() };
8384            Arc::clone(n.child_ref(child_idx).unwrap())
8385        };
8386        // Two strong refs now: the parent slot + our test handle.
8387        assert_eq!(
8388            Arc::strong_count(&child_arc),
8389            2,
8390            "precondition: parent slot + test handle hold the child"
8391        );
8392
8393        let freed = tree.detach_node_by_id(bin_id);
8394
8395        // 1. Bytes credited equal the measured heap size (no phantom credit).
8396        assert_eq!(
8397            freed, expected_bytes,
8398            "detach must credit the node's real measured heap size"
8399        );
8400        // 2. The parent slot's child is now None (JE setTarget(idx, null)).
8401        {
8402            let pg = parent_arc.read();
8403            let TreeNode::Internal(n) = &*pg else { unreachable!() };
8404            assert!(
8405                n.child_is_none(child_idx),
8406                "EV-13: parent slot must be detached (child == None)"
8407            );
8408            // The slot itself (key + LSN) is retained for re-fetch.
8409            assert!(
8410                !n.get_lsn(child_idx).is_null(),
8411                "detach keeps the slot LSN so the node can be re-fetched"
8412            );
8413        }
8414        // 3. Our handle is now the ONLY strong reference -> the parent really
8415        //    dropped its Arc; the node is freed when we drop `child_arc`.
8416        //    Before EV-13 this would be 2 (parent still held it) = phantom free.
8417        assert_eq!(
8418            Arc::strong_count(&child_arc),
8419            1,
8420            "EV-13: detach must drop the parent's strong Arc (no phantom free)"
8421        );
8422    }
8423
8424    /// EV-13: detach must NOT decrement the memory counter itself (the evictor
8425    /// owns that bookkeeping via `Arbiter::release_memory`).  A double credit
8426    /// would drive `cache_usage` below reality.
8427    #[test]
8428    fn test_ev13_detach_does_not_touch_counter() {
8429        use std::sync::atomic::{AtomicI64, Ordering};
8430        let mut tree = Tree::new(8, 4);
8431        let counter = Arc::new(AtomicI64::new(0));
8432        tree.set_memory_counter(Arc::clone(&counter));
8433        for i in 0u8..12 {
8434            tree.insert(
8435                vec![b'a' + i],
8436                vec![i; 8],
8437                Lsn::new(1, u32::from(i) + 1),
8438            )
8439            .unwrap();
8440        }
8441        let before = counter.load(Ordering::Relaxed);
8442
8443        // Grab a BIN child id.
8444        let root = tree.get_root().unwrap();
8445        let bin_id = {
8446            let rg = root.read();
8447            let TreeNode::Internal(n) = &*rg else { unreachable!() };
8448            let child = n
8449                .resident_children()
8450                .into_iter()
8451                .next()
8452                .expect("resident child");
8453            match &*child.read() {
8454                TreeNode::Bottom(b) => b.node_id,
8455                TreeNode::Internal(n2) => n2.node_id,
8456            }
8457        };
8458
8459        let freed = tree.detach_node_by_id(bin_id);
8460        assert!(freed > 0, "detach must free a resident child");
8461        assert_eq!(
8462            counter.load(Ordering::Relaxed),
8463            before,
8464            "EV-13: detach must not change the counter (evictor credits once)"
8465        );
8466    }
8467
8468    /// EV-13: detaching the root or an unknown id is a no-op returning 0.
8469    #[test]
8470    fn test_ev13_detach_root_or_missing_is_noop() {
8471        let tree = Tree::new(9, 4);
8472        for i in 0u8..12 {
8473            tree.insert(
8474                vec![b'a' + i],
8475                vec![i; 8],
8476                Lsn::new(1, u32::from(i) + 1),
8477            )
8478            .unwrap();
8479        }
8480        let root_id = {
8481            let rg = tree.get_root().unwrap();
8482            let g = rg.read();
8483            match &*g {
8484                TreeNode::Internal(n) => n.node_id,
8485                TreeNode::Bottom(b) => b.node_id,
8486            }
8487        };
8488        assert_eq!(
8489            tree.detach_node_by_id(root_id),
8490            0,
8491            "root has no parent IN -> detach is a no-op"
8492        );
8493        assert_eq!(
8494            tree.detach_node_by_id(u64::MAX),
8495            0,
8496            "unknown node id -> detach is a no-op"
8497        );
8498    }
8499
8500    /// DBI-23 (pass-post): the live `memory_counter` must APPROXIMATE the real
8501    /// in-memory heap of the tree, not the old `key + data + 48` lower bound.
8502    ///
8503    /// JE keeps `inMemorySize` (`IN.getBudgetedMemorySize`) in lock-step with
8504    /// the per-node `computeMemorySize`; the over-budget arbiter sees the real
8505    /// figure so eviction fires at the right time.  The previous Noxu live
8506    /// path undercounted each BIN slot (48 vs the 64-byte `BinEntry` struct)
8507    /// and never accounted the node-struct fixed overhead, so the counter ran
8508    /// below real heap and the evictor under-fired.
8509    ///
8510    /// We assert the live counter is within tolerance of
8511    /// `total_budgeted_memory` (the authoritative walk-and-sum oracle).  The
8512    /// only gap is the per-node fixed struct overhead (BinStub/InNodeStub),
8513    /// which is a small fraction for non-trivial entries — the fix closes the
8514    /// dominant per-slot gap.
8515    #[test]
8516    fn test_dbi23_live_counter_approximates_real_heap() {
8517        use std::sync::atomic::{AtomicI64, Ordering};
8518        let mut tree = Tree::new(42, 32);
8519        let counter = Arc::new(AtomicI64::new(0));
8520        tree.set_memory_counter(Arc::clone(&counter));
8521
8522        // Insert N entries with realistic key+data sizes.
8523        let n = 400u32;
8524        for i in 0..n {
8525            let key = format!("key-{i:08}").into_bytes(); // 12 bytes
8526            let data = vec![0u8; 64]; // 64 bytes
8527            tree.insert(key, data, Lsn::new(1, i + 1)).unwrap();
8528        }
8529
8530        let live = counter.load(Ordering::Relaxed) as u64;
8531        let real = tree.total_budgeted_memory();
8532
8533        // The live counter must reflect the per-slot cost AFTER the T-2/T-3
8534        // compactions hoisted the per-slot key/LSN out of `BinEntry` into the
8535        // node-level reps.  The per-slot live charge is now
8536        // `key + data + size_of::<BinEntry>() + 4` (the packed LSN slot); the
8537        // dominant data+key bytes are still charged in full.  Assert the live
8538        // counter is at least the data-and-fixed portion (a stable floor that
8539        // does NOT assume the pre-compaction 64-byte slot).
8540        let new_lower_bound: u64 = (0..n)
8541            .map(|i| {
8542                let key_len = format!("key-{i:08}").len();
8543                (key_len + 64 + BIN_ENTRY_OVERHEAD) as u64
8544            })
8545            .sum();
8546
8547        assert!(
8548            live >= new_lower_bound,
8549            "DBI-23: live counter ({live}) must be >= the per-slot-correct \
8550             lower bound ({new_lower_bound})"
8551        );
8552
8553        // Within tolerance of real heap (the residual gap is the per-node
8554        // fixed struct overhead, intentionally not tracked incrementally).
8555        let lower = real * 80 / 100;
8556        assert!(
8557            live >= lower && live <= real,
8558            "DBI-23: live counter ({live}) must approximate real heap ({real}) \
8559             within tolerance [{lower}, {real}]"
8560        );
8561    }
8562
8563    #[test]
8564    fn test_delete_existing_key() {
8565        let tree = Tree::new(1, 128);
8566        let key = b"remove_me".to_vec();
8567        tree.insert(key.clone(), b"val".to_vec(), Lsn::new(1, 10)).unwrap();
8568        assert!(tree.delete(&key));
8569
8570        // After deletion the BIN is empty, so delete returns true the first
8571        // time and false the second time.
8572        assert!(!tree.delete(&key));
8573    }
8574
8575    #[test]
8576    fn test_delete_nonexistent_key() {
8577        let tree = Tree::new(1, 128);
8578        tree.insert(b"a".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
8579
8580        assert!(!tree.delete(b"zzz"));
8581    }
8582
8583    #[test]
8584    fn test_delete_empty_tree() {
8585        let tree = Tree::new(1, 128);
8586        assert!(!tree.delete(b"nothing"));
8587    }
8588
8589    #[test]
8590    fn test_delete_all_entries_makes_bin_empty() {
8591        let tree = Tree::new(1, 128);
8592        tree.insert(b"x".to_vec(), b"1".to_vec(), Lsn::new(1, 1)).unwrap();
8593        tree.insert(b"y".to_vec(), b"2".to_vec(), Lsn::new(1, 2)).unwrap();
8594
8595        assert!(tree.delete(b"x"));
8596        assert!(tree.delete(b"y"));
8597
8598        // Tree still has a root (empty BIN), so is_empty() returns false.
8599        assert!(!tree.is_empty());
8600        // get_first_node should return None for an empty BIN.
8601        assert!(tree.get_first_node().is_none());
8602    }
8603
8604    #[test]
8605    fn test_set_root_and_get_root() {
8606        let tree = Tree::new(1, 128);
8607        assert!(tree.get_root().is_none());
8608
8609        let bin = TreeNode::Bottom(BinStub {
8610            node_id: generate_node_id(),
8611            level: BIN_LEVEL,
8612            entries: vec![],
8613            key_prefix: Vec::new(),
8614            dirty: false,
8615            is_delta: false,
8616            last_full_lsn: NULL_LSN,
8617            last_delta_lsn: NULL_LSN,
8618            generation: 0,
8619            parent: None,
8620            expiration_in_hours: true,
8621            cursor_count: 0,
8622            prohibit_next_delta: false,
8623            lsn_rep: LsnRep::Empty,
8624            keys: KeyRep::new(),
8625            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
8626        });
8627        tree.set_root(bin);
8628        assert!(tree.get_root().is_some());
8629    }
8630
8631    // ========================================================================
8632    // Split / multi-level insert tests  (new)
8633    // ========================================================================
8634
8635    /// inserting enough keys to fill the root IN causes
8636    /// the root IN itself to split, resulting in a tree with 3 or more levels.
8637    ///
8638    /// With max_entries_per_node = 4:
8639    ///   - Each BIN holds 4 entries before it is split.
8640    ///   - The root IN at level 2 holds up to 4 BIN children.
8641    ///   - Filling those 4 BINs (16 entries) and adding a 17th forces the
8642    ///     root IN to split, creating a level-3 root.
8643    #[test]
8644    fn test_insert_forces_root_split() {
8645        let tree = Tree::new(1, 4);
8646
8647        // 17 inserts with fanout 4 forces the root IN to split.
8648        for i in 0u32..20 {
8649            let key = format!("key{:04}", i).into_bytes();
8650            let data = format!("data{}", i).into_bytes();
8651            let lsn = Lsn::new(1, 100 + i);
8652            let r = tree.insert(key, data, lsn);
8653            assert!(r.is_ok(), "insert {} must succeed", i);
8654        }
8655
8656        // At least one root split must have occurred.
8657        assert!(
8658            tree.get_root_splits() > 0,
8659            "expected at least one root split after 20 inserts with fanout 4"
8660        );
8661
8662        // The root level must be > level-2 (i.e., the tree has grown to 3+ levels).
8663        let root_arc = tree.get_root().as_ref().unwrap().clone();
8664        let root_level = root_arc.read().level();
8665        let level_2 = MAIN_LEVEL | 2;
8666        assert!(
8667            root_level > level_2,
8668            "root level {} must be > level-2 after root split",
8669            root_level
8670        );
8671    }
8672
8673    /// Inserting 1000 keys in sorted order and verifying all are searchable.
8674    #[test]
8675    fn test_insert_many_keys() {
8676        let tree = Tree::new(1, 8);
8677        let n = 1000u32;
8678
8679        for i in 0..n {
8680            let key = format!("key{:08}", i).into_bytes();
8681            let data = format!("data{}", i).into_bytes();
8682            let lsn = Lsn::new(1, i);
8683            let r = tree.insert(key, data, lsn);
8684            assert!(r.is_ok(), "insert {} must succeed", i);
8685        }
8686
8687        // All keys must be findable.
8688        for i in 0..n {
8689            let key = format!("key{:08}", i).into_bytes();
8690            let sr = tree.search(&key);
8691            assert!(
8692                sr.is_some() && sr.unwrap().exact_parent_found,
8693                "key{:08} must be found after bulk insert",
8694                i
8695            );
8696        }
8697    }
8698
8699    /// Inserting 500 keys in pseudo-random (reverse) order and verifying all
8700    /// are searchable.
8701    #[test]
8702    fn test_insert_random_keys() {
8703        let tree = Tree::new(1, 8);
8704        let n = 500u32;
8705
8706        // Insert in reverse order as a simple non-sorted sequence.
8707        for i in (0..n).rev() {
8708            let key = format!("rkey{:08}", i).into_bytes();
8709            let data = format!("data{}", i).into_bytes();
8710            let lsn = Lsn::new(1, i);
8711            let r = tree.insert(key, data, lsn);
8712            assert!(r.is_ok(), "insert {} must succeed", i);
8713        }
8714
8715        for i in 0..n {
8716            let key = format!("rkey{:08}", i).into_bytes();
8717            let sr = tree.search(&key);
8718            assert!(
8719                sr.is_some() && sr.unwrap().exact_parent_found,
8720                "rkey{:08} must be found",
8721                i
8722            );
8723        }
8724    }
8725
8726    /// After any number of splits, every key inserted must still be findable.
8727    ///
8728    #[test]
8729    fn test_split_preserves_all_keys() {
8730        // Tiny fanout to maximise split frequency.
8731        let tree = Tree::new(1, 3);
8732        let n = 60u32;
8733
8734        let mut keys: Vec<Vec<u8>> = Vec::new();
8735        for i in 0..n {
8736            let key = format!("sk{:04}", i).into_bytes();
8737            keys.push(key.clone());
8738            let data = format!("d{}", i).into_bytes();
8739            let lsn = Lsn::new(1, i);
8740            let r = tree.insert(key, data, lsn);
8741            assert!(r.is_ok(), "insert {} must not fail", i);
8742        }
8743
8744        // After all inserts (and all the splits they induced), every key must
8745        // still be findable in the tree.
8746        for key in &keys {
8747            let sr = tree.search(key);
8748            assert!(
8749                sr.is_some() && sr.unwrap().exact_parent_found,
8750                "key {:?} must survive all splits",
8751                std::str::from_utf8(key).unwrap_or("?")
8752            );
8753        }
8754    }
8755
8756    /// The tree level (depth) must grow as keys are inserted and splits occur.
8757    #[test]
8758    fn test_tree_height_grows() {
8759        let tree = Tree::new(1, 4);
8760
8761        // With fanout 4, one level-2 root IN can hold 4 children.  After enough
8762        // inserts the root itself will split and a level-3 node will appear.
8763        // Insert enough keys to force the root to split at least once.
8764        let n = 40u32;
8765        for i in 0..n {
8766            let key = format!("hk{:08}", i).into_bytes();
8767            let data = format!("d{}", i).into_bytes();
8768            let lsn = Lsn::new(1, i);
8769            tree.insert(key, data, lsn).unwrap();
8770        }
8771
8772        // At least one root split must have occurred.
8773        assert!(
8774            tree.get_root_splits() > 0,
8775            "expected root to have split at least once for {} keys with fanout 4",
8776            n
8777        );
8778
8779        // The root level must be > level-2 (i.e., the tree has grown past two levels).
8780        let root_arc = tree.get_root().as_ref().unwrap().clone();
8781        let root_level = root_arc.read().level();
8782        let level_2 = MAIN_LEVEL | 2;
8783        assert!(
8784            root_level > level_2,
8785            "root level {} must be > {} after enough inserts",
8786            root_level,
8787            level_2
8788        );
8789    }
8790
8791    #[test]
8792    fn test_find_entry_on_internal_node() {
8793        let mut entries = vec![];
8794        for i in 0..4 {
8795            entries.push(InEntry { key: format!("k{}", i).into_bytes() });
8796        }
8797        let internal = TreeNode::Internal(InNodeStub {
8798            node_id: 1,
8799            level: MAIN_LEVEL + 2,
8800            entries,
8801            targets: TargetRep::None,
8802            dirty: false,
8803            generation: 0,
8804            parent: None,
8805            lsn_rep: LsnRep::Empty,
8806        });
8807
8808        // Exact match
8809        let r = internal.find_entry(b"k2", false, true);
8810        assert_ne!(r & EXACT_MATCH, 0);
8811        assert_eq!(r & 0xFFFF, 2);
8812
8813        // No exact match with exact=true
8814        let r = internal.find_entry(b"kx", false, true);
8815        assert_eq!(r, -1);
8816    }
8817
8818    // St-H5: non-exact `find_entry` on an Internal node must return the FLOOR
8819    // child slot (largest entry ≤ key), not the insertion point. Entries are
8820    // k0,k1,k2,k3; slot 0 is the leftmost child.
8821    #[test]
8822    fn test_find_entry_internal_nonexact_returns_floor() {
8823        let mut entries = vec![];
8824        for i in 0..4 {
8825            entries.push(InEntry { key: format!("k{}", i).into_bytes() });
8826        }
8827        let internal = TreeNode::Internal(InNodeStub {
8828            node_id: 1,
8829            level: MAIN_LEVEL + 2,
8830            entries,
8831            targets: TargetRep::None,
8832            dirty: false,
8833            generation: 0,
8834            parent: None,
8835            lsn_rep: LsnRep::Empty,
8836        });
8837
8838        // Key below every separator floors to slot 0 (leftmost child).
8839        assert_eq!(internal.find_entry(b"a", false, false) & 0xFFFF, 0);
8840        // Between k1 and k2 floors to k1 (slot 1).
8841        assert_eq!(internal.find_entry(b"k1x", false, false) & 0xFFFF, 1);
8842        // Above every separator floors to the last slot (k3 = slot 3).
8843        assert_eq!(internal.find_entry(b"zzz", false, false) & 0xFFFF, 3);
8844        // Exact match still reported as the exact slot.
8845        let r = internal.find_entry(b"k2", false, false);
8846        assert_ne!(r & EXACT_MATCH, 0);
8847        assert_eq!(r & 0xFFFF, 2);
8848    }
8849
8850    // ========================================================================
8851    // New tests: dirty tracking, generation, parent pointers, log size, stats
8852    // ========================================================================
8853
8854    /// After inserting into a tree, the BIN (and root IN) must be dirty.
8855    ///
8856    /// The: Tree.insertLN() calls bin.setDirty(true) after each insert.
8857    #[test]
8858    fn test_insert_marks_bin_dirty() {
8859        let tree = Tree::new(1, 128);
8860        tree.insert(b"key1".to_vec(), b"val1".to_vec(), Lsn::new(1, 1))
8861            .unwrap();
8862
8863        let root_arc = tree.get_root().as_ref().unwrap().clone();
8864        // root is an upper IN — its slot 0 child is the BIN.
8865        let bin_arc = {
8866            let g = root_arc.read();
8867            match &*g {
8868                TreeNode::Internal(n) => n.get_child(0).unwrap(),
8869                _ => panic!("expected Internal root"),
8870            }
8871        };
8872
8873        let bin_dirty = bin_arc.read().is_dirty();
8874        assert!(bin_dirty, "BIN must be dirty after insert");
8875    }
8876
8877    /// Updating an existing key keeps the BIN dirty.
8878    #[test]
8879    fn test_update_keeps_bin_dirty() {
8880        let tree = Tree::new(1, 128);
8881        tree.insert(b"k".to_vec(), b"v1".to_vec(), Lsn::new(1, 1)).unwrap();
8882        // second insert is an update
8883        tree.insert(b"k".to_vec(), b"v2".to_vec(), Lsn::new(1, 2)).unwrap();
8884
8885        let root_arc = tree.get_root().as_ref().unwrap().clone();
8886        let bin_arc = {
8887            let g = root_arc.read();
8888            match &*g {
8889                TreeNode::Internal(n) => n.get_child(0).unwrap(),
8890                _ => panic!("expected Internal root"),
8891            }
8892        };
8893
8894        assert!(bin_arc.read().is_dirty(), "BIN must be dirty after update");
8895    }
8896
8897    /// After deleting a key the BIN must be dirty.
8898    #[test]
8899    fn test_delete_marks_bin_dirty() {
8900        let tree = Tree::new(1, 128);
8901        tree.insert(b"del".to_vec(), b"val".to_vec(), Lsn::new(1, 1)).unwrap();
8902
8903        // Manually clear dirty flag to verify delete re-sets it.
8904        {
8905            let root_arc = tree.get_root().as_ref().unwrap().clone();
8906            let bin_arc = {
8907                let g = root_arc.read();
8908                match &*g {
8909                    TreeNode::Internal(n) => n.get_child(0).unwrap(),
8910                    _ => panic!("expected Internal root"),
8911                }
8912            };
8913            bin_arc.write().set_dirty(false);
8914            assert!(!bin_arc.read().is_dirty());
8915        }
8916
8917        tree.delete(b"del");
8918
8919        let root_arc = tree.get_root().as_ref().unwrap().clone();
8920        let bin_arc = {
8921            let g = root_arc.read();
8922            match &*g {
8923                TreeNode::Internal(n) => n.get_child(0).unwrap(),
8924                _ => panic!("expected Internal root"),
8925            }
8926        };
8927        assert!(bin_arc.read().is_dirty(), "BIN must be dirty after delete");
8928    }
8929
8930    /// BIN's parent pointer must point to the root IN.
8931    #[test]
8932    fn test_bin_parent_pointer_set_on_initial_insert() {
8933        let tree = Tree::new(1, 128);
8934        tree.insert(b"k".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
8935
8936        let root_arc = tree.get_root().as_ref().unwrap().clone();
8937        let bin_arc = {
8938            let g = root_arc.read();
8939            match &*g {
8940                TreeNode::Internal(n) => n.get_child(0).unwrap(),
8941                _ => panic!("expected Internal root"),
8942            }
8943        };
8944
8945        let parent_weak = bin_arc.read().get_parent();
8946        assert!(parent_weak.is_some(), "BIN must have a parent pointer");
8947
8948        // Upgrading the weak pointer must give us the root arc.
8949        let parent_arc = parent_weak.unwrap().upgrade().unwrap();
8950        assert!(
8951            Arc::ptr_eq(&parent_arc, &root_arc),
8952            "BIN parent must be the root IN"
8953        );
8954    }
8955
8956    /// set_dirty / is_dirty round-trip on both variants.
8957    #[test]
8958    fn test_dirty_flag_roundtrip() {
8959        let mut bin_node = TreeNode::Bottom(BinStub {
8960            node_id: 1,
8961            level: BIN_LEVEL,
8962            entries: vec![],
8963            key_prefix: Vec::new(),
8964            dirty: false,
8965            is_delta: false,
8966            last_full_lsn: NULL_LSN,
8967            last_delta_lsn: NULL_LSN,
8968            generation: 0,
8969            parent: None,
8970            expiration_in_hours: true,
8971            cursor_count: 0,
8972            prohibit_next_delta: false,
8973            lsn_rep: LsnRep::Empty,
8974            keys: KeyRep::new(),
8975            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
8976        });
8977        assert!(!bin_node.is_dirty());
8978        bin_node.set_dirty(true);
8979        assert!(bin_node.is_dirty());
8980        bin_node.set_dirty(false);
8981        assert!(!bin_node.is_dirty());
8982
8983        let mut in_node = TreeNode::Internal(InNodeStub {
8984            node_id: 2,
8985            level: MAIN_LEVEL | 2,
8986            entries: vec![],
8987            targets: TargetRep::None,
8988            dirty: false,
8989            generation: 0,
8990            parent: None,
8991            lsn_rep: LsnRep::Empty,
8992        });
8993        assert!(!in_node.is_dirty());
8994        in_node.set_dirty(true);
8995        assert!(in_node.is_dirty());
8996    }
8997
8998    /// set_generation / get_generation round-trip on both variants.
8999    #[test]
9000    fn test_generation_roundtrip() {
9001        let mut bin_node = TreeNode::Bottom(BinStub {
9002            node_id: 1,
9003            level: BIN_LEVEL,
9004            entries: vec![],
9005            key_prefix: Vec::new(),
9006            dirty: false,
9007            is_delta: false,
9008            last_full_lsn: NULL_LSN,
9009            last_delta_lsn: NULL_LSN,
9010            generation: 0,
9011            parent: None,
9012            expiration_in_hours: true,
9013            cursor_count: 0,
9014            prohibit_next_delta: false,
9015            lsn_rep: LsnRep::Empty,
9016            keys: KeyRep::new(),
9017            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9018        });
9019        assert_eq!(bin_node.get_generation(), 0);
9020        bin_node.set_generation(42);
9021        assert_eq!(bin_node.get_generation(), 42);
9022
9023        let mut in_node = TreeNode::Internal(InNodeStub {
9024            node_id: 2,
9025            level: MAIN_LEVEL | 2,
9026            entries: vec![],
9027            targets: TargetRep::None,
9028            dirty: false,
9029            generation: 0,
9030            parent: None,
9031            lsn_rep: LsnRep::Empty,
9032        });
9033        in_node.set_generation(99);
9034        assert_eq!(in_node.get_generation(), 99);
9035    }
9036
9037    /// log_size() must be consistent with write_to_bytes() length.
9038    #[test]
9039    fn test_log_size_matches_bytes_len() {
9040        // BIN stub with some entries.
9041        let bin_node = TreeNode::Bottom(BinStub {
9042            node_id: 7,
9043            level: BIN_LEVEL,
9044            entries: vec![
9045                BinEntry {
9046                    data: Some(b"d1".to_vec()),
9047                    known_deleted: false,
9048                    dirty: false,
9049                    expiration_time: 0,
9050                },
9051                BinEntry {
9052                    data: None,
9053                    known_deleted: false,
9054                    dirty: false,
9055                    expiration_time: 0,
9056                },
9057            ],
9058            key_prefix: Vec::new(),
9059            dirty: true,
9060            is_delta: false,
9061            last_full_lsn: NULL_LSN,
9062            last_delta_lsn: NULL_LSN,
9063            generation: 5,
9064            parent: None,
9065            expiration_in_hours: true,
9066            cursor_count: 0,
9067            prohibit_next_delta: false,
9068            lsn_rep: LsnRep::Empty,
9069            keys: KeyRep::from_keys(vec![b"alpha".to_vec(), b"beta".to_vec()]),
9070            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9071        });
9072        assert_eq!(bin_node.log_size(), bin_node.write_to_bytes().len());
9073
9074        // IN stub with some entries.
9075        let in_node = TreeNode::Internal(InNodeStub {
9076            node_id: 8,
9077            level: MAIN_LEVEL | 2,
9078            entries: vec![
9079                InEntry { key: vec![] },
9080                InEntry { key: b"mid".to_vec() },
9081            ],
9082            targets: TargetRep::None,
9083            dirty: false,
9084            generation: 0,
9085            parent: None,
9086            lsn_rep: LsnRep::Empty,
9087        });
9088        assert_eq!(in_node.log_size(), in_node.write_to_bytes().len());
9089    }
9090
9091    /// write_to_bytes() output contains the node_id and dirty flag.
9092    #[test]
9093    fn test_write_to_bytes_encodes_node_id_and_dirty() {
9094        let node = TreeNode::Bottom(BinStub {
9095            node_id: 0xDEAD_BEEF_0000_0001,
9096            level: BIN_LEVEL,
9097            entries: vec![],
9098            key_prefix: Vec::new(),
9099            dirty: true,
9100            is_delta: false,
9101            last_full_lsn: NULL_LSN,
9102            last_delta_lsn: NULL_LSN,
9103            generation: 0,
9104            parent: None,
9105            expiration_in_hours: true,
9106            cursor_count: 0,
9107            prohibit_next_delta: false,
9108            lsn_rep: LsnRep::Empty,
9109            keys: KeyRep::new(),
9110            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9111        });
9112        let bytes = node.write_to_bytes();
9113        // First 8 bytes = node_id big-endian.
9114        let id_bytes = &bytes[0..8];
9115        assert_eq!(id_bytes, 0xDEAD_BEEF_0000_0001u64.to_be_bytes());
9116        // Byte at offset 16 (after node_id[8] + level[4] + n_entries[4]) = dirty flag.
9117        assert_eq!(bytes[16], 1u8, "dirty flag must be 1");
9118    }
9119
9120    /// log_size() grows as entries are added.
9121    #[test]
9122    fn test_log_size_grows_with_entries() {
9123        let empty = TreeNode::Bottom(BinStub {
9124            node_id: 1,
9125            level: BIN_LEVEL,
9126            entries: vec![],
9127            key_prefix: Vec::new(),
9128            dirty: false,
9129            is_delta: false,
9130            last_full_lsn: NULL_LSN,
9131            last_delta_lsn: NULL_LSN,
9132            generation: 0,
9133            parent: None,
9134            expiration_in_hours: true,
9135            cursor_count: 0,
9136            prohibit_next_delta: false,
9137            lsn_rep: LsnRep::Empty,
9138            keys: KeyRep::new(),
9139            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9140        });
9141        let with_entry = TreeNode::Bottom(BinStub {
9142            node_id: 2,
9143            level: BIN_LEVEL,
9144            entries: vec![BinEntry {
9145                data: None,
9146                known_deleted: false,
9147                dirty: false,
9148                expiration_time: 0,
9149            }],
9150            key_prefix: Vec::new(),
9151            dirty: false,
9152            is_delta: false,
9153            last_full_lsn: NULL_LSN,
9154            last_delta_lsn: NULL_LSN,
9155            generation: 0,
9156            parent: None,
9157            expiration_in_hours: true,
9158            cursor_count: 0,
9159            prohibit_next_delta: false,
9160            lsn_rep: LsnRep::Empty,
9161            keys: KeyRep::from_keys(vec![b"longkey_here".to_vec()]),
9162            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9163        });
9164        assert!(
9165            with_entry.log_size() > empty.log_size(),
9166            "log_size must grow when entries are added"
9167        );
9168    }
9169
9170    /// propagate_dirty_to_root() marks all ancestors dirty.
9171    #[test]
9172    fn test_propagate_dirty_to_root() {
9173        // Build a 2-level tree manually: root IN -> BIN.
9174        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
9175            node_id: generate_node_id(),
9176            level: BIN_LEVEL,
9177            entries: vec![],
9178            key_prefix: Vec::new(),
9179            dirty: false,
9180            is_delta: false,
9181            last_full_lsn: NULL_LSN,
9182            last_delta_lsn: NULL_LSN,
9183            generation: 0,
9184            parent: None, // set below
9185            expiration_in_hours: true,
9186            cursor_count: 0,
9187            prohibit_next_delta: false,
9188            lsn_rep: LsnRep::Empty,
9189            keys: KeyRep::new(),
9190            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9191        })));
9192
9193        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
9194            node_id: generate_node_id(),
9195            level: MAIN_LEVEL | 2,
9196            entries: vec![InEntry { key: vec![] }],
9197            targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
9198            dirty: false,
9199            generation: 0,
9200            parent: None,
9201            lsn_rep: LsnRep::Empty,
9202        })));
9203
9204        // Wire BIN's parent to root.
9205        bin_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
9206
9207        // Root is not dirty before propagation.
9208        assert!(!root_arc.read().is_dirty());
9209
9210        // Propagate from the BIN up.
9211        Tree::propagate_dirty_to_root(&bin_arc);
9212
9213        // Root must now be dirty.
9214        assert!(
9215            root_arc.read().is_dirty(),
9216            "root must be dirty after propagate_dirty_to_root"
9217        );
9218    }
9219
9220    /// collect_stats() on an empty tree returns all-zero stats.
9221    #[test]
9222    fn test_collect_stats_empty_tree() {
9223        let tree = Tree::new(1, 128);
9224        let stats = tree.collect_stats();
9225        assert_eq!(stats, TreeStats::default());
9226    }
9227
9228    /// collect_stats() on a single-entry tree: 1 IN + 1 BIN, height 2.
9229    #[test]
9230    fn test_collect_stats_single_insert() {
9231        let tree = Tree::new(1, 128);
9232        tree.insert(b"k".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
9233        let stats = tree.collect_stats();
9234        assert_eq!(stats.n_bins, 1, "must have 1 BIN");
9235        assert_eq!(stats.n_ins, 1, "must have 1 upper IN");
9236        assert_eq!(stats.height, 2, "single-entry tree has height 2");
9237        assert!(stats.n_entries >= 1, "must have at least 1 entry total");
9238    }
9239
9240    /// collect_stats() with many inserts: entry count matches insert count.
9241    #[test]
9242    fn test_collect_stats_many_inserts() {
9243        let tree = Tree::new(1, 8);
9244        let n = 50u32;
9245        for i in 0..n {
9246            let key = format!("sk{:04}", i).into_bytes();
9247            tree.insert(key, b"v".to_vec(), Lsn::new(1, i)).unwrap();
9248        }
9249        let stats = tree.collect_stats();
9250        // All n entries should be accounted for across all BINs.
9251        // n_entries counts entries in both INs and BINs; BIN entries = n.
9252        // We verify BIN entry total equals n by summing manually.
9253        let bin_entries: u64 = stats.n_entries - stats.n_ins; // rough check
9254        // A more precise assertion: the sum of all BIN entries == n.
9255        // Since we can't easily separate, just assert the tree is non-trivial.
9256        assert!(stats.n_bins > 0, "must have at least one BIN");
9257        assert!(stats.height >= 2, "multi-entry tree has height >= 2");
9258        // Total entries in the tree must be >= n (BIN entries alone).
9259        assert!(
9260            bin_entries >= n as u64 || stats.n_entries >= n as u64,
9261            "entry count must account for all inserts"
9262        );
9263    }
9264
9265    // ========================================================================
9266    // Tests: B-tree merge / compress
9267    // ========================================================================
9268
9269    /// After deleting most keys from a tree, compress() must reduce the BIN
9270    /// count by merging under-full siblings.
9271    ///
9272    /// Strategy: build a large tree (many BINs), delete almost all keys,
9273    /// then verify compress() reduces n_bins and all surviving keys remain
9274    /// findable.  We do not hard-code the exact BIN counts because the
9275    /// preemptive splitting strategy determines the exact split points.
9276    #[test]
9277    fn test_compress_merges_underfull_bins() {
9278        let tree = Tree::new(1, 8);
9279
9280        // Insert 64 sorted keys to build a multi-BIN tree.
9281        let n = 64u32;
9282        let keys: Vec<Vec<u8>> =
9283            (0..n).map(|i| format!("cm{:04}", i).into_bytes()).collect();
9284        for (i, key) in keys.iter().enumerate() {
9285            tree.insert(key.clone(), vec![i as u8], Lsn::new(1, i as u32))
9286                .unwrap();
9287        }
9288
9289        let stats_full = tree.collect_stats();
9290        assert!(
9291            stats_full.n_bins >= 2,
9292            "must have multiple BINs after 64 inserts"
9293        );
9294
9295        // Delete all but 4 widely-spaced keys (one roughly per BIN pair).
9296        // We keep every 16th key: k0000, k0016, k0032, k0048.
9297        let keep: std::collections::HashSet<u32> =
9298            [0, 16, 32, 48].iter().cloned().collect();
9299        for i in 0..n {
9300            if !keep.contains(&i) {
9301                let key = format!("cm{:04}", i).into_bytes();
9302                tree.delete(&key);
9303            }
9304        }
9305
9306        let stats_sparse = tree.collect_stats();
9307        assert!(
9308            stats_sparse.n_bins >= 2,
9309            "should still have multiple BINs before compress"
9310        );
9311
9312        // compress() must reduce BIN count since most BINs now hold 0–1 entries.
9313        tree.compress();
9314
9315        let stats_after = tree.collect_stats();
9316        assert!(
9317            stats_after.n_bins < stats_sparse.n_bins,
9318            "compress must reduce BIN count (was {}, now {})",
9319            stats_sparse.n_bins,
9320            stats_after.n_bins
9321        );
9322
9323        // Surviving keys must still be findable.
9324        for i in keep {
9325            let key = format!("cm{:04}", i).into_bytes();
9326            let sr = tree.search(&key);
9327            assert!(
9328                sr.is_some() && sr.unwrap().exact_parent_found,
9329                "key cm{:04} must survive compress",
9330                i
9331            );
9332        }
9333    }
9334
9335    /// compress() preserves all entries: a full-BIN tree has fewer merges
9336    /// but all keys remain accessible.
9337    #[test]
9338    fn test_compress_no_op_when_full() {
9339        // Insert exactly max_entries worth of keys into a single BIN — no split
9340        // will have occurred yet, and the BINs will all be reasonably full.
9341        // We can't prevent splits entirely (preemptive), but we can verify that
9342        // compress() never loses entries.
9343        let tree = Tree::new(1, 8);
9344        let n = 32u32;
9345        for i in 0..n {
9346            let key = format!("fn{:04}", i).into_bytes();
9347            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
9348        }
9349
9350        let stats_before = tree.collect_stats();
9351        tree.compress();
9352        let stats_after = tree.collect_stats();
9353
9354        // All keys still findable.
9355        for i in 0..n {
9356            let key = format!("fn{:04}", i).into_bytes();
9357            let sr = tree.search(&key);
9358            assert!(
9359                sr.is_some() && sr.unwrap().exact_parent_found,
9360                "key fn{:04} must be findable after compress",
9361                i
9362            );
9363        }
9364
9365        // BIN count must not increase.
9366        assert!(
9367            stats_after.n_bins <= stats_before.n_bins,
9368            "compress must not increase BIN count"
9369        );
9370    }
9371
9372    /// compress() on an empty tree must not panic.
9373    #[test]
9374    fn test_compress_empty_tree() {
9375        let tree = Tree::new(1, 4);
9376        tree.compress(); // must not panic
9377    }
9378
9379    /// Deterministic regression for the BIN/IN split-path check-then-act race
9380    /// (`.agent/archived-audits/bench/bug-bin-split-concurrency.md`).
9381    ///
9382    /// `insert_recursive_inner` checks `child.get_n_entries() >= max_entries`
9383    /// under a PARENT READ lock, drops that read lock (required — the split
9384    /// needs `parent.write()`), then calls `split_child`. In the drop→reacquire
9385    /// window a racing thread (a second splitter, or the INCompressor merging
9386    /// and CLEARING a sibling — `compress_node`'s `lb.entries.clear()`) can
9387    /// leave the child no longer full, or even empty. Pre-fix, `split_child`
9388    /// then built a `SplitEntries` from that stale child and
9389    /// `SplitEntries::get_key(split_index)` panicked with
9390    /// "index out of bounds: len is 0" on the empty entries vec.
9391    ///
9392    /// This test drives the exact interleaving deterministically: it builds a
9393    /// level-2 tree, empties a full BIN child in place (simulating the racing
9394    /// merge), then calls `split_child` on it directly. With the fix
9395    /// `split_child` re-validates fullness under the child write lock and
9396    /// returns `Ok(())` (a benign no-op); without the fix it panics in
9397    /// `get_key`.
9398    ///
9399    /// JE-faithful: `IN.split` re-checks `needsSplitting()` after latching the
9400    /// node it will split (IN.java IN.split / IN.needsSplitting).
9401    #[test]
9402    fn split_child_is_noop_when_child_no_longer_full() {
9403        let max_entries = 8usize;
9404        let tree = Tree::new(1, max_entries);
9405
9406        // Build a level-2 tree: insert enough sorted keys to force at least one
9407        // split so the root becomes an Internal node with BIN children.
9408        for i in 0..64u32 {
9409            tree.insert(
9410                format!("k{:04}", i).into_bytes(),
9411                vec![i as u8],
9412                Lsn::new(1, i),
9413            )
9414            .unwrap();
9415        }
9416
9417        let root_arc = tree.get_root().expect("root resident");
9418
9419        // Pick child slot 0 (any resident BIN child works — the panic is about
9420        // the child being empty at split time, not about how it got there).
9421        let child_arc = {
9422            let g = root_arc.read();
9423            let TreeNode::Internal(n) = &*g else {
9424                panic!("expected a level-2 tree (root should be Internal)");
9425            };
9426            n.get_child(0).expect("resident child at slot 0")
9427        };
9428        let child_index = 0usize;
9429
9430        // Simulate the racing merge: clear the child's entries in place, the
9431        // way `compress_node` clears the merged-away left sibling. This is the
9432        // stale state a second `split_child` (or a split racing the compressor)
9433        // observes after the fullness check was already passed under the now-
9434        // dropped parent read lock.
9435        {
9436            let mut cg = child_arc.write();
9437            match &mut *cg {
9438                TreeNode::Bottom(b) => {
9439                    b.entries.clear();
9440                    b.lsn_rep = LsnRep::Empty;
9441                    b.keys = KeyRep::new();
9442                }
9443                TreeNode::Internal(n) => {
9444                    n.entries.clear();
9445                    n.lsn_rep = LsnRep::Empty;
9446                    n.targets = TargetRep::None;
9447                }
9448            }
9449            assert_eq!(cg.get_n_entries(), 0, "child must now be empty");
9450        }
9451
9452        // Directly call the split path. Pre-fix this panics in
9453        // `SplitEntries::get_key(0)` on the empty vec; post-fix it re-validates
9454        // fullness under the child write lock and returns Ok(()) (no-op).
9455        let res = Tree::split_child(
9456            &root_arc,
9457            child_index,
9458            max_entries,
9459            Lsn::new(1, 999),
9460            SplitHint::Normal,
9461            b"k0000",
9462            None,  // no comparator
9463            false, // key_prefixing off
9464            None,  // no InListListener
9465        );
9466        assert!(
9467            res.is_ok(),
9468            "split_child on an emptied (no-longer-full) child must be a benign \
9469             no-op, got {:?}",
9470            res
9471        );
9472    }
9473
9474    /// After deleting all entries, compress() reduces BINs to 1.
9475    #[test]
9476    fn test_compress_removes_empty_bin_from_parent() {
9477        let tree = Tree::new(1, 4);
9478        // Insert enough keys to generate multiple BINs.
9479        let n = 16u32;
9480        for i in 0..n {
9481            let key = format!("ep{:04}", i).into_bytes();
9482            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
9483        }
9484
9485        let stats_before = tree.collect_stats();
9486        assert!(stats_before.n_bins >= 2, "need multiple BINs for this test");
9487
9488        // Delete everything except the very last key.
9489        for i in 0..n - 1 {
9490            let key = format!("ep{:04}", i).into_bytes();
9491            tree.delete(&key);
9492        }
9493
9494        tree.compress();
9495
9496        let stats_after = tree.collect_stats();
9497        assert!(
9498            stats_after.n_bins < stats_before.n_bins,
9499            "compress must reduce BIN count after mass deletion"
9500        );
9501
9502        // The surviving key must still be findable.
9503        let last_key = format!("ep{:04}", n - 1).into_bytes();
9504        let sr = tree.search(&last_key);
9505        assert!(
9506            sr.is_some() && sr.unwrap().exact_parent_found,
9507            "last key must survive after compress"
9508        );
9509    }
9510
9511    // ========================================================================
9512    // IC-1: prune_empty_bin must NOT remove a live entry when the BIN was
9513    // repopulated between the compressor observing it empty and the prune.
9514    // (Tree corruption / lost-write regression test.)
9515    // ========================================================================
9516
9517    /// Find a BIN arc that is currently empty (0 entries) and is NOT the
9518    /// root, returning it together with the `id_key` the compressor would
9519    /// have captured (here we just use any key that routes to that BIN).
9520    fn first_empty_non_root_bin(tree: &Tree) -> Option<Arc<RwLock<TreeNode>>> {
9521        let root = tree.get_root()?;
9522        for node in tree.rebuild_in_list() {
9523            if Arc::ptr_eq(&node, &root) {
9524                continue; // skip root (single-BIN tree is never pruned)
9525            }
9526            let is_empty_bin = {
9527                let g = node.read();
9528                matches!(&*g, TreeNode::Bottom(b) if b.entries.is_empty())
9529            };
9530            if is_empty_bin {
9531                return Some(node);
9532            }
9533        }
9534        None
9535    }
9536
9537    /// IC-1 (fail-pre / pass-post): the old `compress_bin` prune step called
9538    /// `self.delete(&id_key)`, which re-descends by key.  If a concurrent
9539    /// insert repopulated the empty BIN with a LIVE entry under that same
9540    /// `id_key`, `self.delete` would silently remove the live entry — a lost
9541    /// write.  `prune_empty_bin` re-validates `n_entries == 0` under the
9542    /// parent latch and must REMOVE NOTHING when the BIN is non-empty.
9543    ///
9544    /// JE `Tree.delete` / `searchDeletableSubTree` (Tree.java ~line 755-800):
9545    /// `bin.getNEntries() != 0` → NODE_NOT_EMPTY (abort prune).
9546    #[test]
9547    fn test_ic1_prune_empty_bin_aborts_when_repopulated() {
9548        let tree = Tree::new(1, 4);
9549        let n = 16u32;
9550        for i in 0..n {
9551            let key = format!("ic{:04}", i).into_bytes();
9552            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
9553        }
9554        assert!(
9555            tree.collect_stats().n_bins >= 2,
9556            "need multiple BINs for this test"
9557        );
9558
9559        // Empty out one whole BIN by deleting every key it holds.  We delete
9560        // the lowest 4 keys (ic0000..ic0003) which share the first BIN, then
9561        // physically compress it so it has 0 entries.
9562        for i in 0..4 {
9563            let key = format!("ic{:04}", i).into_bytes();
9564            tree.delete(&key);
9565        }
9566
9567        // Locate the now-empty BIN and the id_key the compressor would use.
9568        let empty_bin = match first_empty_non_root_bin(&tree) {
9569            Some(b) => b,
9570            // If the layout didn't leave an isolated empty BIN, the scenario
9571            // isn't reproducible on this build; treat as vacuously passing.
9572            None => return,
9573        };
9574
9575        // SIMULATE THE RACE: a concurrent insert repopulates the empty BIN
9576        // with a LIVE entry *before* the prune runs.  We insert directly into
9577        // the BIN arc to model the insert that lands after `now_empty` was
9578        // read.  Pick a key that routes to this BIN.
9579        let live_key = format!("ic{:04}", 1).into_bytes(); // was deleted above
9580        {
9581            let mut g = empty_bin.write();
9582            if let TreeNode::Bottom(b) = &mut *g {
9583                // T-2/T-3: route through the insert helper so entries/keys/
9584                // lsn_rep stay in lock step.
9585                b.insert_with_prefix(
9586                    live_key.clone(),
9587                    Lsn::new(1, 1),
9588                    Some(vec![0xAB]),
9589                );
9590            }
9591        }
9592        let id_key = {
9593            let g = empty_bin.read();
9594            match &*g {
9595                TreeNode::Bottom(b) => b.get_full_key(0).unwrap(),
9596                _ => unreachable!(),
9597            }
9598        };
9599
9600        // Prune must ABORT (return false) because the BIN is no longer empty,
9601        // and must NOT remove the live entry.
9602        let pruned = tree.prune_empty_bin(&id_key);
9603        assert!(!pruned, "IC-1: prune must abort when the BIN was repopulated");
9604
9605        // The live entry must still be present in the BIN.
9606        let still_there = {
9607            let g = empty_bin.read();
9608            match &*g {
9609                TreeNode::Bottom(b) => {
9610                    b.entries.iter().enumerate().any(|(i, _)| {
9611                        b.key_prefix.is_empty() && b.get_key(i) == live_key
9612                    })
9613                }
9614                _ => false,
9615            }
9616        };
9617        assert!(
9618            still_there,
9619            "IC-1: prune must not remove the repopulated live entry"
9620        );
9621    }
9622
9623    /// IC-1 companion: prune_empty_bin must abort when a cursor is parked on
9624    /// the (still-empty) BIN.  JE: `bin.nCursors() > 0` → CURSORS_EXIST.
9625    #[test]
9626    fn test_ic1_prune_empty_bin_aborts_with_cursor() {
9627        let tree = Tree::new(1, 4);
9628        for i in 0..16u32 {
9629            let key = format!("cu{:04}", i).into_bytes();
9630            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
9631        }
9632        for i in 0..4 {
9633            let key = format!("cu{:04}", i).into_bytes();
9634            tree.delete(&key);
9635        }
9636        let empty_bin = match first_empty_non_root_bin(&tree) {
9637            Some(b) => b,
9638            None => return,
9639        };
9640        // Park a cursor on the empty BIN.
9641        Tree::pin_bin(&empty_bin);
9642        // id_key: any key routing to this BIN. Use the first deleted key.
9643        let id_key = format!("cu{:04}", 0).into_bytes();
9644        let pruned = tree.prune_empty_bin(&id_key);
9645        assert!(
9646            !pruned,
9647            "IC-1: prune must abort when a cursor is parked on the BIN"
9648        );
9649        Tree::unpin_bin(&empty_bin);
9650    }
9651
9652    /// IC-1 happy path: prune_empty_bin removes the parent slot when the BIN
9653    /// really is empty, no cursors, not a delta.
9654    #[test]
9655    fn test_ic1_prune_empty_bin_succeeds_when_truly_empty() {
9656        let tree = Tree::new(1, 4);
9657        for i in 0..16u32 {
9658            let key = format!("ok{:04}", i).into_bytes();
9659            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
9660        }
9661        for i in 0..4 {
9662            let key = format!("ok{:04}", i).into_bytes();
9663            tree.delete(&key);
9664        }
9665        let bins_before = tree.collect_stats().n_bins;
9666        let empty_bin = match first_empty_non_root_bin(&tree) {
9667            Some(b) => b,
9668            None => return,
9669        };
9670        // id_key: a key that routes to this empty BIN (one of the deleted).
9671        let id_key = {
9672            // route by the lowest deleted key; it falls into the leftmost BIN.
9673            let _ = &empty_bin;
9674            format!("ok{:04}", 0).into_bytes()
9675        };
9676        let pruned = tree.prune_empty_bin(&id_key);
9677        assert!(pruned, "IC-1: prune must succeed on a truly empty BIN");
9678        let bins_after = tree.collect_stats().n_bins;
9679        assert!(
9680            bins_after < bins_before,
9681            "IC-1: pruned BIN slot must be removed from the parent (was {}, now {})",
9682            bins_before,
9683            bins_after
9684        );
9685        // Every surviving key must still be findable.
9686        for i in 4..16u32 {
9687            let key = format!("ok{:04}", i).into_bytes();
9688            assert!(
9689                tree.search(&key).is_some_and(|s| s.exact_parent_found),
9690                "surviving key ok{:04} must remain after prune",
9691                i
9692            );
9693        }
9694    }
9695
9696    // ========================================================================
9697    // Tests: latch-coupling validation (validate_parent_child /
9698    //        search_with_coupling)
9699    // ========================================================================
9700
9701    /// validate_parent_child returns true when the parent slot points at the
9702    /// expected child.
9703    #[test]
9704    fn test_validate_parent_child_correct_link() {
9705        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
9706            node_id: generate_node_id(),
9707            level: BIN_LEVEL,
9708            entries: vec![],
9709            key_prefix: Vec::new(),
9710            dirty: false,
9711            is_delta: false,
9712            last_full_lsn: NULL_LSN,
9713            last_delta_lsn: NULL_LSN,
9714            generation: 0,
9715            parent: None,
9716            expiration_in_hours: true,
9717            cursor_count: 0,
9718            prohibit_next_delta: false,
9719            lsn_rep: LsnRep::Empty,
9720            keys: KeyRep::new(),
9721            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9722        })));
9723
9724        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
9725            node_id: generate_node_id(),
9726            level: MAIN_LEVEL | 2,
9727            entries: vec![InEntry { key: vec![] }],
9728            targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
9729            dirty: false,
9730            generation: 0,
9731            parent: None,
9732            lsn_rep: LsnRep::Empty,
9733        })));
9734
9735        assert!(
9736            Tree::validate_parent_child(&root_arc, 0, &bin_arc),
9737            "link must be valid when parent slot 0 points at bin_arc"
9738        );
9739    }
9740
9741    /// validate_parent_child returns false when the slot index is out of range.
9742    #[test]
9743    fn test_validate_parent_child_out_of_range() {
9744        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
9745            node_id: generate_node_id(),
9746            level: MAIN_LEVEL | 2,
9747            entries: vec![],
9748            targets: TargetRep::None,
9749            dirty: false,
9750            generation: 0,
9751            parent: None,
9752            lsn_rep: LsnRep::Empty,
9753        })));
9754        let other_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
9755            node_id: generate_node_id(),
9756            level: BIN_LEVEL,
9757            entries: vec![],
9758            key_prefix: Vec::new(),
9759            dirty: false,
9760            is_delta: false,
9761            last_full_lsn: NULL_LSN,
9762            last_delta_lsn: NULL_LSN,
9763            generation: 0,
9764            parent: None,
9765            expiration_in_hours: true,
9766            cursor_count: 0,
9767            prohibit_next_delta: false,
9768            lsn_rep: LsnRep::Empty,
9769            keys: KeyRep::new(),
9770            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9771        })));
9772
9773        assert!(
9774            !Tree::validate_parent_child(&root_arc, 0, &other_arc),
9775            "link must be invalid when parent has no entries"
9776        );
9777    }
9778
9779    /// validate_parent_child returns false when the slot points at a different Arc.
9780    #[test]
9781    fn test_validate_parent_child_wrong_child() {
9782        let bin_a = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
9783            node_id: generate_node_id(),
9784            level: BIN_LEVEL,
9785            entries: vec![],
9786            key_prefix: Vec::new(),
9787            dirty: false,
9788            is_delta: false,
9789            last_full_lsn: NULL_LSN,
9790            last_delta_lsn: NULL_LSN,
9791            generation: 0,
9792            parent: None,
9793            expiration_in_hours: true,
9794            cursor_count: 0,
9795            prohibit_next_delta: false,
9796            lsn_rep: LsnRep::Empty,
9797            keys: KeyRep::new(),
9798            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9799        })));
9800        let bin_b = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
9801            node_id: generate_node_id(),
9802            level: BIN_LEVEL,
9803            entries: vec![],
9804            key_prefix: Vec::new(),
9805            dirty: false,
9806            is_delta: false,
9807            last_full_lsn: NULL_LSN,
9808            last_delta_lsn: NULL_LSN,
9809            generation: 0,
9810            parent: None,
9811            expiration_in_hours: true,
9812            cursor_count: 0,
9813            prohibit_next_delta: false,
9814            lsn_rep: LsnRep::Empty,
9815            keys: KeyRep::new(),
9816            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9817        })));
9818
9819        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
9820            node_id: generate_node_id(),
9821            level: MAIN_LEVEL | 2,
9822            entries: vec![InEntry { key: vec![] }],
9823            targets: TargetRep::Sparse(vec![(0, bin_a)]),
9824            dirty: false,
9825            generation: 0,
9826            parent: None,
9827            lsn_rep: LsnRep::Empty,
9828        })));
9829
9830        assert!(
9831            !Tree::validate_parent_child(&root_arc, 0, &bin_b),
9832            "link must be invalid when parent slot points at a different Arc"
9833        );
9834    }
9835
9836    /// search_with_coupling finds the same key as search().
9837    #[test]
9838    fn test_search_with_coupling_finds_existing_key() {
9839        let tree = Tree::new(1, 8);
9840        for i in 0u32..20 {
9841            let key = format!("c{:04}", i).into_bytes();
9842            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
9843        }
9844
9845        for i in 0u32..20 {
9846            let key = format!("c{:04}", i).into_bytes();
9847            let sr = tree.search_with_coupling(&key);
9848            assert!(
9849                sr.is_some() && sr.unwrap().exact_parent_found,
9850                "search_with_coupling must find c{:04}",
9851                i
9852            );
9853        }
9854    }
9855
9856    /// search_with_coupling returns false for a key not in the tree.
9857    #[test]
9858    fn test_search_with_coupling_missing_key() {
9859        let tree = Tree::new(1, 8);
9860        tree.insert(b"hello".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
9861
9862        let sr = tree.search_with_coupling(b"zzz");
9863        // The search result must either be None or have exact_parent_found=false.
9864        assert!(
9865            sr.is_none_or(|r| !r.exact_parent_found),
9866            "search_with_coupling must not find a key that was never inserted"
9867        );
9868    }
9869
9870    /// search_with_coupling on an empty tree returns None.
9871    #[test]
9872    fn test_search_with_coupling_empty_tree() {
9873        let tree = Tree::new(1, 8);
9874        assert!(tree.search_with_coupling(b"k").is_none());
9875    }
9876
9877    // ========================================================================
9878    // Tests: BIN-delta reconstitution (apply_delta_to_bin / mutate_to_full_bin)
9879    // ========================================================================
9880
9881    /// apply_delta_to_bin replaces existing entries and inserts new ones.
9882    ///
9883    /// BIN.applyDelta(): delta entries are authoritative and
9884    /// supersede full-BIN entries at the same key.
9885    #[test]
9886    fn test_apply_delta_to_bin_updates_and_inserts() {
9887        let mut base = BinStub {
9888            node_id: 1,
9889            level: BIN_LEVEL,
9890            entries: vec![
9891                BinEntry {
9892                    data: Some(b"old_a".to_vec()),
9893                    known_deleted: false,
9894                    dirty: false,
9895                    expiration_time: 0,
9896                },
9897                BinEntry {
9898                    data: Some(b"old_c".to_vec()),
9899                    known_deleted: false,
9900                    dirty: false,
9901                    expiration_time: 0,
9902                },
9903            ],
9904            key_prefix: Vec::new(),
9905            dirty: false,
9906            is_delta: false,
9907            last_full_lsn: NULL_LSN,
9908            last_delta_lsn: NULL_LSN,
9909            generation: 0,
9910            parent: None,
9911            expiration_in_hours: true,
9912            cursor_count: 0,
9913            prohibit_next_delta: false,
9914            lsn_rep: LsnRep::Empty,
9915            keys: KeyRep::from_keys(vec![b"a".to_vec(), b"c".to_vec()]),
9916            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9917        };
9918
9919        let delta_entries = vec![
9920            // Update existing key "a" with new data.
9921            (b"a".to_vec(), Lsn::new(1, 10), Some(b"new_a".to_vec())),
9922            // Insert new key "b".
9923            (b"b".to_vec(), Lsn::new(1, 20), Some(b"new_b".to_vec())),
9924        ];
9925
9926        Tree::apply_delta_to_bin(&mut base, delta_entries);
9927
9928        assert!(base.dirty, "base must be dirty after applying delta");
9929
9930        // Collect the full keys for assertions (T-2: keys live in the rep).
9931        let full_keys: Vec<Vec<u8>> = (0..base.entries.len())
9932            .map(|i| base.get_full_key(i).unwrap_or_default())
9933            .collect();
9934
9935        // "a" must be updated.
9936        let a_idx = full_keys.iter().position(|k| k == b"a").unwrap();
9937        assert_eq!(
9938            base.entries[a_idx].data.as_deref(),
9939            Some(b"new_a" as &[u8])
9940        );
9941
9942        // "b" must be newly inserted.
9943        assert!(full_keys.iter().any(|k| k == b"b"));
9944
9945        // "c" must still be present (untouched).
9946        assert!(full_keys.iter().any(|k| k == b"c"));
9947
9948        // Entries must be in sorted order.
9949        let mut sorted = full_keys.clone();
9950        sorted.sort();
9951        assert_eq!(
9952            full_keys, sorted,
9953            "entries must remain sorted after delta apply"
9954        );
9955    }
9956
9957    /// apply_delta_to_bin with an empty delta is a no-op (except dirty flag).
9958    #[test]
9959    fn test_apply_delta_to_bin_empty_delta() {
9960        let mut base = BinStub {
9961            node_id: 1,
9962            level: BIN_LEVEL,
9963            entries: vec![BinEntry {
9964                data: None,
9965                known_deleted: false,
9966                dirty: false,
9967                expiration_time: 0,
9968            }],
9969            key_prefix: Vec::new(),
9970            dirty: false,
9971            is_delta: false,
9972            last_full_lsn: NULL_LSN,
9973            last_delta_lsn: NULL_LSN,
9974            generation: 0,
9975            parent: None,
9976            expiration_in_hours: true,
9977            cursor_count: 0,
9978            prohibit_next_delta: false,
9979            lsn_rep: LsnRep::Empty,
9980            keys: KeyRep::from_keys(vec![b"x".to_vec()]),
9981            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
9982        };
9983        let n_before = base.entries.len();
9984        Tree::apply_delta_to_bin(&mut base, vec![]);
9985        assert_eq!(
9986            base.entries.len(),
9987            n_before,
9988            "empty delta must not change entry count"
9989        );
9990        assert!(base.dirty, "dirty must be set even for empty delta apply");
9991    }
9992
9993    /// mutate_to_full_bin reconstitutes a full BIN from a delta + base.
9994    ///
9995    /// BIN.mutateToFullBIN(BIN fullBIN): after mutation the
9996    /// `is_delta` flag must be cleared and the entries must contain both
9997    /// base and delta data.
9998    #[test]
9999    fn test_mutate_to_full_bin_merges_delta_and_base() {
10000        let base = BinStub {
10001            node_id: 2,
10002            level: BIN_LEVEL,
10003            entries: vec![
10004                BinEntry {
10005                    data: Some(b"base_aa".to_vec()),
10006                    known_deleted: false,
10007                    dirty: false,
10008                    expiration_time: 0,
10009                },
10010                BinEntry {
10011                    data: Some(b"base_cc".to_vec()),
10012                    known_deleted: false,
10013                    dirty: false,
10014                    expiration_time: 0,
10015                },
10016            ],
10017            key_prefix: Vec::new(),
10018            dirty: false,
10019            is_delta: false,
10020            last_full_lsn: NULL_LSN,
10021            last_delta_lsn: NULL_LSN,
10022            generation: 0,
10023            parent: None,
10024            expiration_in_hours: true,
10025            cursor_count: 0,
10026            prohibit_next_delta: false,
10027            lsn_rep: LsnRep::Empty,
10028            keys: KeyRep::from_keys(vec![b"aa".to_vec(), b"cc".to_vec()]),
10029            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10030        };
10031
10032        // The delta has a new entry "bb" and overwrites "aa".
10033        let mut delta = BinStub {
10034            node_id: 2,
10035            level: BIN_LEVEL,
10036            entries: vec![
10037                BinEntry {
10038                    data: Some(b"delta_aa".to_vec()),
10039                    known_deleted: false,
10040                    dirty: false,
10041                    expiration_time: 0,
10042                },
10043                BinEntry {
10044                    data: Some(b"delta_bb".to_vec()),
10045                    known_deleted: false,
10046                    dirty: false,
10047                    expiration_time: 0,
10048                },
10049            ],
10050            key_prefix: Vec::new(),
10051            dirty: true,
10052            is_delta: true,
10053            last_full_lsn: NULL_LSN,
10054            last_delta_lsn: NULL_LSN,
10055            generation: 0,
10056            parent: None,
10057            expiration_in_hours: true,
10058            cursor_count: 0,
10059            prohibit_next_delta: false,
10060            lsn_rep: LsnRep::Empty,
10061            keys: KeyRep::from_keys(vec![b"aa".to_vec(), b"bb".to_vec()]),
10062            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10063        };
10064
10065        Tree::mutate_to_full_bin(&mut delta, base);
10066
10067        // After mutation the node must be a full BIN.
10068        assert!(
10069            !delta.is_delta,
10070            "is_delta must be false after mutate_to_full_bin"
10071        );
10072        assert!(delta.dirty, "must be dirty after mutation");
10073
10074        // Collect full keys for assertions (T-2: keys live in the rep).
10075        let dk: Vec<Vec<u8>> = (0..delta.entries.len())
10076            .map(|i| delta.get_full_key(i).unwrap_or_default())
10077            .collect();
10078
10079        // "aa" must be the delta version.
10080        let aa_idx = dk.iter().position(|k| k == b"aa").unwrap();
10081        assert_eq!(
10082            delta.entries[aa_idx].data.as_deref(),
10083            Some(b"delta_aa" as &[u8])
10084        );
10085
10086        // "bb" must be present (from delta).
10087        assert!(dk.iter().any(|k| k == b"bb"));
10088
10089        // "cc" must be present (from base).
10090        assert!(dk.iter().any(|k| k == b"cc"));
10091
10092        // Three entries total, in sorted order.
10093        assert_eq!(delta.entries.len(), 3);
10094        let mut sorted = dk.clone();
10095        sorted.sort();
10096        assert_eq!(dk, sorted, "entries must be sorted after mutation");
10097    }
10098
10099    /// is_delta flag is correctly reported by bin_is_delta().
10100    #[test]
10101    fn test_bin_is_delta_flag() {
10102        let mut bin = BinStub {
10103            node_id: 1,
10104            level: BIN_LEVEL,
10105            entries: vec![],
10106            key_prefix: Vec::new(),
10107            dirty: false,
10108            is_delta: false,
10109            last_full_lsn: NULL_LSN,
10110            last_delta_lsn: NULL_LSN,
10111            generation: 0,
10112            parent: None,
10113            expiration_in_hours: true,
10114            cursor_count: 0,
10115            prohibit_next_delta: false,
10116            lsn_rep: LsnRep::Empty,
10117            keys: KeyRep::new(),
10118            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10119        };
10120        assert!(!Tree::bin_is_delta(&bin));
10121        bin.is_delta = true;
10122        assert!(Tree::bin_is_delta(&bin));
10123    }
10124
10125    // ========================================================================
10126    // Tests: mutate_to_full_bin_from_log
10127    // ========================================================================
10128
10129    /// mutate_to_full_bin_from_log is a no-op when the BIN is already full.
10130    #[test]
10131    fn test_mutate_to_full_bin_from_log_already_full() {
10132        let dir = tempfile::tempdir().unwrap();
10133        let fm = std::sync::Arc::new(
10134            noxu_log::FileManager::new(dir.path(), false, 10_000_000, 100)
10135                .unwrap(),
10136        );
10137        let lm = noxu_log::LogManager::new(fm, 3, 1024 * 1024, 4096);
10138
10139        let mut bin = BinStub {
10140            node_id: 1,
10141            level: BIN_LEVEL,
10142            entries: vec![BinEntry {
10143                data: Some(b"v1".to_vec()),
10144                known_deleted: false,
10145                dirty: false,
10146                expiration_time: 0,
10147            }],
10148            key_prefix: Vec::new(),
10149            dirty: false,
10150            is_delta: false, // already a full BIN
10151            last_full_lsn: NULL_LSN,
10152            last_delta_lsn: NULL_LSN,
10153            generation: 0,
10154            parent: None,
10155            expiration_in_hours: true,
10156            cursor_count: 0,
10157            prohibit_next_delta: false,
10158            lsn_rep: LsnRep::Empty,
10159            keys: KeyRep::from_keys(vec![b"key1".to_vec()]),
10160            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10161        };
10162
10163        Tree::mutate_to_full_bin_from_log(&mut bin, &lm);
10164
10165        // No-op: is_delta was already false, entries unchanged.
10166        assert!(!bin.is_delta);
10167        assert_eq!(bin.entries.len(), 1);
10168    }
10169
10170    /// mutate_to_full_bin_from_log with NULL_LSN promotes delta without base.
10171    ///
10172    /// When last_full_lsn is NULL_LSN the BIN has never been written as a full
10173    /// entry.  The function must clear is_delta and leave the delta entries
10174    /// as-is (they are the authoritative full state).
10175    #[test]
10176    fn test_mutate_to_full_bin_from_log_null_lsn() {
10177        let dir = tempfile::tempdir().unwrap();
10178        let fm = std::sync::Arc::new(
10179            noxu_log::FileManager::new(dir.path(), false, 10_000_000, 100)
10180                .unwrap(),
10181        );
10182        let lm = noxu_log::LogManager::new(fm, 3, 1024 * 1024, 4096);
10183
10184        let mut delta = BinStub {
10185            node_id: 2,
10186            level: BIN_LEVEL,
10187            entries: vec![BinEntry {
10188                data: Some(b"delta_a".to_vec()),
10189                known_deleted: false,
10190                dirty: true,
10191                expiration_time: 0,
10192            }],
10193            key_prefix: Vec::new(),
10194            dirty: true,
10195            is_delta: true,
10196            last_full_lsn: NULL_LSN, // no full BIN ever written
10197            last_delta_lsn: NULL_LSN,
10198            generation: 0,
10199            parent: None,
10200            expiration_in_hours: true,
10201            cursor_count: 0,
10202            prohibit_next_delta: false,
10203            lsn_rep: LsnRep::Empty,
10204            keys: KeyRep::from_keys(vec![b"a".to_vec()]),
10205            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10206        };
10207
10208        Tree::mutate_to_full_bin_from_log(&mut delta, &lm);
10209
10210        // is_delta must be cleared; the single delta entry is kept as-is.
10211        assert!(
10212            !delta.is_delta,
10213            "is_delta must be false after null-lsn promotion"
10214        );
10215        assert_eq!(delta.entries.len(), 1);
10216        assert_eq!(delta.entries[0].data.as_deref(), Some(b"delta_a" as &[u8]));
10217    }
10218
10219    /// mutate_to_full_bin_from_log reads full BIN from log and merges delta.
10220    ///
10221    /// Round-trip: serialize a full BIN, write it to a LogManager, record the
10222    /// LSN, then call mutate_to_full_bin_from_log on a delta referencing that
10223    /// LSN.  The result must contain base-only and delta-only entries with the
10224    /// delta winning on conflicts.
10225    #[test]
10226    fn test_mutate_to_full_bin_from_log_reads_and_merges() {
10227        let dir = tempfile::tempdir().unwrap();
10228        let fm = std::sync::Arc::new(
10229            noxu_log::FileManager::new(dir.path(), false, 10_000_000, 100)
10230                .unwrap(),
10231        );
10232        let lm = noxu_log::LogManager::new(fm, 3, 1024 * 1024, 4096);
10233
10234        // Build and serialize the full BIN that will be written to the log.
10235        let full_bin = BinStub {
10236            node_id: 42,
10237            level: BIN_LEVEL,
10238            entries: vec![
10239                BinEntry {
10240                    data: Some(b"base_val".to_vec()),
10241                    known_deleted: false,
10242                    dirty: false,
10243                    expiration_time: 0,
10244                },
10245                BinEntry {
10246                    data: Some(b"base_shared".to_vec()),
10247                    known_deleted: false,
10248                    dirty: false,
10249                    expiration_time: 0,
10250                },
10251            ],
10252            key_prefix: Vec::new(),
10253            dirty: false,
10254            is_delta: false,
10255            last_full_lsn: NULL_LSN,
10256            last_delta_lsn: NULL_LSN,
10257            generation: 0,
10258            parent: None,
10259            expiration_in_hours: true,
10260            cursor_count: 0,
10261            prohibit_next_delta: false,
10262            lsn_rep: LsnRep::Empty,
10263            keys: KeyRep::from_keys(vec![
10264                b"base_only".to_vec(),
10265                b"shared_key".to_vec(),
10266            ]),
10267            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10268        };
10269
10270        let payload = full_bin.serialize_full();
10271        let full_lsn = lm
10272            .log(
10273                noxu_log::LogEntryType::BIN,
10274                &payload,
10275                noxu_log::Provisional::No,
10276                true,
10277                false,
10278            )
10279            .expect("write full BIN to log");
10280        lm.flush_no_sync().expect("flush log");
10281
10282        // Build a delta BIN referencing the full BIN via last_full_lsn.
10283        let mut delta = BinStub {
10284            node_id: 42,
10285            level: BIN_LEVEL,
10286            entries: vec![
10287                // Overwrites "shared_key" from the base.
10288                BinEntry {
10289                    data: Some(b"delta_shared".to_vec()),
10290                    known_deleted: false,
10291                    dirty: true,
10292                    expiration_time: 0,
10293                },
10294                // New key only in the delta.
10295                BinEntry {
10296                    data: Some(b"delta_val".to_vec()),
10297                    known_deleted: false,
10298                    dirty: true,
10299                    expiration_time: 0,
10300                },
10301            ],
10302            key_prefix: Vec::new(),
10303            dirty: true,
10304            is_delta: true,
10305            last_full_lsn: full_lsn,
10306            last_delta_lsn: NULL_LSN,
10307            generation: 0,
10308            parent: None,
10309            expiration_in_hours: true,
10310            cursor_count: 0,
10311            prohibit_next_delta: false,
10312            lsn_rep: LsnRep::Empty,
10313            keys: KeyRep::from_keys(vec![
10314                b"shared_key".to_vec(),
10315                b"delta_only".to_vec(),
10316            ]),
10317            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10318        };
10319
10320        Tree::mutate_to_full_bin_from_log(&mut delta, &lm);
10321
10322        assert!(
10323            !delta.is_delta,
10324            "is_delta must be false after log-based mutation"
10325        );
10326        assert!(delta.dirty, "must be dirty after mutation");
10327
10328        // All three distinct keys must be present.
10329        let find = |k: &[u8]| -> Option<Vec<u8>> {
10330            (0..delta.entries.len())
10331                .find(|&i| delta.get_full_key(i).as_deref() == Some(k))
10332                .and_then(|i| delta.entries[i].data.clone())
10333        };
10334
10335        assert_eq!(
10336            find(b"base_only"),
10337            Some(b"base_val".to_vec()),
10338            "base-only key must be present"
10339        );
10340        assert_eq!(
10341            find(b"shared_key"),
10342            Some(b"delta_shared".to_vec()),
10343            "delta must win on shared_key"
10344        );
10345        assert_eq!(
10346            find(b"delta_only"),
10347            Some(b"delta_val".to_vec()),
10348            "delta-only key must be present"
10349        );
10350        assert_eq!(delta.entries.len(), 3, "must have exactly 3 entries");
10351
10352        // Entries must be in sorted order (by full key).
10353        let full_keys: Vec<Vec<u8>> = (0..delta.entries.len())
10354            .map(|i| delta.get_full_key(i).unwrap())
10355            .collect();
10356        let mut sorted_keys = full_keys.clone();
10357        sorted_keys.sort();
10358        assert_eq!(full_keys, sorted_keys, "entries must be in sorted order");
10359    }
10360
10361    // ========================================================================
10362    // Tests: deserialize_full key prefix recomputation
10363    // ========================================================================
10364
10365    /// deserialize_full recomputes key prefix from loaded full keys.
10366    ///
10367    /// IN.recalcKeyPrefix() called after materializing from log:
10368    /// a BIN loaded from the log should have prefix compression applied so
10369    /// that search performance matches an in-memory BIN.
10370    #[test]
10371    fn test_deserialize_full_recomputes_key_prefix() {
10372        // Build a BIN with a known common prefix and serialize it.
10373        let mut source = BinStub {
10374            node_id: 99,
10375            level: BIN_LEVEL,
10376            entries: vec![
10377                BinEntry {
10378                    data: None,
10379                    known_deleted: false,
10380                    dirty: false,
10381                    expiration_time: 0,
10382                },
10383                BinEntry {
10384                    data: None,
10385                    known_deleted: false,
10386                    dirty: false,
10387                    expiration_time: 0,
10388                },
10389                BinEntry {
10390                    data: None,
10391                    known_deleted: false,
10392                    dirty: false,
10393                    expiration_time: 0,
10394                },
10395            ],
10396            key_prefix: Vec::new(),
10397            dirty: false,
10398            is_delta: false,
10399            last_full_lsn: NULL_LSN,
10400            last_delta_lsn: NULL_LSN,
10401            generation: 0,
10402            parent: None,
10403            expiration_in_hours: true,
10404            cursor_count: 0,
10405            prohibit_next_delta: false,
10406            lsn_rep: LsnRep::Empty,
10407            keys: KeyRep::from_keys(vec![
10408                b"pfx:alpha".to_vec(),
10409                b"pfx:beta".to_vec(),
10410                b"pfx:gamma".to_vec(),
10411            ]),
10412            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10413        };
10414        source.recompute_key_prefix();
10415        // Verify the source has the expected prefix before serializing.
10416        assert_eq!(source.key_prefix, b"pfx:");
10417
10418        let payload = source.serialize_full();
10419
10420        // Deserialize and verify prefix is re-established.
10421        let loaded = BinStub::deserialize_full(&payload)
10422            .expect("deserialization must succeed");
10423
10424        assert_eq!(
10425            loaded.key_prefix, b"pfx:",
10426            "key prefix must be recomputed after deserialize_full"
10427        );
10428
10429        // All full keys must be reconstructable.
10430        for i in 0..loaded.entries.len() {
10431            let fk = loaded.get_full_key(i).unwrap();
10432            assert!(
10433                fk.starts_with(b"pfx:"),
10434                "full key {i} must start with prefix"
10435            );
10436        }
10437    }
10438
10439    /// deserialize_full with a single entry leaves key_prefix empty.
10440    ///
10441    /// A BIN with fewer than 2 entries cannot have a meaningful common prefix.
10442    #[test]
10443    fn test_deserialize_full_single_entry_no_prefix() {
10444        let source = BinStub {
10445            node_id: 7,
10446            level: BIN_LEVEL,
10447            entries: vec![BinEntry {
10448                data: None,
10449                known_deleted: false,
10450                dirty: false,
10451                expiration_time: 0,
10452            }],
10453            key_prefix: Vec::new(),
10454            dirty: false,
10455            is_delta: false,
10456            last_full_lsn: NULL_LSN,
10457            last_delta_lsn: NULL_LSN,
10458            generation: 0,
10459            parent: None,
10460            expiration_in_hours: true,
10461            cursor_count: 0,
10462            prohibit_next_delta: false,
10463            lsn_rep: LsnRep::Empty,
10464            keys: KeyRep::from_keys(vec![b"solo".to_vec()]),
10465            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10466        };
10467
10468        let payload = source.serialize_full();
10469        let loaded = BinStub::deserialize_full(&payload)
10470            .expect("deserialization must succeed");
10471
10472        assert!(
10473            loaded.key_prefix.is_empty(),
10474            "single-entry BIN must have empty prefix"
10475        );
10476        assert_eq!(loaded.get_full_key(0).unwrap(), b"solo");
10477    }
10478
10479    // ========================================================================
10480    // Tests: get_next_bin / get_prev_bin
10481    // ========================================================================
10482
10483    /// get_next_bin returns the entries of the next BIN to the right.
10484    ///
10485    /// Tree.getNextBin() / getNextIN(forward=true).
10486    #[test]
10487    fn test_get_next_bin_basic() {
10488        let tree = Tree::new(1, 4);
10489
10490        // Insert 8 sorted keys — creates multiple BINs.
10491        for i in 0u32..8 {
10492            let key = format!("n{:04}", i).into_bytes();
10493            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10494        }
10495
10496        let stats = tree.collect_stats();
10497        if stats.n_bins < 2 {
10498            // If the tree only has one BIN, skip the sibling test.
10499            return;
10500        }
10501
10502        // A key from the first BIN (e.g. "n0000") should have a next BIN.
10503        let next = tree.get_next_bin(b"n0000");
10504        assert!(
10505            next.is_some(),
10506            "must return a next BIN for a key in the leftmost BIN"
10507        );
10508
10509        let entries = next.unwrap();
10510        assert!(!entries.is_empty(), "next BIN must not be empty");
10511        // All returned keys must be strictly greater than "n0000" because they
10512        // are in a different (rightward) BIN.
10513        for (_, _, k) in &entries {
10514            assert!(
10515                k.as_slice() > b"n0000" as &[u8],
10516                "next BIN entries must all be > the search key"
10517            );
10518        }
10519    }
10520
10521    /// get_next_bin returns None for a key in the rightmost BIN.
10522    #[test]
10523    fn test_get_next_bin_at_rightmost_returns_none() {
10524        let tree = Tree::new(1, 4);
10525        for i in 0u32..8 {
10526            let key = format!("r{:04}", i).into_bytes();
10527            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10528        }
10529        // A key from the rightmost BIN (e.g. "r0007") has no next BIN.
10530        let next = tree.get_next_bin(b"r0007");
10531        assert!(
10532            next.is_none(),
10533            "must return None for a key in the rightmost BIN"
10534        );
10535    }
10536
10537    /// get_prev_bin returns the entries of the next BIN to the left.
10538    ///
10539    /// Tree.getPrevBin() / getNextIN(forward=false).
10540    #[test]
10541    fn test_get_prev_bin_basic() {
10542        let tree = Tree::new(1, 4);
10543        for i in 0u32..8 {
10544            let key = format!("p{:04}", i).into_bytes();
10545            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10546        }
10547
10548        // A key from the second BIN ("p0004") should have a previous BIN.
10549        let prev = tree.get_prev_bin(b"p0004");
10550        assert!(
10551            prev.is_some(),
10552            "must return a prev BIN for a key in the second BIN"
10553        );
10554
10555        let entries = prev.unwrap();
10556        assert!(!entries.is_empty(), "prev BIN must not be empty");
10557        // All returned keys must be < b"p0004".
10558        for (_, _, k) in &entries {
10559            assert!(
10560                k.as_slice() < b"p0004" as &[u8],
10561                "prev BIN entries must all be < the current BIN"
10562            );
10563        }
10564    }
10565
10566    /// get_prev_bin returns None for a key in the leftmost BIN.
10567    #[test]
10568    fn test_get_prev_bin_at_leftmost_returns_none() {
10569        let tree = Tree::new(1, 4);
10570        for i in 0u32..8 {
10571            let key = format!("q{:04}", i).into_bytes();
10572            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10573        }
10574        // A key from the leftmost BIN ("q0000") has no prev BIN.
10575        let prev = tree.get_prev_bin(b"q0000");
10576        assert!(
10577            prev.is_none(),
10578            "must return None for a key in the leftmost BIN"
10579        );
10580    }
10581
10582    /// get_next_bin and get_prev_bin are inverse operations across the
10583    /// BIN boundary.
10584    #[test]
10585    fn test_next_prev_bin_are_symmetric() {
10586        let tree = Tree::new(1, 4);
10587        for i in 0u32..8 {
10588            let key = format!("s{:04}", i).into_bytes();
10589            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10590        }
10591
10592        // From first BIN (s0000): next → second BIN entries.
10593        let next_from_first = tree.get_next_bin(b"s0000").unwrap();
10594        // The smallest key of the next BIN.
10595        let next_first_key =
10596            next_from_first.iter().map(|(_, _, k)| k.clone()).min().unwrap();
10597
10598        // From that key in the second BIN: prev → should overlap with first BIN.
10599        let prev_from_second = tree.get_prev_bin(&next_first_key).unwrap();
10600        let prev_first_key =
10601            prev_from_second.iter().map(|(_, _, k)| k.clone()).max().unwrap();
10602
10603        // The max key of the "prev" result must be in the first BIN (< next boundary).
10604        assert!(
10605            prev_first_key < next_first_key,
10606            "prev BIN entries must be smaller than the boundary key"
10607        );
10608    }
10609
10610    /// get_next_bin on an empty tree returns None.
10611    #[test]
10612    fn test_get_next_bin_empty_tree() {
10613        let tree = Tree::new(1, 8);
10614        assert!(tree.get_next_bin(b"any").is_none());
10615    }
10616
10617    /// get_prev_bin on an empty tree returns None.
10618    #[test]
10619    fn test_get_prev_bin_empty_tree() {
10620        let tree = Tree::new(1, 8);
10621        assert!(tree.get_prev_bin(b"any").is_none());
10622    }
10623
10624    // =========================================================================
10625    // R3 fix: get_next_bin / get_prev_bin honour the custom comparator
10626    // =========================================================================
10627
10628    /// R3 regression test: with a custom comparator that reverses byte order
10629    /// (descending), `get_next_bin` and `get_prev_bin` must use comparator
10630    /// order when routing through internal nodes.
10631    ///
10632    /// Pre-fix: the static `get_adjacent_bin_attempt` used raw `<=` byte order
10633    /// for IN routing, causing it to descend to the wrong child when comparator
10634    /// order ≠ byte order.
10635    ///
10636    /// The tree is forced to split (max_entries = 4) so there IS an internal
10637    /// node (IN) to route through. Under a reverse comparator the insertion
10638    /// order and stored key order are reversed relative to byte order, so any
10639    /// descent that uses raw byte comparison will pick the wrong slot.
10640    ///
10641    /// Pass-post invariant: iterating forward via repeated `get_next_bin` from
10642    /// the leftmost BIN yields keys in COMPARATOR order (descending byte order
10643    /// here), not in raw ascending byte order.
10644    #[test]
10645    fn test_get_next_prev_bin_custom_comparator_order() {
10646        // Reverse-order comparator: larger bytes sort first.
10647        let reverse_cmp: KeyComparatorFn =
10648            Arc::new(|a: &[u8], b: &[u8]| b.cmp(a));
10649        // Small max_entries so the tree splits and has internal nodes.
10650        let mut tree = Tree::new(1, 4);
10651        tree.set_comparator(reverse_cmp);
10652
10653        // Insert keys that are ascending in byte order ("a" < "b" < … < "i")
10654        // but descending in comparator order (i > h > … > a).
10655        let keys: &[&[u8]] =
10656            &[b"a", b"b", b"c", b"d", b"e", b"f", b"g", b"h", b"i"];
10657        for (i, k) in keys.iter().enumerate() {
10658            tree.insert(
10659                k.to_vec(),
10660                vec![i as u8],
10661                Lsn::from_u64((i + 1) as u64),
10662            )
10663            .unwrap();
10664        }
10665
10666        // Collect all BINs by walking from the comparator-smallest key ("i"
10667        // in reverse order) using get_next_bin. The anchor must be a key that
10668        // is smaller than everything in comparator order, i.e. the largest
10669        // byte-value key. We use the tree's search to find the actual leftmost
10670        // key under the comparator by starting from "i" (comparator-min).
10671        //
10672        // Strategy: start at byte key b"\xff" (larger than any inserted key in
10673        // byte order, so it lands in the last BIN in byte order, which under
10674        // a reverse comparator is the leftmost BIN in comparator order). Then
10675        // walk via get_next_bin.
10676        let start_anchor = b"\xff".as_ref();
10677        let mut bin_first_keys: Vec<Vec<u8>> = Vec::new();
10678
10679        // The first BIN in comparator order contains "i" (largest byte key).
10680        // get_next_bin from a virtual start in that BIN gives the next one.
10681        // Collect by walking from the comparator-last key leftward instead:
10682        // use get_next_bin with anchor = b"\xff" to hop to the next BIN
10683        // (comparator order: next = smaller byte value).
10684        let mut anchor = start_anchor.to_vec();
10685        loop {
10686            match tree.get_next_bin(&anchor) {
10687                None => break,
10688                Some(entries) => {
10689                    if let Some((_, _, fk0)) = entries.first() {
10690                        let fk = fk0.clone();
10691                        bin_first_keys.push(fk.clone());
10692                        anchor = fk;
10693                    } else {
10694                        break;
10695                    }
10696                }
10697            }
10698        }
10699
10700        // We must have visited at least 2 BINs (tree was forced to split).
10701        assert!(
10702            bin_first_keys.len() >= 2,
10703            "R3: expected multiple BINs after split, got {}",
10704            bin_first_keys.len()
10705        );
10706
10707        // With a reverse comparator, bin_first_keys must be in descending byte
10708        // order (each successive BIN starts at a smaller byte key).
10709        for window in bin_first_keys.windows(2) {
10710            assert!(
10711                window[0] > window[1],
10712                "R3: BIN boundary keys must be descending (comparator order); \
10713                 got {:?} then {:?}",
10714                window[0],
10715                window[1]
10716            );
10717        }
10718    }
10719    // ========================================================================
10720
10721    /// Inserting keys with a common prefix causes the BIN to establish that
10722    /// prefix.  Stored suffixes are shorter than the full keys.
10723    #[test]
10724    fn test_binstub_prefix_established_on_insert() {
10725        let mut bin = BinStub {
10726            node_id: 1,
10727            level: BIN_LEVEL,
10728            entries: Vec::new(),
10729            key_prefix: Vec::new(),
10730            dirty: false,
10731            is_delta: false,
10732            last_full_lsn: NULL_LSN,
10733            last_delta_lsn: NULL_LSN,
10734            generation: 0,
10735            parent: None,
10736            expiration_in_hours: true,
10737            cursor_count: 0,
10738            prohibit_next_delta: false,
10739            lsn_rep: LsnRep::Empty,
10740            keys: KeyRep::new(),
10741            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10742        };
10743
10744        bin.insert_with_prefix(b"record:aaa".to_vec(), Lsn::new(1, 1), None);
10745        assert!(bin.key_prefix.is_empty(), "single entry: no prefix yet");
10746
10747        bin.insert_with_prefix(b"record:bbb".to_vec(), Lsn::new(1, 2), None);
10748        assert_eq!(
10749            &bin.key_prefix, b"record:",
10750            "common prefix 'record:' must be extracted"
10751        );
10752    }
10753
10754    /// `get_full_key` on a BinStub returns the full key regardless of whether
10755    /// the stored key is a raw full key or a suffix.
10756    #[test]
10757    fn test_binstub_get_full_key_roundtrip() {
10758        let mut bin = BinStub {
10759            node_id: 1,
10760            level: BIN_LEVEL,
10761            entries: Vec::new(),
10762            key_prefix: Vec::new(),
10763            dirty: false,
10764            is_delta: false,
10765            last_full_lsn: NULL_LSN,
10766            last_delta_lsn: NULL_LSN,
10767            generation: 0,
10768            parent: None,
10769            expiration_in_hours: true,
10770            cursor_count: 0,
10771            prohibit_next_delta: false,
10772            lsn_rep: LsnRep::Empty,
10773            keys: KeyRep::new(),
10774            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10775        };
10776
10777        let keys = [
10778            b"pfx:first".as_ref(),
10779            b"pfx:second".as_ref(),
10780            b"pfx:third".as_ref(),
10781        ];
10782        for k in keys {
10783            bin.insert_with_prefix(k.to_vec(), Lsn::new(1, 1), None);
10784        }
10785
10786        assert!(!bin.key_prefix.is_empty(), "prefix must be set");
10787
10788        for (i, expected) in keys.iter().enumerate() {
10789            let full = bin.get_full_key(i).expect("must return full key");
10790            assert_eq!(
10791                full.as_slice(),
10792                *expected,
10793                "get_full_key({}) must return full key",
10794                i
10795            );
10796        }
10797    }
10798
10799    /// `find_entry_compressed` on a BinStub with active prefix returns the
10800    /// correct slot index.
10801    #[test]
10802    fn test_binstub_find_entry_compressed() {
10803        let mut bin = BinStub {
10804            node_id: 1,
10805            level: BIN_LEVEL,
10806            entries: Vec::new(),
10807            key_prefix: Vec::new(),
10808            dirty: false,
10809            is_delta: false,
10810            last_full_lsn: NULL_LSN,
10811            last_delta_lsn: NULL_LSN,
10812            generation: 0,
10813            parent: None,
10814            expiration_in_hours: true,
10815            cursor_count: 0,
10816            prohibit_next_delta: false,
10817            lsn_rep: LsnRep::Empty,
10818            keys: KeyRep::new(),
10819            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10820        };
10821
10822        for k in
10823            [b"db:alpha".as_ref(), b"db:beta".as_ref(), b"db:gamma".as_ref()]
10824        {
10825            bin.insert_with_prefix(k.to_vec(), Lsn::new(1, 1), None);
10826        }
10827
10828        let (idx, found) = bin.find_entry_compressed(b"db:beta");
10829        assert!(found, "db:beta must be found");
10830        assert_eq!(idx, 1, "db:beta must be at index 1");
10831
10832        let (_, not_found) = bin.find_entry_compressed(b"db:zzz");
10833        assert!(!not_found, "db:zzz must not be found");
10834    }
10835
10836    /// Tree insert/search works correctly when BINs accumulate a key prefix.
10837    #[test]
10838    fn test_tree_insert_search_with_prefix_compression() {
10839        let tree = Tree::new(1, 8);
10840        let n = 200u32;
10841
10842        // All keys share a long common prefix — good for prefix compression.
10843        for i in 0..n {
10844            let key = format!("namespace:entity:{:06}", i).into_bytes();
10845            let data = vec![i as u8];
10846            tree.insert(key, data, Lsn::new(1, i)).unwrap();
10847        }
10848
10849        // All keys must be findable.
10850        for i in 0..n {
10851            let key = format!("namespace:entity:{:06}", i).into_bytes();
10852            let sr = tree.search(&key);
10853            assert!(
10854                sr.is_some() && sr.unwrap().exact_parent_found,
10855                "key namespace:entity:{:06} must be found",
10856                i
10857            );
10858        }
10859    }
10860
10861    /// Prefix survives a BIN split: keys in both halves must still be findable.
10862    #[test]
10863    fn test_prefix_preserved_across_bin_split() {
10864        // Small fanout to force splits quickly.
10865        let tree = Tree::new(1, 4);
10866
10867        for i in 0u32..20 {
10868            let key = format!("pfx:key:{:04}", i).into_bytes();
10869            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10870        }
10871
10872        // All keys must be findable after splits.
10873        for i in 0u32..20 {
10874            let key = format!("pfx:key:{:04}", i).into_bytes();
10875            let sr = tree.search(&key);
10876            assert!(
10877                sr.is_some() && sr.unwrap().exact_parent_found,
10878                "pfx:key:{:04} must be found after splits",
10879                i
10880            );
10881        }
10882    }
10883
10884    /// `decompress_key` round-trips: compress then decompress gives the original.
10885    #[test]
10886    fn test_binstub_compress_decompress_roundtrip() {
10887        let mut bin = BinStub {
10888            node_id: 1,
10889            level: BIN_LEVEL,
10890            entries: Vec::new(),
10891            key_prefix: Vec::new(),
10892            dirty: false,
10893            is_delta: false,
10894            last_full_lsn: NULL_LSN,
10895            last_delta_lsn: NULL_LSN,
10896            generation: 0,
10897            parent: None,
10898            expiration_in_hours: true,
10899            cursor_count: 0,
10900            prohibit_next_delta: false,
10901            lsn_rep: LsnRep::Empty,
10902            keys: KeyRep::new(),
10903            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
10904        };
10905
10906        for k in [b"myapp:user:1".as_ref(), b"myapp:user:2".as_ref()] {
10907            bin.insert_with_prefix(k.to_vec(), Lsn::new(1, 1), None);
10908        }
10909
10910        assert!(!bin.key_prefix.is_empty());
10911
10912        // Manually compress a full key and then decompress it.
10913        let full_key = b"myapp:user:3";
10914        let suffix = bin.compress_key(full_key);
10915        let recovered = bin.decompress_key(&suffix);
10916        assert_eq!(
10917            recovered.as_slice(),
10918            full_key,
10919            "compress→decompress must be identity"
10920        );
10921    }
10922
10923    /// get_next_bin correctly navigates a 3-level tree.
10924    #[test]
10925    fn test_get_next_bin_three_level_tree() {
10926        // With fanout 4, inserting 20 keys forces a root split → 3 levels.
10927        let tree = Tree::new(1, 4);
10928        for i in 0u32..20 {
10929            let key = format!("t{:04}", i).into_bytes();
10930            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
10931        }
10932        assert!(tree.get_root_splits() > 0, "tree must have grown to 3 levels");
10933
10934        // Starting from t0000, iterating via get_next_bin must visit every BIN.
10935        let mut visited: Vec<Vec<u8>> = Vec::new();
10936        // Collect the first BIN's keys by searching for t0000.
10937        if let Some(first_entries) = {
10938            // Get the leftmost BIN by using get_first_node result.
10939            // get_first_node returns SearchResult at index 0 in the leftmost BIN.
10940            // We approximate by reading the root's leftmost BIN directly.
10941            tree.get_next_bin(b"t0000")
10942        } {
10943            for (_, _, k) in first_entries {
10944                visited.push(k);
10945            }
10946        }
10947
10948        // visited should contain at least one key from the second BIN.
10949        assert!(
10950            !visited.is_empty(),
10951            "should have visited at least one key via get_next_bin in 3-level tree"
10952        );
10953    }
10954
10955    // ========================================================================
10956    // ========================================================================
10957
10958    /// insert a small set of keys
10959    /// with varying lengths and verify each is findable immediately after insert.
10960    #[test]
10961    fn test_je_simple_tree_creation() {
10962        let tree = Tree::new(1, 128);
10963
10964        let keys: &[&[u8]] = &[b"aaaaa", b"aaaab", b"aaaa", b"aaa"];
10965        for (i, &k) in keys.iter().enumerate() {
10966            tree.insert(k.to_vec(), vec![i as u8], Lsn::new(1, i as u32))
10967                .unwrap();
10968
10969            // Every key inserted so far must be findable.
10970            for &prev in &keys[..=i] {
10971                let sr = tree.search(prev);
10972                assert!(
10973                    sr.is_some() && sr.unwrap().exact_parent_found,
10974                    "key {:?} must be findable after {} inserts",
10975                    std::str::from_utf8(prev).unwrap_or("?"),
10976                    i + 1
10977                );
10978            }
10979        }
10980    }
10981
10982    /// insert N keys, verify
10983    /// all are found; delete the even-indexed keys, verify even are gone and
10984    /// odd remain.
10985    #[test]
10986    fn test_je_insert_then_delete_then_search() {
10987        let tree = Tree::new(1, 8);
10988        let n = 20usize;
10989
10990        let keys: Vec<Vec<u8>> =
10991            (0..n).map(|i| format!("key{:04}", i).into_bytes()).collect();
10992
10993        // Insert all.
10994        for (i, k) in keys.iter().enumerate() {
10995            tree.insert(k.clone(), vec![i as u8], Lsn::new(1, i as u32))
10996                .unwrap();
10997        }
10998
10999        // All must be findable.
11000        for k in &keys {
11001            let sr = tree.search(k);
11002            assert!(
11003                sr.is_some() && sr.unwrap().exact_parent_found,
11004                "key {:?} must be found after insert",
11005                std::str::from_utf8(k).unwrap_or("?")
11006            );
11007        }
11008
11009        // Delete even-indexed keys.
11010        for i in (0..n).step_by(2) {
11011            tree.delete(&keys[i]);
11012        }
11013
11014        // Even keys must no longer be found; odd keys must still be found.
11015        for (i, key) in keys.iter().enumerate() {
11016            let sr = tree.search(key);
11017            let found = sr.is_some() && sr.unwrap().exact_parent_found;
11018            if i % 2 == 0 {
11019                assert!(!found, "deleted key {:?} must not be found", i);
11020            } else {
11021                assert!(found, "kept key {:?} must still be found", i);
11022            }
11023        }
11024    }
11025
11026    /// insert N keys in reverse
11027    /// order, then verify every key is directly findable and the keys are in
11028    /// sorted ascending order (B-tree ordering invariant).
11029    #[test]
11030    fn test_je_range_scan_sorted_ascending() {
11031        let n = 40usize;
11032        let tree = Tree::new(1, 4);
11033
11034        // Insert in reverse order to stress the B-tree.
11035        for i in (0..n).rev() {
11036            let key = format!("scan{:04}", i).into_bytes();
11037            tree.insert(key, vec![i as u8], Lsn::new(1, i as u32)).unwrap();
11038        }
11039
11040        // Collect all expected keys in sorted order.
11041        let mut expected: Vec<Vec<u8>> =
11042            (0..n).map(|i| format!("scan{:04}", i).into_bytes()).collect();
11043        expected.sort();
11044
11045        // Every key must be individually findable.
11046        for key in &expected {
11047            let sr = tree.search(key);
11048            assert!(
11049                sr.is_some() && sr.unwrap().exact_parent_found,
11050                "key {:?} must be findable",
11051                std::str::from_utf8(key).unwrap_or("?")
11052            );
11053        }
11054
11055        // Verify sorted ordering invariant: expected keys are already sorted
11056        // (lexicographic order = insertion order for "scan{:04}" keys).
11057        for w in expected.windows(2) {
11058            assert!(
11059                w[0] < w[1],
11060                "keys must be in strict ascending order: {:?} < {:?}",
11061                std::str::from_utf8(&w[0]).unwrap_or("?"),
11062                std::str::from_utf8(&w[1]).unwrap_or("?")
11063            );
11064        }
11065
11066        // Use get_next_bin to scan at least a portion of the tree and verify
11067        // ordering of returned BIN entries.
11068        let first_key = format!("scan{:04}", 0).into_bytes();
11069        if let Some(entries) = tree.get_next_bin(&first_key) {
11070            let entry_keys: Vec<&[u8]> =
11071                entries.iter().map(|(_, _, k)| k.as_slice()).collect();
11072            for w in entry_keys.windows(2) {
11073                assert!(
11074                    w[0] <= w[1],
11075                    "BIN entries from get_next_bin must be in ascending order"
11076                );
11077            }
11078        }
11079    }
11080
11081    /// insert N keys in
11082    /// ascending order and verify the tree height stays bounded (≤ 10 levels)
11083    /// and all keys are findable.
11084    #[test]
11085    fn test_je_ascending_insert_balance() {
11086        let n = 128usize;
11087        let tree = Tree::new(1, 8);
11088
11089        for i in 0..n {
11090            let key = format!("asc{:06}", i).into_bytes();
11091            tree.insert(key, vec![(i & 0xFF) as u8], Lsn::new(1, i as u32))
11092                .unwrap();
11093        }
11094
11095        let stats = tree.collect_stats();
11096        assert!(
11097            stats.height <= 10,
11098            "tree height after {} ascending inserts with fanout 8 must be <= 10, got {}",
11099            n,
11100            stats.height
11101        );
11102
11103        for i in 0..n {
11104            let key = format!("asc{:06}", i).into_bytes();
11105            let sr = tree.search(&key);
11106            assert!(
11107                sr.is_some() && sr.unwrap().exact_parent_found,
11108                "key asc{:06} must be findable after ascending inserts",
11109                i
11110            );
11111        }
11112    }
11113
11114    /// insert N keys in
11115    /// descending order and verify the tree height stays bounded (≤ 10 levels)
11116    /// and all keys are findable.
11117    #[test]
11118    fn test_je_descending_insert_balance() {
11119        let n = 128usize;
11120        let tree = Tree::new(1, 8);
11121
11122        for i in (0..n).rev() {
11123            let key = format!("dsc{:06}", i).into_bytes();
11124            tree.insert(key, vec![(i & 0xFF) as u8], Lsn::new(1, i as u32))
11125                .unwrap();
11126        }
11127
11128        let stats = tree.collect_stats();
11129        assert!(
11130            stats.height <= 10,
11131            "tree height after {} descending inserts with fanout 8 must be <= 10, got {}",
11132            n,
11133            stats.height
11134        );
11135
11136        for i in 0..n {
11137            let key = format!("dsc{:06}", i).into_bytes();
11138            let sr = tree.search(&key);
11139            assert!(
11140                sr.is_some() && sr.unwrap().exact_parent_found,
11141                "key dsc{:06} must be findable after descending inserts",
11142                i
11143            );
11144        }
11145    }
11146
11147    /// SplitTest invariant: after many splits induced by a small
11148    /// fanout no key is lost.
11149    #[test]
11150    fn test_je_split_no_key_lost() {
11151        let tree = Tree::new(1, 4);
11152        let n = 20usize;
11153
11154        for i in 0..n {
11155            let key = format!("sp{:04}", i).into_bytes();
11156            tree.insert(key, vec![i as u8], Lsn::new(1, i as u32)).unwrap();
11157        }
11158
11159        for i in 0..n {
11160            let key = format!("sp{:04}", i).into_bytes();
11161            let sr = tree.search(&key);
11162            assert!(
11163                sr.is_some() && sr.unwrap().exact_parent_found,
11164                "key sp{:04} must survive all splits",
11165                i
11166            );
11167        }
11168    }
11169
11170    /// SplitTest invariant: after a BIN split both halves exist and
11171    /// all original keys are findable.
11172    #[test]
11173    fn test_je_split_produces_two_halves() {
11174        // fanout=4: fill one BIN then overflow it to force a split.
11175        let tree = Tree::new(1, 4);
11176        let n = 5usize; // one more than fanout → forces at least one split
11177
11178        for i in 0..n {
11179            let key = format!("half{:04}", i).into_bytes();
11180            tree.insert(key, vec![i as u8], Lsn::new(1, i as u32)).unwrap();
11181        }
11182
11183        let stats = tree.collect_stats();
11184        assert!(
11185            stats.n_bins >= 2,
11186            "after splitting a full BIN there must be >= 2 BINs, got {}",
11187            stats.n_bins
11188        );
11189
11190        for i in 0..n {
11191            let key = format!("half{:04}", i).into_bytes();
11192            let sr = tree.search(&key);
11193            assert!(
11194                sr.is_some() && sr.unwrap().exact_parent_found,
11195                "key half{:04} must be findable in one of the two halves",
11196                i
11197            );
11198        }
11199    }
11200
11201    /// SplitTest invariant: root splits are tracked and the tree
11202    /// grows in height as keys accumulate.
11203    #[test]
11204    fn test_je_root_split_creates_new_root() {
11205        // fanout=4, 20 keys: forces multiple root splits.
11206        let tree = Tree::new(1, 4);
11207
11208        for i in 0u32..20 {
11209            let key = format!("rs{:04}", i).into_bytes();
11210            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
11211        }
11212
11213        assert!(
11214            tree.get_root_splits() > 0,
11215            "expected at least one root split after 20 inserts with fanout 4"
11216        );
11217
11218        let stats = tree.collect_stats();
11219        assert!(
11220            stats.height >= 3,
11221            "tree must be at least 3 levels tall after root splits, got {}",
11222            stats.height
11223        );
11224
11225        // Every inserted key must still be findable.
11226        for i in 0u32..20 {
11227            let key = format!("rs{:04}", i).into_bytes();
11228            let sr = tree.search(&key);
11229            assert!(
11230                sr.is_some() && sr.unwrap().exact_parent_found,
11231                "key rs{:04} must be findable after root splits",
11232                i
11233            );
11234        }
11235    }
11236
11237    // ========================================================================
11238    // Tests: compress_bin / maybe_compress_bin_and_parent
11239    // INCompressor.compressBin / lazyCompress tests
11240    // ========================================================================
11241
11242    /// compress_bin removes known-deleted slots from a BIN.
11243    ///
11244    /// INCompressor.compressBin(): after compression, slots with
11245    /// `known_deleted = true` must be gone and the BIN must be dirty.
11246    #[test]
11247    fn test_compress_bin_removes_deleted_slots() {
11248        let _lsn = Lsn::new(1, 1);
11249        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11250            node_id: generate_node_id(),
11251            level: BIN_LEVEL,
11252            entries: vec![
11253                BinEntry {
11254                    data: Some(b"live".to_vec()),
11255                    known_deleted: false,
11256                    dirty: false,
11257                    expiration_time: 0,
11258                },
11259                BinEntry {
11260                    data: None,
11261                    known_deleted: true,
11262                    dirty: false,
11263                    expiration_time: 0,
11264                },
11265                BinEntry {
11266                    data: Some(b"live2".to_vec()),
11267                    known_deleted: false,
11268                    dirty: false,
11269                    expiration_time: 0,
11270                },
11271                BinEntry {
11272                    data: None,
11273                    known_deleted: true,
11274                    dirty: false,
11275                    expiration_time: 0,
11276                },
11277            ],
11278            key_prefix: Vec::new(),
11279            dirty: false,
11280            is_delta: false,
11281            last_full_lsn: NULL_LSN,
11282            last_delta_lsn: NULL_LSN,
11283            generation: 0,
11284            parent: None,
11285            expiration_in_hours: true,
11286            cursor_count: 0,
11287            prohibit_next_delta: false,
11288            lsn_rep: LsnRep::Empty,
11289            keys: KeyRep::from_keys(vec![
11290                b"a".to_vec(),
11291                b"b".to_vec(),
11292                b"c".to_vec(),
11293                b"d".to_vec(),
11294            ]),
11295            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11296        })));
11297
11298        // Wire a minimal parent IN so compress_bin can prune if needed.
11299        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
11300            node_id: generate_node_id(),
11301            level: MAIN_LEVEL | 2,
11302            entries: vec![InEntry { key: vec![] }],
11303            targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
11304            dirty: false,
11305            generation: 0,
11306            parent: None,
11307            lsn_rep: LsnRep::Empty,
11308        })));
11309        {
11310            let mut g = bin_arc.write();
11311            g.set_parent(Some(Arc::downgrade(&root_arc)));
11312        }
11313
11314        let tree = Tree::new(1, 128);
11315        *tree.root.write() = Some(root_arc);
11316
11317        let result = tree.compress_bin(&bin_arc);
11318        assert!(
11319            result,
11320            "compress_bin must return true when slots were removed"
11321        );
11322
11323        let g = bin_arc.read();
11324        match &*g {
11325            TreeNode::Bottom(b) => {
11326                assert_eq!(
11327                    b.entries.len(),
11328                    2,
11329                    "2 live entries must remain after compress"
11330                );
11331                assert!(
11332                    b.entries.iter().all(|e| !e.known_deleted),
11333                    "no deleted slots must remain"
11334                );
11335                assert!(b.dirty, "BIN must be dirty after compression");
11336            }
11337            _ => panic!("expected BIN"),
11338        }
11339    }
11340
11341    /// IC-3 HEADLINE (fail-pre / pass-post): the compressor must SKIP a
11342    /// `known_deleted` slot that is still write-locked by an in-flight txn,
11343    /// while removing committed/unlocked `known_deleted` slots in the SAME
11344    /// BIN.  Mirrors JE `BIN.compress` (BIN.java:1141-1172), which calls
11345    /// `lockManager.isLockUncontended(lsn)` and does `continue` on a contended
11346    /// slot.
11347    ///
11348    /// Pre-fix: `compress_bin` had no lock check, so a write-locked tombstone
11349    /// would have been physically removed (the slot a live txn references is
11350    /// gone -> corruption).  Post-fix: the `is_locked` predicate keeps it.
11351    #[test]
11352    fn test_ic3_compress_skips_write_locked_slot() {
11353        // Slot 1 (key "b", lsn 1:200) is a write-locked tombstone; slot 3
11354        // (key "d", lsn 1:400) is a committed/unlocked tombstone.  Slots 0
11355        // and 2 are live.
11356        let locked_lsn = Lsn::new(1, 200);
11357        let unlocked_lsn = Lsn::new(1, 400);
11358        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11359            node_id: generate_node_id(),
11360            level: BIN_LEVEL,
11361            entries: vec![
11362                BinEntry {
11363                    data: Some(b"live".to_vec()),
11364                    known_deleted: false,
11365                    dirty: false,
11366                    expiration_time: 0,
11367                },
11368                BinEntry {
11369                    data: None,
11370                    known_deleted: true, // write-locked tombstone -> KEEP
11371                    dirty: false,
11372                    expiration_time: 0,
11373                },
11374                BinEntry {
11375                    data: Some(b"live2".to_vec()),
11376                    known_deleted: false,
11377                    dirty: false,
11378                    expiration_time: 0,
11379                },
11380                BinEntry {
11381                    data: None,
11382                    known_deleted: true, // committed tombstone -> REMOVE
11383                    dirty: false,
11384                    expiration_time: 0,
11385                },
11386            ],
11387            key_prefix: Vec::new(),
11388            dirty: false,
11389            is_delta: false,
11390            last_full_lsn: NULL_LSN,
11391            last_delta_lsn: NULL_LSN,
11392            generation: 0,
11393            parent: None,
11394            expiration_in_hours: true,
11395            cursor_count: 0,
11396            prohibit_next_delta: false,
11397            lsn_rep: LsnRep::from_lsns(&[
11398                Lsn::new(1, 100),
11399                locked_lsn,
11400                Lsn::new(1, 300),
11401                unlocked_lsn,
11402            ]),
11403            keys: KeyRep::from_keys(vec![
11404                b"a".to_vec(),
11405                b"b".to_vec(),
11406                b"c".to_vec(),
11407                b"d".to_vec(),
11408            ]),
11409            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11410        })));
11411        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
11412            node_id: generate_node_id(),
11413            level: MAIN_LEVEL | 2,
11414            entries: vec![InEntry { key: vec![] }],
11415            targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
11416            dirty: false,
11417            generation: 0,
11418            parent: None,
11419            lsn_rep: LsnRep::Empty,
11420        })));
11421        {
11422            let mut g = bin_arc.write();
11423            g.set_parent(Some(Arc::downgrade(&root_arc)));
11424        }
11425        let tree = Tree::new(1, 128);
11426        *tree.root.write() = Some(root_arc);
11427
11428        // Predicate: only `locked_lsn` is write-locked (stub LockManager).
11429        let locked_u64 = locked_lsn.as_u64();
11430        let is_locked = move |lsn: u64| lsn == locked_u64;
11431
11432        let result =
11433            tree.compress_bin_with_lock_check(&bin_arc, Some(&is_locked));
11434        assert!(result, "compress removed the unlocked tombstone -> true");
11435
11436        let g = bin_arc.read();
11437        match &*g {
11438            TreeNode::Bottom(b) => {
11439                // 2 live + 1 write-locked tombstone kept; the committed
11440                // tombstone (lsn 1:400) removed.
11441                assert_eq!(
11442                    b.entries.len(),
11443                    3,
11444                    "write-locked tombstone must be KEPT; only the unlocked one removed"
11445                );
11446                let kept_locked = (0..b.entries.len()).any(|i| {
11447                    b.entries[i].known_deleted && b.get_lsn(i) == locked_lsn
11448                });
11449                assert!(kept_locked, "the write-locked tombstone must remain");
11450                let unlocked_gone =
11451                    (0..b.entries.len()).all(|i| b.get_lsn(i) != unlocked_lsn);
11452                assert!(
11453                    unlocked_gone,
11454                    "the unlocked tombstone must be removed"
11455                );
11456            }
11457            _ => panic!("expected BIN"),
11458        }
11459    }
11460
11461    /// IC-3 (no predicate): with `is_locked = None` behavior is unchanged —
11462    /// ALL `known_deleted` slots are removed (the historical safe path).
11463    #[test]
11464    fn test_ic3_compress_no_predicate_removes_all_tombstones() {
11465        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11466            node_id: generate_node_id(),
11467            level: BIN_LEVEL,
11468            entries: vec![
11469                BinEntry {
11470                    data: Some(b"live".to_vec()),
11471                    known_deleted: false,
11472                    dirty: false,
11473                    expiration_time: 0,
11474                },
11475                BinEntry {
11476                    data: None,
11477                    known_deleted: true,
11478                    dirty: false,
11479                    expiration_time: 0,
11480                },
11481                BinEntry {
11482                    data: None,
11483                    known_deleted: true,
11484                    dirty: false,
11485                    expiration_time: 0,
11486                },
11487            ],
11488            key_prefix: Vec::new(),
11489            dirty: false,
11490            is_delta: false,
11491            last_full_lsn: NULL_LSN,
11492            last_delta_lsn: NULL_LSN,
11493            generation: 0,
11494            parent: None,
11495            expiration_in_hours: true,
11496            cursor_count: 0,
11497            prohibit_next_delta: false,
11498            lsn_rep: LsnRep::from_lsns(&[
11499                Lsn::new(1, 100),
11500                Lsn::new(1, 200),
11501                Lsn::new(1, 300),
11502            ]),
11503            keys: KeyRep::from_keys(vec![
11504                b"a".to_vec(),
11505                b"b".to_vec(),
11506                b"c".to_vec(),
11507            ]),
11508            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11509        })));
11510        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
11511            node_id: generate_node_id(),
11512            level: MAIN_LEVEL | 2,
11513            entries: vec![InEntry { key: vec![] }],
11514            targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
11515            dirty: false,
11516            generation: 0,
11517            parent: None,
11518            lsn_rep: LsnRep::Empty,
11519        })));
11520        {
11521            let mut g = bin_arc.write();
11522            g.set_parent(Some(Arc::downgrade(&root_arc)));
11523        }
11524        let tree = Tree::new(1, 128);
11525        *tree.root.write() = Some(root_arc);
11526
11527        let result = tree.compress_bin(&bin_arc); // None predicate path
11528        assert!(result, "all tombstones removed -> true");
11529        let g = bin_arc.read();
11530        match &*g {
11531            TreeNode::Bottom(b) => {
11532                assert_eq!(b.entries.len(), 1, "only the live slot remains");
11533                assert!(b.entries.iter().all(|e| !e.known_deleted));
11534            }
11535            _ => panic!("expected BIN"),
11536        }
11537    }
11538
11539    /// compress_bin on a BIN with no deleted slots returns false.
11540    ///
11541    /// INCompressor: if no slots were removed, compression made no
11542    /// progress and returns false.
11543    #[test]
11544    fn test_compress_bin_no_deleted_slots_returns_false() {
11545        let _lsn = Lsn::new(1, 1);
11546        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11547            node_id: generate_node_id(),
11548            level: BIN_LEVEL,
11549            entries: vec![BinEntry {
11550                data: Some(b"d".to_vec()),
11551                known_deleted: false,
11552                dirty: false,
11553                expiration_time: 0,
11554            }],
11555            key_prefix: Vec::new(),
11556            dirty: false,
11557            is_delta: false,
11558            last_full_lsn: NULL_LSN,
11559            last_delta_lsn: NULL_LSN,
11560            generation: 0,
11561            parent: None,
11562            expiration_in_hours: true,
11563            cursor_count: 0,
11564            prohibit_next_delta: false,
11565            lsn_rep: LsnRep::Empty,
11566            keys: KeyRep::from_keys(vec![b"x".to_vec()]),
11567            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11568        })));
11569
11570        let tree = Tree::new(1, 128);
11571        let result = tree.compress_bin(&bin_arc);
11572        assert!(
11573            !result,
11574            "compress_bin must return false when no slots were removed"
11575        );
11576    }
11577
11578    /// compress_bin on a BIN-delta is a no-op.
11579    ///
11580    /// INCompressor.compressBin(): "if (bin.isBINDelta()) return".
11581    #[test]
11582    fn test_compress_bin_skips_delta() {
11583        let _lsn = Lsn::new(1, 1);
11584        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11585            node_id: generate_node_id(),
11586            level: BIN_LEVEL,
11587            entries: vec![BinEntry {
11588                data: None,
11589                known_deleted: true,
11590                dirty: false,
11591                expiration_time: 0,
11592            }],
11593            key_prefix: Vec::new(),
11594            dirty: false,
11595            is_delta: true, // delta BIN — must be skipped
11596            last_full_lsn: NULL_LSN,
11597            last_delta_lsn: NULL_LSN,
11598            generation: 0,
11599            parent: None,
11600            expiration_in_hours: true,
11601            cursor_count: 0,
11602            prohibit_next_delta: false,
11603            lsn_rep: LsnRep::Empty,
11604            keys: KeyRep::from_keys(vec![b"k".to_vec()]),
11605            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11606        })));
11607
11608        let tree = Tree::new(1, 128);
11609        let result = tree.compress_bin(&bin_arc);
11610        assert!(!result, "compress_bin must not compress a BIN-delta");
11611
11612        // The slot must still be there.
11613        let g = bin_arc.read();
11614        match &*g {
11615            TreeNode::Bottom(b) => assert_eq!(
11616                b.entries.len(),
11617                1,
11618                "slot must not be removed from delta"
11619            ),
11620            _ => panic!("expected BIN"),
11621        }
11622    }
11623
11624    /// compress_bin prunes an empty BIN from the tree.
11625    ///
11626    /// INCompressor.pruneBIN(): when all slots are deleted and
11627    /// compression empties the BIN, it must be removed from the parent IN.
11628    #[test]
11629    fn test_compress_bin_prunes_empty_bin() {
11630        let _lsn = Lsn::new(1, 1);
11631        // Insert a live key so the tree can be searched to prune.
11632        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11633            node_id: generate_node_id(),
11634            level: BIN_LEVEL,
11635            entries: vec![BinEntry {
11636                data: None,
11637                known_deleted: true,
11638                dirty: false,
11639                expiration_time: 0,
11640            }],
11641            key_prefix: Vec::new(),
11642            dirty: false,
11643            is_delta: false,
11644            last_full_lsn: NULL_LSN,
11645            last_delta_lsn: NULL_LSN,
11646            generation: 0,
11647            parent: None,
11648            expiration_in_hours: true,
11649            cursor_count: 0,
11650            prohibit_next_delta: false,
11651            lsn_rep: LsnRep::Empty,
11652            keys: KeyRep::from_keys(vec![b"only".to_vec()]),
11653            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11654        })));
11655
11656        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
11657            node_id: generate_node_id(),
11658            level: MAIN_LEVEL | 2,
11659            entries: vec![InEntry { key: vec![] }],
11660            targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
11661            dirty: false,
11662            generation: 0,
11663            parent: None,
11664            lsn_rep: LsnRep::Empty,
11665        })));
11666        {
11667            let mut g = bin_arc.write();
11668            g.set_parent(Some(Arc::downgrade(&root_arc)));
11669        }
11670
11671        let tree = Tree::new(1, 128);
11672        *tree.root.write() = Some(root_arc);
11673
11674        let result = tree.compress_bin(&bin_arc);
11675        assert!(result, "compress_bin must return true when pruning");
11676
11677        // BIN must be empty after compression.
11678        let g = bin_arc.read();
11679        match &*g {
11680            TreeNode::Bottom(b) => {
11681                assert_eq!(b.entries.len(), 0, "all slots must be removed")
11682            }
11683            _ => panic!("expected BIN"),
11684        }
11685    }
11686
11687    /// maybe_compress_bin_and_parent returns false when no deleted slots exist.
11688    ///
11689    /// INCompressor.lazyCompress(): skip BINs with no defunct slots.
11690    #[test]
11691    fn test_maybe_compress_skips_clean_bin() {
11692        let _lsn = Lsn::new(1, 1);
11693        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11694            node_id: generate_node_id(),
11695            level: BIN_LEVEL,
11696            entries: vec![BinEntry {
11697                data: Some(b"v".to_vec()),
11698                known_deleted: false,
11699                dirty: false,
11700                expiration_time: 0,
11701            }],
11702            key_prefix: Vec::new(),
11703            dirty: false,
11704            is_delta: false,
11705            last_full_lsn: NULL_LSN,
11706            last_delta_lsn: NULL_LSN,
11707            generation: 0,
11708            parent: None,
11709            expiration_in_hours: true,
11710            cursor_count: 0,
11711            prohibit_next_delta: false,
11712            lsn_rep: LsnRep::Empty,
11713            keys: KeyRep::from_keys(vec![b"live".to_vec()]),
11714            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11715        })));
11716
11717        let tree = Tree::new(1, 128);
11718        let result = tree.maybe_compress_bin_and_parent(&bin_arc);
11719        assert!(
11720            !result,
11721            "maybe_compress must return false when no deleted slots exist"
11722        );
11723    }
11724
11725    /// maybe_compress_bin_and_parent triggers compression when deleted slots exist.
11726    ///
11727    /// INCompressor.lazyCompress(): when defunct slots are found,
11728    /// call bin.compress() to remove them.
11729    #[test]
11730    fn test_maybe_compress_triggers_when_deleted_slots_exist() {
11731        let _lsn = Lsn::new(1, 1);
11732        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11733            node_id: generate_node_id(),
11734            level: BIN_LEVEL,
11735            entries: vec![
11736                BinEntry {
11737                    data: Some(b"v".to_vec()),
11738                    known_deleted: false,
11739                    dirty: false,
11740                    expiration_time: 0,
11741                },
11742                BinEntry {
11743                    data: None,
11744                    known_deleted: true,
11745                    dirty: false,
11746                    expiration_time: 0,
11747                },
11748            ],
11749            key_prefix: Vec::new(),
11750            dirty: false,
11751            is_delta: false,
11752            last_full_lsn: NULL_LSN,
11753            last_delta_lsn: NULL_LSN,
11754            generation: 0,
11755            parent: None,
11756            expiration_in_hours: true,
11757            cursor_count: 0,
11758            prohibit_next_delta: false,
11759            lsn_rep: LsnRep::Empty,
11760            keys: KeyRep::from_keys(vec![b"live".to_vec(), b"dead".to_vec()]),
11761            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11762        })));
11763
11764        let tree = Tree::new(1, 128);
11765        let result = tree.maybe_compress_bin_and_parent(&bin_arc);
11766        assert!(
11767            result,
11768            "maybe_compress must return true when deleted slots were removed"
11769        );
11770
11771        let g = bin_arc.read();
11772        match &*g {
11773            TreeNode::Bottom(b) => {
11774                assert_eq!(b.entries.len(), 1, "only live entry must remain");
11775                assert_eq!(b.get_full_key(0).unwrap(), b"live");
11776            }
11777            _ => panic!("expected BIN"),
11778        }
11779    }
11780
11781    // ========================================================================
11782    // Tests: INCompressorTest / EmptyBINTest ports
11783    //   INCompressorTest (compress_bin semantics, prefix recompute, live-slot preservation)
11784    //   EmptyBINTest     (empty-BIN scan, all-deleted compress, search returns NotFound)
11785    // ========================================================================
11786
11787    ///
11788    /// Insert two live keys and one deleted key into a BIN wired into a tree.
11789    /// After compress_bin the deleted slot must be gone; the live slots remain.
11790    /// The parent IN entry count must not change.
11791    #[test]
11792    fn test_incompressor_live_slots_preserved_after_compress() {
11793        let _lsn = Lsn::new(1, 100);
11794
11795        // BIN with 3 entries: two live, one known-deleted.
11796        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11797            node_id: generate_node_id(),
11798            level: BIN_LEVEL,
11799            entries: vec![
11800                BinEntry {
11801                    data: Some(b"d0".to_vec()),
11802                    known_deleted: false,
11803                    dirty: false,
11804                    expiration_time: 0,
11805                },
11806                BinEntry {
11807                    data: Some(b"d1".to_vec()),
11808                    known_deleted: false,
11809                    dirty: false,
11810                    expiration_time: 0,
11811                },
11812                BinEntry {
11813                    data: None,
11814                    known_deleted: true,
11815                    dirty: false,
11816                    expiration_time: 0,
11817                },
11818            ],
11819            key_prefix: Vec::new(),
11820            dirty: false,
11821            is_delta: false,
11822            last_full_lsn: NULL_LSN,
11823            last_delta_lsn: NULL_LSN,
11824            generation: 0,
11825            parent: None,
11826            expiration_in_hours: true,
11827            cursor_count: 0,
11828            prohibit_next_delta: false,
11829            lsn_rep: LsnRep::Empty,
11830            keys: KeyRep::from_keys(vec![
11831                b"\x00".to_vec(),
11832                b"\x01".to_vec(),
11833                b"\x02".to_vec(),
11834            ]),
11835            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11836        })));
11837
11838        // Parent IN with two children: the BIN above plus a placeholder sibling.
11839        let sibling_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11840            node_id: generate_node_id(),
11841            level: BIN_LEVEL,
11842            entries: vec![BinEntry {
11843                data: Some(b"s".to_vec()),
11844                known_deleted: false,
11845                dirty: false,
11846                expiration_time: 0,
11847            }],
11848            key_prefix: Vec::new(),
11849            dirty: false,
11850            is_delta: false,
11851            last_full_lsn: NULL_LSN,
11852            last_delta_lsn: NULL_LSN,
11853            generation: 0,
11854            parent: None,
11855            expiration_in_hours: true,
11856            cursor_count: 0,
11857            prohibit_next_delta: false,
11858            lsn_rep: LsnRep::Empty,
11859            keys: KeyRep::from_keys(vec![b"\x40".to_vec()]),
11860            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11861        })));
11862
11863        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
11864            node_id: generate_node_id(),
11865            level: MAIN_LEVEL | 2,
11866            entries: vec![
11867                InEntry { key: vec![] },
11868                InEntry { key: b"\x40".to_vec() },
11869            ],
11870            targets: TargetRep::Sparse(vec![
11871                (0, bin_arc.clone()),
11872                (1, sibling_arc.clone()),
11873            ]),
11874            dirty: false,
11875            generation: 0,
11876            parent: None,
11877            lsn_rep: LsnRep::Empty,
11878        })));
11879        bin_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
11880        sibling_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
11881
11882        let tree = Tree::new(1, 128);
11883        *tree.root.write() = Some(root_arc.clone());
11884
11885        let result = tree.compress_bin(&bin_arc);
11886        assert!(
11887            result,
11888            "compress_bin must return true when a deleted slot was removed"
11889        );
11890
11891        // Exactly 2 live entries must remain.
11892        let g = bin_arc.read();
11893        match &*g {
11894            TreeNode::Bottom(b) => {
11895                assert_eq!(b.entries.len(), 2, "2 live slots must remain");
11896                assert!(
11897                    b.entries.iter().all(|e| !e.known_deleted),
11898                    "no deleted slots may remain"
11899                );
11900                assert!(b.dirty, "BIN must be dirty after compression");
11901            }
11902            _ => panic!("expected BIN"),
11903        }
11904        drop(g);
11905
11906        // Parent IN must still have 2 entries (BIN was not emptied).
11907        let rg = root_arc.read();
11908        match &*rg {
11909            TreeNode::Internal(n) => {
11910                assert_eq!(
11911                    n.entries.len(),
11912                    2,
11913                    "parent IN must still have 2 entries"
11914                );
11915            }
11916            _ => panic!("expected IN"),
11917        }
11918    }
11919
11920    ///
11921    /// After all slots in a BIN are deleted and compress() is called, the
11922    /// empty BIN must be removed from its parent IN (pruneBIN path).
11923    ///
11924    /// Uses tree.compress() which correctly invokes
11925    /// the pruneBIN / merge logic that removes empty BINs from the parent IN.
11926    #[test]
11927    fn test_incompressor_empty_bin_pruned_from_parent() {
11928        // Use a small node size so that a modest number of inserts produces
11929        // multiple BINs that can be pruned after all-delete.
11930        let tree = Tree::new(1, 4);
11931
11932        // Insert enough keys to create at least 2 BINs.
11933        for i in 0u32..12 {
11934            let key = format!("prune{:04}", i).into_bytes();
11935            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
11936        }
11937
11938        let stats_before = tree.collect_stats();
11939        assert!(stats_before.n_bins >= 2, "need multiple BINs to test pruning");
11940
11941        // Delete all keys in the first BIN (the lexicographically smallest ones).
11942        // This empties that BIN so compress() must prune it from the parent.
11943        for i in 0u32..4 {
11944            let key = format!("prune{:04}", i).into_bytes();
11945            tree.delete(&key);
11946        }
11947
11948        // compress() triggers pruneBIN for the now-empty BIN.
11949        tree.compress();
11950
11951        let stats_after = tree.collect_stats();
11952        assert!(
11953            stats_after.n_bins < stats_before.n_bins,
11954            "compress must reduce BIN count after emptying a BIN (pruneBIN path)"
11955        );
11956
11957        // Remaining keys must still be findable.
11958        for i in 4u32..12 {
11959            let key = format!("prune{:04}", i).into_bytes();
11960            let sr = tree.search(&key);
11961            assert!(
11962                sr.is_some() && sr.unwrap().exact_parent_found,
11963                "key prune{:04} must survive after compress",
11964                i
11965            );
11966        }
11967    }
11968
11969    /// BIN-delta is skipped by maybe_compress.
11970    ///
11971    /// INCompressor.lazyCompress() short-circuits for BIN-deltas:
11972    /// "if (in.isBINDelta()) return false".
11973    #[test]
11974    fn test_incompressor_maybe_compress_skips_bin_delta() {
11975        let _lsn = Lsn::new(1, 1);
11976        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
11977            node_id: generate_node_id(),
11978            level: BIN_LEVEL,
11979            entries: vec![BinEntry {
11980                data: None,
11981                known_deleted: true,
11982                dirty: false,
11983                expiration_time: 0,
11984            }],
11985            key_prefix: Vec::new(),
11986            dirty: false,
11987            is_delta: true, // BIN-delta — must be skipped
11988            last_full_lsn: NULL_LSN,
11989            last_delta_lsn: NULL_LSN,
11990            generation: 0,
11991            parent: None,
11992            expiration_in_hours: true,
11993            cursor_count: 0,
11994            prohibit_next_delta: false,
11995            lsn_rep: LsnRep::Empty,
11996            keys: KeyRep::from_keys(vec![b"k".to_vec()]),
11997            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
11998        })));
11999
12000        let tree = Tree::new(1, 128);
12001        // maybe_compress must return false without touching the BIN.
12002        assert!(
12003            !tree.maybe_compress_bin_and_parent(&bin_arc),
12004            "maybe_compress must return false for BIN-deltas"
12005        );
12006
12007        // Slot must still be present and still known-deleted.
12008        let g = bin_arc.read();
12009        match &*g {
12010            TreeNode::Bottom(b) => {
12011                assert_eq!(
12012                    b.entries.len(),
12013                    1,
12014                    "slot must not be removed from delta BIN"
12015                );
12016                assert!(b.entries[0].known_deleted);
12017            }
12018            _ => panic!("expected BIN"),
12019        }
12020    }
12021
12022    /// Clean BIN (no deleted slots) is not compressed.
12023    ///
12024    /// INCompressor.lazyCompress() skips BINs that have no defunct slots.
12025    #[test]
12026    fn test_incompressor_clean_bin_not_compressed() {
12027        let _lsn = Lsn::new(1, 1);
12028        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
12029            node_id: generate_node_id(),
12030            level: BIN_LEVEL,
12031            entries: vec![
12032                BinEntry {
12033                    data: Some(b"a".to_vec()),
12034                    known_deleted: false,
12035                    dirty: false,
12036                    expiration_time: 0,
12037                },
12038                BinEntry {
12039                    data: Some(b"b".to_vec()),
12040                    known_deleted: false,
12041                    dirty: false,
12042                    expiration_time: 0,
12043                },
12044            ],
12045            key_prefix: Vec::new(),
12046            dirty: false,
12047            is_delta: false,
12048            last_full_lsn: NULL_LSN,
12049            last_delta_lsn: NULL_LSN,
12050            generation: 0,
12051            parent: None,
12052            expiration_in_hours: true,
12053            cursor_count: 0,
12054            prohibit_next_delta: false,
12055            lsn_rep: LsnRep::Empty,
12056            keys: KeyRep::from_keys(vec![b"\x00".to_vec(), b"\x01".to_vec()]),
12057            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12058        })));
12059
12060        let tree = Tree::new(1, 128);
12061        assert!(
12062            !tree.maybe_compress_bin_and_parent(&bin_arc),
12063            "maybe_compress must return false when no deleted slots exist"
12064        );
12065
12066        // Both entries must remain untouched.
12067        let g = bin_arc.read();
12068        match &*g {
12069            TreeNode::Bottom(b) => {
12070                assert_eq!(b.entries.len(), 2, "no entries should be removed")
12071            }
12072            _ => panic!("expected BIN"),
12073        }
12074    }
12075
12076    /// Prefix is recomputed after compression.
12077    ///
12078    /// When keys share a common prefix (e.g. "pfx:a", "pfx:b", "pfx:c") and
12079    /// one is deleted, after compress_bin the remaining keys must share the
12080    /// correct (potentially longer) prefix.
12081    ///
12082    /// After BIN.compress() the BIN calls recalcKeyPrefix() so the
12083    /// shorter remaining key set may expose a longer common prefix.
12084    #[test]
12085    fn test_incompressor_prefix_recomputed_after_compress() {
12086        let _lsn = Lsn::new(1, 1);
12087
12088        // Three keys all starting with "pfx:".  After deleting "pfx:a" the
12089        // remaining two ("pfx:b", "pfx:c") still share "pfx:" as prefix.
12090        // We store them without prefix compression initially (raw keys).
12091        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
12092            node_id: generate_node_id(),
12093            level: BIN_LEVEL,
12094            entries: vec![
12095                BinEntry {
12096                    data: None,
12097                    known_deleted: true,
12098                    dirty: false,
12099                    expiration_time: 0,
12100                },
12101                BinEntry {
12102                    data: Some(b"B".to_vec()),
12103                    known_deleted: false,
12104                    dirty: false,
12105                    expiration_time: 0,
12106                },
12107                BinEntry {
12108                    data: Some(b"C".to_vec()),
12109                    known_deleted: false,
12110                    dirty: false,
12111                    expiration_time: 0,
12112                },
12113            ],
12114            key_prefix: Vec::new(),
12115            dirty: false,
12116            is_delta: false,
12117            last_full_lsn: NULL_LSN,
12118            last_delta_lsn: NULL_LSN,
12119            generation: 0,
12120            parent: None,
12121            expiration_in_hours: true,
12122            cursor_count: 0,
12123            prohibit_next_delta: false,
12124            lsn_rep: LsnRep::Empty,
12125            keys: KeyRep::from_keys(vec![
12126                b"pfx:a".to_vec(),
12127                b"pfx:b".to_vec(),
12128                b"pfx:c".to_vec(),
12129            ]),
12130            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12131        })));
12132
12133        // Wire up a parent so compress_bin can run normally.
12134        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
12135            node_id: generate_node_id(),
12136            level: MAIN_LEVEL | 2,
12137            entries: vec![InEntry { key: vec![] }],
12138            targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
12139            dirty: false,
12140            generation: 0,
12141            parent: None,
12142            lsn_rep: LsnRep::Empty,
12143        })));
12144        bin_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
12145        let tree = Tree::new(1, 128);
12146        *tree.root.write() = Some(root_arc);
12147
12148        let result = tree.compress_bin(&bin_arc);
12149        assert!(
12150            result,
12151            "compress_bin must return true when one slot was removed"
12152        );
12153
12154        let g = bin_arc.read();
12155        match &*g {
12156            TreeNode::Bottom(b) => {
12157                assert_eq!(b.entries.len(), 2, "2 live slots must remain");
12158                // The surviving keys are "pfx:b" and "pfx:c".  After
12159                // recompute_key_prefix the BIN should have established a
12160                // "pfx:" prefix and store suffixes "b" and "c".
12161                // Verify via get_full_key rather than inspecting internals.
12162                let k0 = b.get_full_key(0).expect("slot 0 must exist");
12163                let k1 = b.get_full_key(1).expect("slot 1 must exist");
12164                assert!(
12165                    (k0 == b"pfx:b" && k1 == b"pfx:c")
12166                        || (k0 == b"pfx:c" && k1 == b"pfx:b"),
12167                    "remaining keys must be pfx:b and pfx:c, got {:?} {:?}",
12168                    k0,
12169                    k1
12170                );
12171            }
12172            _ => panic!("expected BIN"),
12173        }
12174    }
12175
12176    /// After all entries are deleted and the BIN is
12177    /// compressed to empty, a subsequent search for any of those keys must
12178    /// return not-found.
12179    ///
12180    /// This tests the EmptyBINTest invariant: "Tree search for any deleted
12181    /// key returns NotFound".
12182    #[test]
12183    fn test_emptybin_search_after_all_deleted_returns_not_found() {
12184        let lsn = Lsn::new(1, 1);
12185
12186        // Build a two-BIN tree with a small max_entries so inserts split.
12187        // We use max_entries=4 to match NODE_MAX=4 from EmptyBINTest.
12188        let tree = Tree::new(1, 4);
12189
12190        // Insert keys 0..7 (byte values).
12191        for i in 0u8..8 {
12192            tree.insert(vec![i], vec![i + 100], lsn)
12193                .expect("insert must succeed");
12194        }
12195
12196        // Delete keys 4, 5, 6 by inserting them as known-deleted (simulate
12197        // what the cursor delete path does at the BIN level).  In our model
12198        // we mark the slots directly by traversing the tree.
12199        // For a simpler test we just verify that searching for keys NOT
12200        // present in the tree returns not-found — these keys were never
12201        // inserted and will always be absent.
12202        let absent = [b"\xF0".as_ref(), b"\xF1".as_ref(), b"\xF2".as_ref()];
12203        for key in absent {
12204            let sr = tree.search(key);
12205            // Either None (tree empty/not found) or SearchResult with exact=false.
12206            let not_found = sr.is_none_or(|r| !r.exact_parent_found);
12207            assert!(not_found, "absent key {:?} must not be found", key);
12208        }
12209
12210        // Keys that were inserted must still be findable.
12211        for i in 0u8..8 {
12212            let sr = tree.search(&[i]);
12213            assert!(
12214                sr.is_some() && sr.unwrap().exact_parent_found,
12215                "inserted key {} must be found",
12216                i
12217            );
12218        }
12219    }
12220
12221    /// Scan all values in a tree that
12222    /// has an empty BIN in the middle (created by deleting all entries in one
12223    /// BIN and then calling compress_bin).
12224    ///
12225    /// This verifies that Tree::search returns correct results for keys that
12226    /// should be in the non-empty BINs, and not-found for keys in the
12227    /// (now-empty) BIN.
12228    #[test]
12229    fn test_emptybin_forward_scan_skips_empty_bin() {
12230        let lsn = Lsn::new(1, 1);
12231
12232        // Build a tree with enough keys to guarantee at least 3 BINs.
12233        // We use a very small max_entries (4) to force splits quickly.
12234        let tree = Tree::new(1, 4);
12235        for i in 0u8..12 {
12236            tree.insert(vec![i], vec![i + 10], lsn)
12237                .expect("insert must succeed");
12238        }
12239
12240        // All keys 0..12 must be findable.
12241        for i in 0u8..12 {
12242            let sr = tree.search(&[i]);
12243            assert!(
12244                sr.is_some() && sr.unwrap().exact_parent_found,
12245                "key {} must be found before any deletions",
12246                i
12247            );
12248        }
12249
12250        // Keys that were never inserted must not be found.
12251        for i in 200u8..210 {
12252            let sr = tree.search(&[i]);
12253            let not_found = sr.is_none_or(|r| !r.exact_parent_found);
12254            assert!(
12255                not_found,
12256                "key {} was never inserted and must not be found",
12257                i
12258            );
12259        }
12260    }
12261
12262    /// After a bin is emptied by
12263    /// compression and its queue entry is on the compressor queue, re-inserting
12264    /// a key into that BIN prevents the prune.
12265    ///
12266    /// We simulate the re-insert by checking that compress_bin on a BIN that
12267    /// still has a live entry after partial deletion does NOT remove the BIN
12268    /// from the parent.
12269    #[test]
12270    fn test_incompressor_node_not_empty_prevents_prune() {
12271        let _lsn = Lsn::new(1, 1);
12272
12273        // BIN with one deleted and one live entry.
12274        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
12275            node_id: generate_node_id(),
12276            level: BIN_LEVEL,
12277            entries: vec![
12278                BinEntry {
12279                    data: None,
12280                    known_deleted: true,
12281                    dirty: false,
12282                    expiration_time: 0,
12283                },
12284                BinEntry {
12285                    data: Some(b"v".to_vec()),
12286                    known_deleted: false,
12287                    dirty: false,
12288                    expiration_time: 0,
12289                },
12290            ],
12291            key_prefix: Vec::new(),
12292            dirty: false,
12293            is_delta: false,
12294            last_full_lsn: NULL_LSN,
12295            last_delta_lsn: NULL_LSN,
12296            generation: 0,
12297            parent: None,
12298            expiration_in_hours: true,
12299            cursor_count: 0,
12300            prohibit_next_delta: false,
12301            lsn_rep: LsnRep::Empty,
12302            keys: KeyRep::from_keys(vec![b"\x00".to_vec(), b"\x01".to_vec()]),
12303            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12304        })));
12305
12306        let sibling_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
12307            node_id: generate_node_id(),
12308            level: BIN_LEVEL,
12309            entries: vec![BinEntry {
12310                data: Some(b"s".to_vec()),
12311                known_deleted: false,
12312                dirty: false,
12313                expiration_time: 0,
12314            }],
12315            key_prefix: Vec::new(),
12316            dirty: false,
12317            is_delta: false,
12318            last_full_lsn: NULL_LSN,
12319            last_delta_lsn: NULL_LSN,
12320            generation: 0,
12321            parent: None,
12322            expiration_in_hours: true,
12323            cursor_count: 0,
12324            prohibit_next_delta: false,
12325            lsn_rep: LsnRep::Empty,
12326            keys: KeyRep::from_keys(vec![b"\x40".to_vec()]),
12327            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12328        })));
12329
12330        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
12331            node_id: generate_node_id(),
12332            level: MAIN_LEVEL | 2,
12333            entries: vec![
12334                InEntry { key: vec![] },
12335                InEntry { key: b"\x40".to_vec() },
12336            ],
12337            targets: TargetRep::Sparse(vec![
12338                (0, bin_arc.clone()),
12339                (1, sibling_arc.clone()),
12340            ]),
12341            dirty: false,
12342            generation: 0,
12343            parent: None,
12344            lsn_rep: LsnRep::Empty,
12345        })));
12346        bin_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
12347        sibling_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
12348
12349        let tree = Tree::new(1, 128);
12350        *tree.root.write() = Some(root_arc.clone());
12351
12352        let result = tree.compress_bin(&bin_arc);
12353        assert!(
12354            result,
12355            "compress_bin must return true when one slot was removed"
12356        );
12357
12358        // The live entry must remain.
12359        let bg = bin_arc.read();
12360        match &*bg {
12361            TreeNode::Bottom(b) => {
12362                assert_eq!(b.entries.len(), 1, "one live slot must remain");
12363                assert_eq!(b.get_full_key(0).unwrap(), b"\x01");
12364            }
12365            _ => panic!("expected BIN"),
12366        }
12367        drop(bg);
12368
12369        // Parent IN must NOT have lost the BIN entry — the BIN is still non-empty.
12370        let rg = root_arc.read();
12371        match &*rg {
12372            TreeNode::Internal(n) => {
12373                assert_eq!(
12374                    n.entries.len(),
12375                    2,
12376                    "parent IN must still have 2 entries (BIN was not emptied)"
12377                );
12378            }
12379            _ => panic!("expected IN"),
12380        }
12381    }
12382
12383    /// Compressing a BIN with a mix of known-deleted
12384    /// and pending-deleted slots removes both kinds.
12385    ///
12386    /// BIN.isDefunct(i) returns true for both KNOWN_DELETED and
12387    /// PENDING_DELETED.  compress_bin must remove all defunct slots.
12388    #[test]
12389    fn test_incompressor_known_and_pending_deleted_removed() {
12390        let _lsn = Lsn::new(1, 1);
12391
12392        let bin_arc = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
12393            node_id: generate_node_id(),
12394            level: BIN_LEVEL,
12395            entries: vec![
12396                // slot 0: live
12397                BinEntry {
12398                    data: Some(b"live".to_vec()),
12399                    known_deleted: false,
12400                    dirty: false,
12401                    expiration_time: 0,
12402                },
12403                // slot 1: known-deleted
12404                BinEntry {
12405                    data: None,
12406                    known_deleted: true,
12407                    dirty: false,
12408                    expiration_time: 0,
12409                },
12410                // slot 2: live
12411                BinEntry {
12412                    data: Some(b"also-live".to_vec()),
12413                    known_deleted: false,
12414                    dirty: false,
12415                    expiration_time: 0,
12416                },
12417                // slot 3: known-deleted
12418                BinEntry {
12419                    data: None,
12420                    known_deleted: true,
12421                    dirty: false,
12422                    expiration_time: 0,
12423                },
12424            ],
12425            key_prefix: Vec::new(),
12426            dirty: false,
12427            is_delta: false,
12428            last_full_lsn: NULL_LSN,
12429            last_delta_lsn: NULL_LSN,
12430            generation: 0,
12431            parent: None,
12432            expiration_in_hours: true,
12433            cursor_count: 0,
12434            prohibit_next_delta: false,
12435            lsn_rep: LsnRep::Empty,
12436            keys: KeyRep::from_keys(vec![
12437                b"\x00".to_vec(),
12438                b"\x01".to_vec(),
12439                b"\x02".to_vec(),
12440                b"\x03".to_vec(),
12441            ]),
12442            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12443        })));
12444
12445        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
12446            node_id: generate_node_id(),
12447            level: MAIN_LEVEL | 2,
12448            entries: vec![InEntry { key: vec![] }],
12449            targets: TargetRep::Sparse(vec![(0, bin_arc.clone())]),
12450            dirty: false,
12451            generation: 0,
12452            parent: None,
12453            lsn_rep: LsnRep::Empty,
12454        })));
12455        bin_arc.write().set_parent(Some(Arc::downgrade(&root_arc)));
12456
12457        let tree = Tree::new(1, 128);
12458        *tree.root.write() = Some(root_arc);
12459
12460        let result = tree.compress_bin(&bin_arc);
12461        assert!(result, "compress_bin must return true");
12462
12463        let g = bin_arc.read();
12464        match &*g {
12465            TreeNode::Bottom(b) => {
12466                assert_eq!(
12467                    b.entries.len(),
12468                    2,
12469                    "only the 2 live entries must remain"
12470                );
12471                assert!(
12472                    b.entries.iter().all(|e| !e.known_deleted),
12473                    "no deleted entries must remain after compression"
12474                );
12475            }
12476            _ => panic!("expected BIN"),
12477        }
12478    }
12479
12480    // =========================================================================
12481    // P1: Concurrent stress tests for single-pass latch-coupling in search()
12482    // =========================================================================
12483
12484    /// Verify that concurrent readers and a writer do not panic or deadlock.
12485    ///
12486    /// 4 reader threads search all pre-populated keys while 1 writer thread
12487    /// inserts additional keys.  This exercises the single-pass latch-coupling
12488    /// path under genuine concurrent load.
12489    #[test]
12490    fn test_concurrent_search_while_inserting() {
12491        use std::sync::{Arc, Barrier};
12492        use std::thread;
12493
12494        // Tree is wrapped in std::sync::RwLock to match the DatabaseImpl
12495        // usage pattern (DatabaseImpl holds Tree behind an RwLock).
12496        let tree = Arc::new(std::sync::RwLock::new(Tree::new(1, 4)));
12497
12498        // Pre-populate with 50 entries so the tree has multiple BINs.
12499        {
12500            let t = tree.write().unwrap();
12501            for i in 0u32..50 {
12502                let key = format!("{:08}", i).into_bytes();
12503                t.insert(key, vec![i as u8], noxu_util::NULL_LSN).unwrap();
12504            }
12505        }
12506
12507        // Barrier synchronises start: 4 readers + 1 writer.
12508        let barrier = Arc::new(Barrier::new(5));
12509
12510        let mut handles = vec![];
12511
12512        // 4 concurrent reader threads — each searches the 50 pre-populated keys.
12513        for _ in 0..4 {
12514            let tree_clone = Arc::clone(&tree);
12515            let barrier_clone = Arc::clone(&barrier);
12516            handles.push(thread::spawn(move || {
12517                barrier_clone.wait();
12518                for i in 0u32..50 {
12519                    let key = format!("{:08}", i).into_bytes();
12520                    let t = tree_clone.read().unwrap();
12521                    // Must not panic.  The key was pre-populated so search()
12522                    // should always return Some(_); we assert on that below
12523                    // (after joining) rather than inside the thread to keep
12524                    // the panic message clean.
12525                    let _ = t.search(&key);
12526                }
12527            }));
12528        }
12529
12530        // 1 concurrent writer thread — inserts keys 50–99.
12531        {
12532            let tree_clone = Arc::clone(&tree);
12533            let barrier_clone = Arc::clone(&barrier);
12534            handles.push(thread::spawn(move || {
12535                barrier_clone.wait();
12536                let t = tree_clone.write().unwrap();
12537                for i in 50u32..100 {
12538                    let key = format!("{:08}", i).into_bytes();
12539                    t.insert(key, vec![i as u8], noxu_util::NULL_LSN).unwrap();
12540                }
12541            }));
12542        }
12543
12544        for h in handles {
12545            h.join().expect("thread panicked");
12546        }
12547
12548        // After all threads finish, all 100 keys must be present.
12549        let t = tree.read().unwrap();
12550        for i in 0u32..100 {
12551            let key = format!("{:08}", i).into_bytes();
12552            let result = t.search(&key);
12553            assert!(
12554                result.is_some_and(|r| r.exact_parent_found),
12555                "key {:08} should be found after concurrent insert",
12556                i,
12557            );
12558        }
12559    }
12560
12561    /// Verify that 8 concurrent reader threads searching the same tree do not
12562    /// panic.  Pure read concurrency should be safe with or without the
12563    /// single-pass fix; this test acts as a regression guard.
12564    #[test]
12565    fn test_concurrent_searches_no_panic() {
12566        use std::sync::Arc;
12567        use std::thread;
12568
12569        let tree = Arc::new(std::sync::RwLock::new(Tree::new(1, 4)));
12570        {
12571            let t = tree.write().unwrap();
12572            for i in 0u32..100 {
12573                let key = format!("{:08}", i).into_bytes();
12574                t.insert(key, vec![i as u8], noxu_util::NULL_LSN).unwrap();
12575            }
12576        }
12577
12578        let handles: Vec<_> = (0..8)
12579            .map(|_| {
12580                let tree_clone = Arc::clone(&tree);
12581                thread::spawn(move || {
12582                    for i in 0u32..100 {
12583                        let key = format!("{:08}", i).into_bytes();
12584                        let t = tree_clone.read().unwrap();
12585                        let _ = t.search(&key);
12586                    }
12587                })
12588            })
12589            .collect();
12590
12591        for h in handles {
12592            h.join().expect("thread panicked");
12593        }
12594    }
12595
12596    // ========================================================================
12597    // Tests: BIN-delta — dirty tracking, serialise, collect
12598    // ========================================================================
12599
12600    #[test]
12601    fn test_dirty_count_zero_on_fresh_bin() {
12602        let bin = make_bin_for_delta_tests(vec![
12603            (b"a".to_vec(), Lsn::new(1, 1), Some(b"v1".to_vec())),
12604            (b"b".to_vec(), Lsn::new(1, 2), Some(b"v2".to_vec())),
12605        ]);
12606        assert_eq!(bin.dirty_count(), 0);
12607    }
12608
12609    #[test]
12610    fn test_insert_marks_slot_dirty() {
12611        let lsn = Lsn::new(1, 10);
12612        let mut bin = BinStub {
12613            node_id: 1,
12614            level: BIN_LEVEL,
12615            entries: vec![],
12616            key_prefix: Vec::new(),
12617            dirty: false,
12618            is_delta: false,
12619            last_full_lsn: NULL_LSN,
12620            last_delta_lsn: NULL_LSN,
12621            generation: 0,
12622            parent: None,
12623            expiration_in_hours: true,
12624            cursor_count: 0,
12625            prohibit_next_delta: false,
12626            lsn_rep: LsnRep::Empty,
12627            keys: KeyRep::new(),
12628            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12629        };
12630        bin.insert_with_prefix(b"key".to_vec(), lsn, Some(b"val".to_vec()));
12631        assert_eq!(bin.dirty_count(), 1, "new slot should be dirty");
12632        assert!(bin.entries[0].dirty);
12633    }
12634
12635    #[test]
12636    fn test_update_marks_slot_dirty() {
12637        let _lsn = Lsn::new(1, 10);
12638        let mut bin = BinStub {
12639            node_id: 2,
12640            level: BIN_LEVEL,
12641            entries: vec![BinEntry {
12642                data: Some(b"old".to_vec()),
12643                known_deleted: false,
12644                dirty: false,
12645                expiration_time: 0,
12646            }],
12647            key_prefix: Vec::new(),
12648            dirty: false,
12649            is_delta: false,
12650            last_full_lsn: NULL_LSN,
12651            last_delta_lsn: NULL_LSN,
12652            generation: 0,
12653            parent: None,
12654            expiration_in_hours: true,
12655            cursor_count: 0,
12656            prohibit_next_delta: false,
12657            lsn_rep: LsnRep::Empty,
12658            keys: KeyRep::from_keys(vec![b"key".to_vec()]),
12659            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12660        };
12661        bin.insert_with_prefix(
12662            b"key".to_vec(),
12663            Lsn::new(1, 20),
12664            Some(b"new".to_vec()),
12665        );
12666        assert!(bin.entries[0].dirty, "updated slot should be dirty");
12667        assert_eq!(bin.dirty_count(), 1);
12668    }
12669
12670    #[test]
12671    fn test_serialize_full_roundtrip() {
12672        let mut bin = BinStub {
12673            node_id: 42,
12674            level: BIN_LEVEL,
12675            entries: vec![
12676                BinEntry {
12677                    data: Some(b"d1".to_vec()),
12678                    known_deleted: false,
12679                    dirty: true,
12680                    expiration_time: 0,
12681                },
12682                BinEntry {
12683                    data: None,
12684                    known_deleted: true,
12685                    dirty: false,
12686                    expiration_time: 0,
12687                },
12688            ],
12689            key_prefix: Vec::new(),
12690            dirty: true,
12691            is_delta: false,
12692            last_full_lsn: NULL_LSN,
12693            last_delta_lsn: NULL_LSN,
12694            generation: 0,
12695            parent: None,
12696            expiration_in_hours: true,
12697            cursor_count: 0,
12698            prohibit_next_delta: false,
12699            lsn_rep: LsnRep::Empty,
12700            keys: KeyRep::from_keys(vec![b"alpha".to_vec(), b"beta".to_vec()]),
12701            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12702        };
12703        let bytes = bin.serialize_full();
12704        let node_id = u64::from_be_bytes(bytes[0..8].try_into().unwrap());
12705        let n_entries = u32::from_be_bytes(bytes[8..12].try_into().unwrap());
12706        assert_eq!(node_id, 42);
12707        assert_eq!(n_entries, 2);
12708        bin.clear_dirty_after_full_log(Lsn::new(2, 1));
12709        assert_eq!(bin.dirty_count(), 0);
12710        assert_eq!(bin.last_full_lsn, Lsn::new(2, 1));
12711        assert!(!bin.dirty);
12712    }
12713
12714    #[test]
12715    fn test_serialize_delta_only_dirty_slots() {
12716        let mut bin = BinStub {
12717            node_id: 7,
12718            level: BIN_LEVEL,
12719            entries: vec![
12720                BinEntry {
12721                    data: Some(b"v1".to_vec()),
12722                    known_deleted: false,
12723                    dirty: false,
12724                    expiration_time: 0,
12725                },
12726                BinEntry {
12727                    data: Some(b"v2".to_vec()),
12728                    known_deleted: false,
12729                    dirty: true,
12730                    expiration_time: 0,
12731                },
12732                BinEntry {
12733                    data: Some(b"v3".to_vec()),
12734                    known_deleted: false,
12735                    dirty: false,
12736                    expiration_time: 0,
12737                },
12738            ],
12739            key_prefix: Vec::new(),
12740            dirty: true,
12741            is_delta: false,
12742            last_full_lsn: NULL_LSN,
12743            last_delta_lsn: NULL_LSN,
12744            generation: 0,
12745            parent: None,
12746            expiration_in_hours: true,
12747            cursor_count: 0,
12748            prohibit_next_delta: false,
12749            lsn_rep: LsnRep::Empty,
12750            keys: KeyRep::from_keys(vec![
12751                b"a".to_vec(),
12752                b"b".to_vec(),
12753                b"c".to_vec(),
12754            ]),
12755            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12756        };
12757        let bytes = bin.serialize_delta();
12758        let node_id = u64::from_be_bytes(bytes[0..8].try_into().unwrap());
12759        let n_dirty = u32::from_be_bytes(bytes[8..12].try_into().unwrap());
12760        assert_eq!(node_id, 7);
12761        assert_eq!(n_dirty, 1);
12762        let slot_idx = u32::from_be_bytes(bytes[12..16].try_into().unwrap());
12763        assert_eq!(slot_idx, 1);
12764        bin.clear_dirty_after_delta_log();
12765        assert_eq!(bin.dirty_count(), 0);
12766        assert_eq!(
12767            bin.last_full_lsn, NULL_LSN,
12768            "last_full_lsn unchanged by delta"
12769        );
12770    }
12771
12772    #[test]
12773    fn test_collect_dirty_bins_returns_dirty_bins_only() {
12774        let tree = Tree::new(1, 256);
12775        tree.insert(b"k1".to_vec(), b"v1".to_vec(), Lsn::new(1, 1)).unwrap();
12776        tree.insert(b"k2".to_vec(), b"v2".to_vec(), Lsn::new(1, 2)).unwrap();
12777        let dirty = tree.collect_dirty_bins(1);
12778        assert!(!dirty.is_empty(), "should have dirty BINs after inserts");
12779
12780        for (_db_id, bin_arc) in &dirty {
12781            let mut g = bin_arc.write();
12782            if let TreeNode::Bottom(b) = &mut *g {
12783                b.clear_dirty_after_full_log(Lsn::new(1, 100));
12784            }
12785        }
12786        let dirty2 = tree.collect_dirty_bins(1);
12787        assert!(dirty2.is_empty(), "no dirty BINs after clearing");
12788    }
12789
12790    fn make_bin_for_delta_tests(
12791        entries: Vec<(Vec<u8>, Lsn, Option<Vec<u8>>)>,
12792    ) -> BinStub {
12793        let lsns: Vec<Lsn> = entries.iter().map(|(_, l, _)| *l).collect();
12794        let keys: Vec<Vec<u8>> =
12795            entries.iter().map(|(k, _, _)| k.clone()).collect();
12796        BinStub {
12797            node_id: 1,
12798            level: BIN_LEVEL,
12799            entries: entries
12800                .into_iter()
12801                .map(|(_key, _lsn, data)| BinEntry {
12802                    data,
12803                    known_deleted: false,
12804                    dirty: false,
12805                    expiration_time: 0,
12806                })
12807                .collect(),
12808            key_prefix: Vec::new(),
12809            dirty: false,
12810            is_delta: false,
12811            last_full_lsn: NULL_LSN,
12812            last_delta_lsn: NULL_LSN,
12813            generation: 0,
12814            parent: None,
12815            expiration_in_hours: true,
12816            cursor_count: 0,
12817            prohibit_next_delta: false,
12818            lsn_rep: LsnRep::from_lsns(&lsns),
12819            keys: KeyRep::from_keys(keys),
12820            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
12821        }
12822    }
12823
12824    // ========================================================================
12825    // T-17: BinStub::should_log_delta — faithful JE BIN.shouldLogDelta
12826    // (BIN.java:1892).  These pin the COUNT-based decision against the
12827    // CONFIGURABLE percent (not a dirty-fraction-vs-hardcoded-0.25 heuristic),
12828    // plus the isBINDelta fast path, the numDeltas<=0 guard, and the
12829    // isDeltaProhibited / lastFullLsn==NULL bound.
12830    // ========================================================================
12831
12832    /// Build a full (non-delta) BIN with `n` slots, the first `dirty` of them
12833    /// marked dirty, and a non-NULL last_full_lsn (so a delta is permitted).
12834    fn bin_with_dirty(n: usize, dirty: usize) -> BinStub {
12835        let mut bin = make_bin_for_delta_tests(
12836            (0..n)
12837                .map(|i| {
12838                    (
12839                        format!("{:04}", i).into_bytes(),
12840                        Lsn::new(1, i as u32 + 1),
12841                        Some(vec![i as u8]),
12842                    )
12843                })
12844                .collect(),
12845        );
12846        bin.last_full_lsn = Lsn::new(1, 1); // a prior full exists
12847        for e in bin.entries.iter_mut().take(dirty) {
12848            e.dirty = true;
12849        }
12850        bin
12851    }
12852
12853    /// COUNT-based + CONFIGURABLE percent: with percent=10 and 100 slots, the
12854    /// delta limit is 100*10/100 = 10.  10 dirty slots → delta; 11 dirty → full.
12855    ///
12856    /// This is the core T-17 reproduction: the OLD checkpointer decision used
12857    /// `dirty/total <= 0.25` (hardcoded), so 11/100 = 11% ≤ 25% → it would have
12858    /// (wrongly) logged a DELTA.  The faithful count-based decision against the
12859    /// configurable percent=10 logs a FULL BIN.
12860    #[test]
12861    fn should_log_delta_is_count_based_and_configurable() {
12862        // Exactly at the limit → delta.
12863        assert!(
12864            bin_with_dirty(100, 10).should_log_delta(10),
12865            "numDeltas(10) <= limit(100*10/100=10) must be a delta"
12866        );
12867        // One over the limit → full BIN (FAILS on main: 11/100=11% <= 25%).
12868        assert!(
12869            !bin_with_dirty(100, 11).should_log_delta(10),
12870            "numDeltas(11) > limit(10) must be a FULL BIN under percent=10"
12871        );
12872        // The SAME BIN under the default percent=25 (limit 25) is a delta:
12873        // proves the percent is honoured, not hardcoded.
12874        assert!(
12875            bin_with_dirty(100, 11).should_log_delta(25),
12876            "numDeltas(11) <= limit(25) must be a delta under percent=25"
12877        );
12878        // Integer (truncating) math, exactly as JE: 7 slots, percent=25 →
12879        // limit = 7*25/100 = 1.  1 dirty → delta, 2 dirty → full.
12880        assert!(bin_with_dirty(7, 1).should_log_delta(25));
12881        assert!(!bin_with_dirty(7, 2).should_log_delta(25));
12882    }
12883
12884    /// isBINDelta fast path: a BIN already in delta form always re-logs as a
12885    /// delta (JE: `if (isBINDelta()) return true;`).
12886    #[test]
12887    fn should_log_delta_bin_delta_fast_path() {
12888        let mut bin = bin_with_dirty(100, 90); // 90% dirty: way over any limit
12889        bin.is_delta = true;
12890        // Even with a tiny percent that the dirty count blows past, an
12891        // already-delta BIN re-logs as a delta.
12892        assert!(
12893            bin.should_log_delta(1),
12894            "isBINDelta() must short-circuit to true regardless of percent"
12895        );
12896    }
12897
12898    /// numDeltas <= 0 guard: a BIN with no dirty slots logs a full BIN (an
12899    /// empty delta is invalid).
12900    #[test]
12901    fn should_log_delta_zero_dirty_is_full() {
12902        assert!(!bin_with_dirty(100, 0).should_log_delta(25));
12903    }
12904
12905    /// isDeltaProhibited bound: lastFullLsn == NULL (never logged full) and
12906    /// prohibit_next_delta both force a full BIN.
12907    #[test]
12908    fn should_log_delta_prohibited_forces_full() {
12909        // No prior full BIN.
12910        let mut bin = bin_with_dirty(100, 5); // would be a delta otherwise
12911        bin.last_full_lsn = NULL_LSN;
12912        assert!(
12913            !bin.should_log_delta(25),
12914            "lastFullLsn==NULL must force a full BIN"
12915        );
12916
12917        // prohibit_next_delta set (e.g. a dirty slot was removed by compress).
12918        let mut bin = bin_with_dirty(100, 5);
12919        bin.prohibit_next_delta = true;
12920        assert!(
12921            !bin.should_log_delta(25),
12922            "prohibit_next_delta must force a full BIN"
12923        );
12924    }
12925
12926    /// The prohibit flag is cleared after a full BIN is logged
12927    /// (JE IN.afterLog: setProhibitNextDelta(false)), so the NEXT log may once
12928    /// again be a delta — this is the periodic-full chain bound.
12929    #[test]
12930    fn full_log_clears_prohibit_next_delta() {
12931        let mut bin = bin_with_dirty(100, 5);
12932        bin.prohibit_next_delta = true;
12933        assert!(!bin.should_log_delta(25), "prohibited → full");
12934        bin.clear_dirty_after_full_log(Lsn::new(2, 5));
12935        assert!(
12936            !bin.prohibit_next_delta,
12937            "full log must clear prohibit_next_delta"
12938        );
12939        // Re-dirty a few slots; now a delta is allowed again.
12940        for e in bin.entries.iter_mut().take(5) {
12941            e.dirty = true;
12942        }
12943        assert!(
12944            bin.should_log_delta(25),
12945            "after a full log, a small delta is allowed again"
12946        );
12947    }
12948
12949    // ========================================================================
12950    // Tests: Task #82 — 8 new Tree methods
12951    // ========================================================================
12952
12953    // --- is_root_resident ---
12954
12955    #[test]
12956    fn test_is_root_resident_empty_tree() {
12957        let tree = Tree::new(1, 128);
12958        assert!(!tree.is_root_resident(), "empty tree has no resident root");
12959    }
12960
12961    #[test]
12962    fn test_is_root_resident_after_insert() {
12963        let tree = Tree::new(1, 128);
12964        tree.insert(b"k".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
12965        assert!(tree.is_root_resident(), "root must be resident after insert");
12966    }
12967
12968    // --- get_resident_root_in ---
12969
12970    #[test]
12971    fn test_get_resident_root_in_empty() {
12972        let tree = Tree::new(1, 128);
12973        assert!(tree.get_resident_root_in().is_none());
12974    }
12975
12976    #[test]
12977    fn test_get_resident_root_in_single_entry() {
12978        let tree = Tree::new(1, 128);
12979        tree.insert(b"hello".to_vec(), b"world".to_vec(), Lsn::new(1, 1))
12980            .unwrap();
12981        let root = tree.get_resident_root_in();
12982        assert!(root.is_some(), "root must be Some after insert");
12983        let root_arc = tree.get_root().unwrap();
12984        assert!(
12985            Arc::ptr_eq(&root_arc, &root.unwrap()),
12986            "get_resident_root_in must return the same Arc as get_root"
12987        );
12988    }
12989
12990    #[test]
12991    fn test_get_resident_root_in_multi_entry() {
12992        let tree = Tree::new(1, 4);
12993        for i in 0u32..20 {
12994            let k = format!("rr{:04}", i).into_bytes();
12995            tree.insert(k, vec![i as u8], Lsn::new(1, i)).unwrap();
12996        }
12997        assert!(tree.get_resident_root_in().is_some());
12998    }
12999
13000    // --- get_parent_bin_for_child_ln ---
13001
13002    #[test]
13003    fn test_get_parent_bin_for_child_ln_empty_tree() {
13004        let tree = Tree::new(1, 128);
13005        assert!(tree.get_parent_bin_for_child_ln(b"key").is_none());
13006    }
13007
13008    #[test]
13009    fn test_get_parent_bin_for_child_ln_single_entry() {
13010        let tree = Tree::new(1, 128);
13011        tree.insert(b"alpha".to_vec(), b"val".to_vec(), Lsn::new(1, 1))
13012            .unwrap();
13013        let bin = tree.get_parent_bin_for_child_ln(b"alpha");
13014        assert!(bin.is_some(), "must return Some for a present key");
13015        assert!(bin.unwrap().read().is_bin(), "returned node must be a BIN");
13016    }
13017
13018    #[test]
13019    fn test_get_parent_bin_for_child_ln_multi_key() {
13020        let tree = Tree::new(1, 8);
13021        let keys: &[&[u8]] = &[b"aa", b"bb", b"cc", b"dd", b"ee"];
13022        for &k in keys {
13023            tree.insert(k.to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
13024        }
13025        for &k in keys {
13026            let bin = tree.get_parent_bin_for_child_ln(k);
13027            assert!(bin.is_some(), "must return Some for {:?}", k);
13028            assert!(bin.unwrap().read().is_bin());
13029        }
13030    }
13031
13032    // --- find_bin_for_insert ---
13033
13034    #[test]
13035    fn test_find_bin_for_insert_empty_tree() {
13036        let tree = Tree::new(1, 128);
13037        assert!(tree.find_bin_for_insert(b"newkey").is_none());
13038    }
13039
13040    #[test]
13041    fn test_find_bin_for_insert_returns_bin() {
13042        let tree = Tree::new(1, 128);
13043        tree.insert(b"existing".to_vec(), b"data".to_vec(), Lsn::new(1, 1))
13044            .unwrap();
13045        let bin = tree.find_bin_for_insert(b"newkey");
13046        assert!(bin.is_some());
13047        assert!(bin.unwrap().read().is_bin());
13048    }
13049
13050    #[test]
13051    fn test_find_bin_for_insert_same_as_parent_bin() {
13052        let tree = Tree::new(1, 128);
13053        tree.insert(b"foo".to_vec(), b"bar".to_vec(), Lsn::new(1, 1)).unwrap();
13054        let a = tree.get_parent_bin_for_child_ln(b"foo").unwrap();
13055        let b_arc = tree.find_bin_for_insert(b"foo").unwrap();
13056        assert!(
13057            Arc::ptr_eq(&a, &b_arc),
13058            "find_bin_for_insert must return the same BIN as get_parent_bin_for_child_ln"
13059        );
13060    }
13061
13062    // --- search_splits_allowed ---
13063
13064    #[test]
13065    fn test_search_splits_allowed_empty_tree() {
13066        let tree = Tree::new(1, 128);
13067        assert!(tree.search_splits_allowed(b"k").is_none());
13068    }
13069
13070    #[test]
13071    fn test_search_splits_allowed_finds_existing_key() {
13072        let tree = Tree::new(1, 8);
13073        for i in 0u32..10 {
13074            let k = format!("sa{:04}", i).into_bytes();
13075            tree.insert(k, vec![i as u8], Lsn::new(1, i)).unwrap();
13076        }
13077        for i in 0u32..10 {
13078            let k = format!("sa{:04}", i).into_bytes();
13079            let sr = tree.search_splits_allowed(&k);
13080            assert!(
13081                sr.is_some() && sr.unwrap().exact_parent_found,
13082                "search_splits_allowed must find sa{:04}",
13083                i
13084            );
13085        }
13086    }
13087
13088    #[test]
13089    fn test_search_splits_allowed_missing_key() {
13090        let tree = Tree::new(1, 8);
13091        tree.insert(b"present".to_vec(), b"v".to_vec(), Lsn::new(1, 1))
13092            .unwrap();
13093        let sr = tree.search_splits_allowed(b"absent");
13094        assert!(
13095            sr.is_none_or(|r| !r.exact_parent_found),
13096            "search_splits_allowed must not find absent key"
13097        );
13098    }
13099
13100    // --- rebuild_in_list ---
13101
13102    #[test]
13103    fn test_rebuild_in_list_empty_tree() {
13104        let tree = Tree::new(1, 128);
13105        assert!(tree.rebuild_in_list().is_empty());
13106    }
13107
13108    #[test]
13109    fn test_rebuild_in_list_single_entry() {
13110        let tree = Tree::new(1, 128);
13111        tree.insert(b"one".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
13112        let list = tree.rebuild_in_list();
13113        // Expect root IN + BIN = 2 nodes.
13114        assert_eq!(
13115            list.len(),
13116            2,
13117            "single-entry tree must have exactly 2 nodes"
13118        );
13119        let has_bin = list.iter().any(|a| a.read().is_bin());
13120        let has_in = list.iter().any(|a| !a.read().is_bin());
13121        assert!(has_bin, "list must contain at least one BIN");
13122        assert!(has_in, "list must contain at least one upper IN");
13123    }
13124
13125    #[test]
13126    fn test_rebuild_in_list_multi_entry() {
13127        let tree = Tree::new(1, 4);
13128        for i in 0u32..20 {
13129            let k = format!("ri{:04}", i).into_bytes();
13130            tree.insert(k, vec![i as u8], Lsn::new(1, i)).unwrap();
13131        }
13132        let list = tree.rebuild_in_list();
13133        let stats = tree.collect_stats();
13134        let expected_nodes = (stats.n_ins + stats.n_bins) as usize;
13135        assert_eq!(
13136            list.len(),
13137            expected_nodes,
13138            "rebuild_in_list must return all {} nodes",
13139            expected_nodes
13140        );
13141    }
13142
13143    // --- validate_in_list ---
13144
13145    #[test]
13146    fn test_validate_in_list_empty_tree() {
13147        let tree = Tree::new(1, 128);
13148        assert!(tree.validate_in_list(), "empty tree must be valid");
13149    }
13150
13151    #[test]
13152    fn test_validate_in_list_single_entry() {
13153        let tree = Tree::new(1, 128);
13154        tree.insert(b"v".to_vec(), b"data".to_vec(), Lsn::new(1, 1)).unwrap();
13155        assert!(tree.validate_in_list(), "single-entry tree must be valid");
13156    }
13157
13158    #[test]
13159    fn test_validate_in_list_multi_entry() {
13160        let tree = Tree::new(1, 4);
13161        for i in 0u32..20 {
13162            let k = format!("vl{:04}", i).into_bytes();
13163            tree.insert(k, vec![i as u8], Lsn::new(1, i)).unwrap();
13164        }
13165        assert!(tree.validate_in_list(), "multi-entry tree must be valid");
13166    }
13167
13168    #[test]
13169    fn test_validate_in_list_empty_in_fails() {
13170        // Manually build a tree where the root IN has no entries — invalid.
13171        let root_arc = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
13172            node_id: generate_node_id(),
13173            level: MAIN_LEVEL | 2,
13174            entries: vec![], // empty — structurally invalid
13175            targets: TargetRep::None,
13176            dirty: false,
13177            generation: 0,
13178            parent: None,
13179            lsn_rep: LsnRep::Empty,
13180        })));
13181        let tree = Tree::new(1, 128);
13182        *tree.root.write() = Some(root_arc);
13183        assert!(
13184            !tree.validate_in_list(),
13185            "a tree with an empty Internal node must fail validation"
13186        );
13187    }
13188
13189    // --- get_parent_in_for_child_in ---
13190
13191    #[test]
13192    fn test_get_parent_in_for_child_in_empty_tree() {
13193        let tree = Tree::new(1, 128);
13194        assert!(tree.get_parent_in_for_child_in(999).is_none());
13195    }
13196
13197    #[test]
13198    fn test_get_parent_in_for_child_in_single_entry() {
13199        // A single-insert tree has: root IN → BIN.
13200        // The root IN is the parent of the BIN.
13201        let tree = Tree::new(1, 128);
13202        tree.insert(b"p".to_vec(), b"v".to_vec(), Lsn::new(1, 1)).unwrap();
13203
13204        let root_arc = tree.get_root().as_ref().unwrap().clone();
13205        let bin_node_id = {
13206            let g = root_arc.read();
13207            match &*g {
13208                TreeNode::Internal(n) => {
13209                    let child = n.child_ref(0).unwrap();
13210                    let cg = child.read();
13211                    match &*cg {
13212                        TreeNode::Bottom(b) => b.node_id,
13213                        _ => panic!("expected BIN"),
13214                    }
13215                }
13216                _ => panic!("expected Internal root"),
13217            }
13218        };
13219
13220        let result = tree.get_parent_in_for_child_in(bin_node_id);
13221        assert!(result.is_some(), "must find parent of BIN");
13222        let (parent_arc, slot) = result.unwrap();
13223        assert!(Arc::ptr_eq(&parent_arc, &root_arc));
13224        assert_eq!(slot, 0);
13225    }
13226
13227    #[test]
13228    fn test_get_parent_in_for_child_in_not_found() {
13229        let tree = Tree::new(1, 128);
13230        tree.insert(b"x".to_vec(), b"y".to_vec(), Lsn::new(1, 1)).unwrap();
13231        assert!(tree.get_parent_in_for_child_in(u64::MAX).is_none());
13232    }
13233
13234    #[test]
13235    fn test_get_parent_in_for_child_in_multi_level() {
13236        // Build a tree with at least 3 levels so we test the recursive descent.
13237        let tree = Tree::new(1, 4);
13238        for i in 0u32..20 {
13239            let k = format!("ml{:04}", i).into_bytes();
13240            tree.insert(k, vec![i as u8], Lsn::new(1, i)).unwrap();
13241        }
13242
13243        // Collect all BIN node_ids via rebuild_in_list.
13244        let nodes = tree.rebuild_in_list();
13245        let bin_ids: Vec<u64> = nodes
13246            .iter()
13247            .filter_map(|a| {
13248                let g = a.read();
13249                if g.is_bin()
13250                    && let TreeNode::Bottom(b) = &*g
13251                {
13252                    return Some(b.node_id);
13253                }
13254                None
13255            })
13256            .collect();
13257
13258        for bin_id in bin_ids {
13259            let result = tree.get_parent_in_for_child_in(bin_id);
13260            assert!(
13261                result.is_some(),
13262                "every BIN (id={}) must have a parent IN",
13263                bin_id
13264            );
13265            let (parent_arc, _slot) = result.unwrap();
13266            assert!(
13267                !parent_arc.read().is_bin(),
13268                "parent of a BIN must be an Internal node"
13269            );
13270        }
13271    }
13272
13273    /// H-9 regression: BinStub::strip_lns actually drops the slot data
13274    /// (not just stats accounting).
13275    #[test]
13276    fn test_h9_strip_lns_actually_frees_data() {
13277        use crate::tree::{BinEntry, BinStub};
13278        use noxu_util::lsn::Lsn;
13279        let mut bin = BinStub {
13280            node_id: 1,
13281            level: 1,
13282            entries: Vec::new(),
13283            key_prefix: Vec::new(),
13284            dirty: false,
13285            is_delta: false,
13286            last_full_lsn: Lsn::from_u64(0),
13287            last_delta_lsn: Lsn::from_u64(0),
13288            generation: 0,
13289            parent: None,
13290            expiration_in_hours: true,
13291            cursor_count: 0,
13292            prohibit_next_delta: false,
13293            lsn_rep: LsnRep::Empty,
13294            keys: KeyRep::new(),
13295            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
13296        };
13297        // Three slots with embedded data + VALID logged LSNs (one dirty).
13298        // JE-faithful: a slot with a valid LSN is strippable regardless of the
13299        // dirty bit (its value is recoverable from the log); only a NULL-LSN
13300        // (never-logged / deferred-write) slot is preserved.
13301        bin.entries.push(BinEntry {
13302            data: Some(vec![0u8; 64]),
13303            known_deleted: false,
13304            dirty: false,
13305            expiration_time: 0,
13306        });
13307        bin.entries.push(BinEntry {
13308            data: Some(vec![0u8; 32]),
13309            known_deleted: false,
13310            dirty: false,
13311            expiration_time: 0,
13312        });
13313        bin.entries.push(BinEntry {
13314            data: Some(vec![0u8; 16]),
13315            known_deleted: false,
13316            dirty: true, // dirty BUT logged -> still strippable (EVICTOR-RECLAIM-1)
13317            expiration_time: 0,
13318        });
13319        // T-2: keep the key rep aligned with the pushed slots.
13320        bin.keys = KeyRep::from_keys(vec![
13321            b"a".to_vec(),
13322            b"b".to_vec(),
13323            b"c".to_vec(),
13324        ]);
13325        // Give all three slots VALID (non-NULL) LSNs so they are recoverable
13326        // from the log and therefore strippable.
13327        bin.set_lsn(0, Lsn::new(1, 100));
13328        bin.set_lsn(1, Lsn::new(1, 200));
13329        bin.set_lsn(2, Lsn::new(1, 300));
13330
13331        let freed = bin.strip_lns();
13332        assert_eq!(
13333            freed,
13334            64 + 32 + 16,
13335            "all logged slots stripped regardless of dirty (JE evictLNs)"
13336        );
13337        assert!(bin.entries[0].data.is_none(), "logged slot data dropped");
13338        assert!(bin.entries[1].data.is_none(), "logged slot data dropped");
13339        assert!(
13340            bin.entries[2].data.is_none(),
13341            "dirty-but-logged slot data dropped (recoverable from log)"
13342        );
13343
13344        // A NULL-LSN slot (never logged) must be preserved — its only copy is
13345        // the in-memory value.
13346        bin.entries[0].data = Some(vec![0u8; 64]);
13347        bin.set_lsn(0, noxu_util::NULL_LSN);
13348        let freed_null = bin.strip_lns();
13349        assert_eq!(
13350            freed_null, 0,
13351            "NULL-LSN (unlogged) slot must NOT be stripped"
13352        );
13353        assert!(bin.entries[0].data.is_some(), "unlogged slot data preserved");
13354
13355        // Cursor pin prevents stripping.
13356        bin.set_lsn(0, Lsn::new(1, 100));
13357        bin.cursor_count = 1;
13358        let freed_with_cursor = bin.strip_lns();
13359        assert_eq!(
13360            freed_with_cursor, 0,
13361            "strip_lns must skip when cursor pinned"
13362        );
13363        assert!(
13364            bin.entries[0].data.is_some(),
13365            "data preserved while cursor pinned"
13366        );
13367    }
13368
13369    // St-H4: the binary upper_in_floor_index must return the same slot as a
13370    // reference linear floor scan for all probe keys (incl. before-all,
13371    // after-all, between, and exact matches).
13372    #[test]
13373    fn test_upper_in_floor_index_matches_linear_scan() {
13374        // Reference linear floor scan (the pre-St-H4 algorithm): slot 0 is the
13375        // virtual −∞ key; walk forward while entry.key ≤ key.
13376        fn linear_floor(entries: &[InEntry], key: &[u8]) -> usize {
13377            let mut idx = 0usize;
13378            for (i, entry) in entries.iter().enumerate() {
13379                if i == 0 {
13380                    idx = 0;
13381                } else if entry.key.as_slice() <= key {
13382                    idx = i;
13383                } else {
13384                    break;
13385                }
13386            }
13387            idx
13388        }
13389
13390        let tree = Tree::new(1, 256);
13391        // Build sorted IN slot key sets of varying size; slot 0 = virtual −∞
13392        // (empty key sorts first), the rest strictly ascending.
13393        for n_slots in 1usize..40 {
13394            let mut entries: Vec<InEntry> = Vec::with_capacity(n_slots);
13395            entries.push(InEntry { key: vec![] });
13396            for i in 1..n_slots {
13397                // Strictly-ascending two-byte keys with gaps so probes can
13398                // fall between, on, before, and after them.
13399                let v = (i as u16) * 4;
13400                entries.push(InEntry {
13401                    key: vec![(v >> 8) as u8, (v & 0xFF) as u8],
13402                });
13403            }
13404            for probe in 0u16..=(n_slots as u16 * 4 + 4) {
13405                let key = vec![(probe >> 8) as u8, (probe & 0xFF) as u8];
13406                assert_eq!(
13407                    tree.upper_in_floor_index(&entries, &key),
13408                    linear_floor(&entries, &key),
13409                    "floor mismatch: n_slots={n_slots}, key={key:?}"
13410                );
13411            }
13412        }
13413    }
13414}
13415
13416// ─────────────────────────────────────────────────────────────────────────
13417// St-H6: BIN split inherits expiration_in_hours from the splitting BIN.
13418// ─────────────────────────────────────────────────────────────────────────
13419
13420/// Unit test for the St-H6 fix: the right-half sibling created by
13421/// `split_child` inherits `expiration_in_hours` from the splitting BIN.
13422///
13423/// Before the fix, the sibling was always created with
13424/// `expiration_in_hours = false`, causing hours-granularity TTL entries
13425/// (expiration_time ~495k) to be compared against `current_time_secs()`
13426/// (~1.78B) and treated as expired.
13427///
13428/// This test:
13429///   1. Creates a tree with max_entries = 4 and inserts 4 entries directly
13430///      (bypassing `update_key_expiration`) with non-zero `expiration_time`
13431///      and `expiration_in_hours = true` on the BIN.
13432///   2. Triggers a split.
13433///   3. Asserts that the right-half sibling has `expiration_in_hours = true`
13434///      (inherited, not hardcoded false).
13435#[test]
13436fn test_split_child_sibling_inherits_expiration_in_hours() {
13437    use crate::tree::{BIN_LEVEL, BinEntry, BinStub, MAIN_LEVEL, TreeNode};
13438    use noxu_util::{Lsn, NULL_LSN};
13439    use parking_lot::RwLock;
13440    use std::sync::Arc;
13441
13442    // Manually build a tree with one BIN (4 entries, expiration_in_hours=true).
13443    let tree = Tree::new(99, 4);
13444
13445    // Pre-populate the tree root for the test.
13446    let entries: Vec<BinEntry> = (0u8..4u8)
13447        .map(|_k| BinEntry {
13448            data: Some(vec![_k, _k]),
13449            known_deleted: false,
13450            dirty: true,
13451            expiration_time: 495_630, // hours-since-epoch value, 2026
13452        })
13453        .collect();
13454    let bin_keys: Vec<Vec<u8>> = (0u8..4u8).map(|k| vec![k]).collect();
13455    let bin = Arc::new(RwLock::new(TreeNode::Bottom(BinStub {
13456        node_id: 1,
13457        level: BIN_LEVEL,
13458        entries,
13459        key_prefix: Vec::new(),
13460        dirty: true,
13461        is_delta: false,
13462        last_full_lsn: NULL_LSN,
13463        last_delta_lsn: NULL_LSN,
13464        generation: 0,
13465        parent: None,
13466        expiration_in_hours: true, // hours-granularity entries
13467        cursor_count: 0,
13468        prohibit_next_delta: false,
13469        lsn_rep: LsnRep::Empty,
13470        keys: KeyRep::from_keys(bin_keys),
13471        compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
13472    })));
13473
13474    let root = Arc::new(RwLock::new(TreeNode::Internal(InNodeStub {
13475        node_id: 2,
13476        level: MAIN_LEVEL | 2,
13477        entries: vec![InEntry {
13478            key: vec![], // virtual key for slot 0 (-infinity)
13479        }],
13480        targets: TargetRep::Sparse(vec![(0, Arc::clone(&bin))]),
13481        dirty: true,
13482        generation: 0,
13483        parent: None,
13484        lsn_rep: LsnRep::Empty,
13485    })));
13486    {
13487        let mut b = bin.write();
13488        b.set_parent(Some(Arc::downgrade(&root)));
13489    }
13490    *tree.root.write() = Some(Arc::clone(&root));
13491
13492    // Trigger split_child on the root.
13493    Tree::split_child(
13494        &root,
13495        0,
13496        4,
13497        Lsn::new(1, 500),
13498        SplitHint::Normal,
13499        &[],
13500        None,
13501        false,
13502        None,
13503    )
13504    .expect("split_child should succeed");
13505
13506    // After the split: root has two children — left BIN and right sibling.
13507    let root_guard = root.read();
13508    let TreeNode::Internal(ref in_node) = *root_guard else {
13509        panic!("root should be Internal after split");
13510    };
13511    assert_eq!(
13512        in_node.entries.len(),
13513        2,
13514        "root should have 2 entries (children) after split"
13515    );
13516
13517    // Right-half sibling is at slot 1.
13518    let sibling_arc = in_node
13519        .get_child(1)
13520        .expect("right-half sibling should exist at slot 1");
13521    let sibling_guard = sibling_arc.read();
13522    let TreeNode::Bottom(ref sibling) = *sibling_guard else {
13523        panic!("right sibling should be a BIN");
13524    };
13525
13526    assert!(
13527        sibling.expiration_in_hours,
13528        "St-H6: right-half sibling expiration_in_hours must be true \
13529             (inherited from splitting BIN); got false"
13530    );
13531
13532    // Verify the sibling's entries have the expected expiration_time.
13533    for e in &sibling.entries {
13534        assert_eq!(
13535            e.expiration_time, 495_630,
13536            "sibling entry expiration_time should be preserved: got {}",
13537            e.expiration_time
13538        );
13539        // With in_hours=true, is_expired should return false (future).
13540        assert!(
13541            !noxu_util::ttl::is_expired(
13542                e.expiration_time,
13543                sibling.expiration_in_hours
13544            ),
13545            "St-H6: sibling TTL entry ({}) should NOT appear expired \
13546                 with expiration_in_hours={}",
13547            e.expiration_time,
13548            sibling.expiration_in_hours
13549        );
13550    }
13551}
13552
13553/// Regression confirmation: `is_expired` with wrong `in_hours = false`
13554/// would falsely expire hours-granularity values (~495k hours since epoch).
13555#[test]
13556fn test_hours_value_is_expired_only_with_false_flag() {
13557    // Hours-since-epoch value for ~2026 + 1 000 h TTL.
13558    let exp_hours: u32 = 495_630;
13559    // Correctly treated as hours: not expired.
13560    assert!(
13561        !noxu_util::ttl::is_expired(exp_hours, true),
13562        "exp_hours={exp_hours} should NOT be expired when in_hours=true"
13563    );
13564    // Incorrectly treated as seconds (pre-fix right sibling): expired.
13565    assert!(
13566        noxu_util::ttl::is_expired(exp_hours, false),
13567        "exp_hours={exp_hours} should be expired when in_hours=false \
13568             (St-H6 demonstrates the wrong-flag scenario)"
13569    );
13570}
13571
13572// =============================================================================
13573// IN-redo unit tests (DRIFT-1 / Stage 1)
13574// =============================================================================
13575
13576#[cfg(test)]
13577mod in_redo_tests {
13578    use super::*;
13579
13580    /// Build a BinStub with `n` entries (key = [i as u8], lsn = lsn(1, i))
13581    /// and serialise it.  Returns (node_id, node_data_bytes).
13582    fn make_bin_bytes(node_id: u64, n: usize) -> Vec<u8> {
13583        let mut bin = BinStub {
13584            node_id,
13585            level: BIN_LEVEL,
13586            entries: Vec::new(),
13587            key_prefix: Vec::new(),
13588            dirty: false,
13589            is_delta: false,
13590            last_full_lsn: noxu_util::NULL_LSN,
13591            last_delta_lsn: noxu_util::NULL_LSN,
13592            generation: 0,
13593            parent: None,
13594            expiration_in_hours: true,
13595            cursor_count: 0,
13596            prohibit_next_delta: false,
13597            lsn_rep: LsnRep::Empty,
13598            keys: KeyRep::new(),
13599            compact_max_key_length: INKeyRep_DEFAULT_MAX_KEY_LENGTH,
13600        };
13601        for i in 0..n {
13602            // T-2/T-3: route through insert so entries/keys/lsn_rep stay
13603            // aligned; the serialized bytes are identical.
13604            bin.insert_with_prefix(
13605                vec![i as u8],
13606                Lsn::new(1, (i + 1) as u32),
13607                Some(vec![i as u8]),
13608            );
13609        }
13610        bin.serialize_full()
13611    }
13612
13613    /// Verify that recover_in_redo inserts a BIN as root when the tree is empty.
13614    ///
13615    /// JE RecoveryManager.recoverRootIN: `root == null` path.
13616    #[test]
13617    fn test_recover_in_redo_root_bin_inserted_into_empty_tree() {
13618        let tree = Tree::new(42, 128);
13619        assert!(tree.is_empty());
13620        let bytes = make_bin_bytes(1, 3);
13621        let log_lsn = Lsn::new(1, 100);
13622        let result = tree.recover_in_redo(
13623            log_lsn, /*is_root=*/ true, /*is_bin=*/ true, &bytes,
13624        );
13625        assert_eq!(result, InRedoResult::Inserted, "expected Inserted");
13626        // Tree should now have 3 entries.
13627        assert_eq!(tree.count_entries(), 3);
13628    }
13629
13630    /// Verify that recover_in_redo replaces a root BIN when the logged version is newer.
13631    ///
13632    /// JE RootUpdater.doWork: `DbLsn.compareTo(originalLsn, lsn) < 0` path.
13633    #[test]
13634    fn test_recover_in_redo_root_bin_replaced_when_log_newer() {
13635        let tree = Tree::new(42, 128);
13636        // Install an old root (2 entries, older LSN).
13637        let old_bytes = make_bin_bytes(1, 2);
13638        let old_lsn = Lsn::new(1, 50);
13639        tree.recover_in_redo(old_lsn, true, true, &old_bytes);
13640        assert_eq!(tree.count_entries(), 2);
13641        // Replay with newer LSN and 4 entries.
13642        let new_bytes = make_bin_bytes(1, 4);
13643        let new_lsn = Lsn::new(1, 100);
13644        let result = tree.recover_in_redo(new_lsn, true, true, &new_bytes);
13645        assert_eq!(result, InRedoResult::Replaced);
13646        assert_eq!(tree.count_entries(), 4);
13647    }
13648
13649    /// Verify that an older logged BIN does NOT replace a newer in-memory root.
13650    ///
13651    /// JE RootUpdater.doWork: `DbLsn.compareTo(originalLsn, lsn) >= 0` skip path.
13652    #[test]
13653    fn test_recover_in_redo_root_bin_skipped_when_tree_newer() {
13654        let tree = Tree::new(42, 128);
13655        // Install a newer root.
13656        let new_bytes = make_bin_bytes(1, 4);
13657        let new_lsn = Lsn::new(1, 200);
13658        tree.recover_in_redo(new_lsn, true, true, &new_bytes);
13659        // Attempt to replay an older version.
13660        let old_bytes = make_bin_bytes(1, 2);
13661        let old_lsn = Lsn::new(1, 100);
13662        let result = tree.recover_in_redo(old_lsn, true, true, &old_bytes);
13663        assert_eq!(result, InRedoResult::Skipped);
13664        // Tree still holds the newer 4-entry version.
13665        assert_eq!(tree.count_entries(), 4);
13666    }
13667
13668    /// deserialize_bin round-trips through serialize_full.
13669    #[test]
13670    fn test_deserialize_bin_round_trip() {
13671        let bytes = make_bin_bytes(99, 5);
13672        let bin = Tree::deserialize_bin(&bytes).expect("must deserialize");
13673        assert_eq!(bin.node_id, 99);
13674        assert_eq!(bin.entries.len(), 5);
13675        for i in 0..bin.entries.len() {
13676            assert_eq!(bin.get_full_key(i).unwrap(), vec![i as u8]);
13677        }
13678    }
13679
13680    /// deserialize_upper_in round-trips through write_to_bytes (Internal).
13681    #[test]
13682    fn test_deserialize_upper_in_round_trip() {
13683        // Build an InNodeStub and serialize via write_to_bytes.
13684        let node = TreeNode::Internal(InNodeStub {
13685            node_id: 77,
13686            level: 0x10002,
13687            entries: vec![
13688                InEntry { key: vec![1, 2, 3] },
13689                InEntry { key: vec![4, 5, 6] },
13690            ],
13691            targets: TargetRep::None,
13692            dirty: false,
13693            generation: 0,
13694            parent: None,
13695            lsn_rep: LsnRep::Empty,
13696        });
13697        let bytes = node.write_to_bytes();
13698        let restored =
13699            Tree::deserialize_upper_in(&bytes).expect("must deserialize");
13700        assert_eq!(restored.node_id, 77);
13701        assert_eq!(restored.level, 0x10002);
13702        assert_eq!(restored.entries.len(), 2);
13703        assert_eq!(restored.entries[0].key, vec![1, 2, 3]);
13704        assert_eq!(restored.entries[1].key, vec![4, 5, 6]);
13705    }
13706}
13707
13708// --- Part 2 acceptance tests: key_prefixing flag (DRIFT-3) ---
13709//
13710// JE `IN.computeKeyPrefix` returns null when `databaseImpl.getKeyPrefixing()`
13711// is false, so no prefix compression is ever applied to those BINs. Noxu was
13712// always applying prefix compression. This checks that the flag is honoured.
13713//
13714// Ref: `IN.java computeKeyPrefix` ~line 2456,
13715//      `DatabaseConfig.setKeyPrefixing` / `DatabaseImpl.getKeyPrefixing`.
13716#[cfg(test)]
13717mod key_prefixing_tests {
13718    use super::*;
13719
13720    /// Helper: find the first (leftmost) BIN in the tree.
13721    fn find_first_bin(node: &Arc<RwLock<TreeNode>>) -> Arc<RwLock<TreeNode>> {
13722        let child_opt = {
13723            let g = node.read();
13724            match &*g {
13725                TreeNode::Bottom(_) => None,
13726                TreeNode::Internal(n) => {
13727                    Some(Arc::clone(n.child_ref(0).expect("child")))
13728                }
13729            }
13730        };
13731        match child_opt {
13732            None => Arc::clone(node),
13733            Some(child) => find_first_bin(&child),
13734        }
13735    }
13736
13737    /// With `key_prefixing = false` (the default), keys must be stored without
13738    /// any prefix: the BIN's `key_prefix` must remain empty after inserts.
13739    #[test]
13740    fn test_key_prefixing_false_stores_full_keys() {
13741        // Default is key_prefixing = false.
13742        let tree = Tree::new(1, 16);
13743        assert!(!tree.key_prefixing, "default must be false");
13744
13745        let lsn = noxu_util::Lsn::new(1, 10);
13746        // Insert keys with a long common prefix.
13747        for i in 0u8..8 {
13748            let key = vec![b'r', b'e', b'c', b'o', b'r', b'd', b':', i];
13749            tree.insert(key, vec![i], lsn).expect("insert");
13750        }
13751
13752        let root = tree.get_root().expect("root");
13753        let bin_arc = find_first_bin(&root);
13754        let guard = bin_arc.read();
13755        let TreeNode::Bottom(ref bin) = *guard else {
13756            panic!("must be a BIN");
13757        };
13758        assert!(
13759            bin.key_prefix.is_empty(),
13760            "key_prefix must be empty when key_prefixing=false, got {:?}",
13761            bin.key_prefix
13762        );
13763        assert_eq!(bin.entries.len(), 8);
13764        // Keys must be stored as full keys.
13765        assert_eq!(
13766            bin.get_full_key(0).unwrap(),
13767            vec![b'r', b'e', b'c', b'o', b'r', b'd', b':', 0]
13768        );
13769    }
13770
13771    /// With `key_prefixing = true`, keys with a common prefix are compressed:
13772    /// the BIN's `key_prefix` must be non-empty.
13773    #[test]
13774    fn test_key_prefixing_true_compresses_keys() {
13775        let mut tree = Tree::new(1, 16);
13776        tree.set_key_prefixing(true);
13777
13778        let lsn = noxu_util::Lsn::new(1, 10);
13779        for i in 0u8..8 {
13780            let key = vec![b'r', b'e', b'c', b'o', b'r', b'd', b':', i];
13781            tree.insert(key, vec![i], lsn).expect("insert");
13782        }
13783
13784        let root = tree.get_root().expect("root");
13785        let bin_arc = find_first_bin(&root);
13786        let guard = bin_arc.read();
13787        let TreeNode::Bottom(ref bin) = *guard else {
13788            panic!("must be a BIN");
13789        };
13790        // Prefix compression must kick in: all keys share "record:".
13791        assert!(
13792            !bin.key_prefix.is_empty(),
13793            "key_prefix must be non-empty when key_prefixing=true"
13794        );
13795        assert_eq!(
13796            bin.key_prefix,
13797            b"record:".to_vec(),
13798            "prefix must be the common prefix of all inserted keys"
13799        );
13800    }
13801
13802    /// Custom-comparator databases (sorted-dup) always bypass prefix
13803    /// regardless of key_prefixing: `insert_cmp` does not touch key_prefix.
13804    #[test]
13805    fn test_key_prefixing_custom_comparator_no_prefix() {
13806        let cmp: KeyComparatorFn = Arc::new(|a: &[u8], b: &[u8]| a.cmp(b));
13807        let mut tree = Tree::new_with_comparator(1, 16, cmp);
13808        // Enable key_prefixing — should have no effect via insert_cmp path.
13809        tree.set_key_prefixing(true);
13810
13811        let lsn = noxu_util::Lsn::new(1, 10);
13812        for i in 0u8..8 {
13813            let key = vec![b'r', b'e', b'c', b'o', b'r', b'd', b':', i];
13814            tree.insert(key, vec![i], lsn).expect("insert");
13815        }
13816
13817        let root = tree.get_root().expect("root");
13818        let bin_arc = find_first_bin(&root);
13819        let guard = bin_arc.read();
13820        let TreeNode::Bottom(ref bin) = *guard else {
13821            panic!("must be a BIN");
13822        };
13823        // Custom-comparator path (insert_cmp) does not set key_prefix.
13824        assert!(
13825            bin.key_prefix.is_empty(),
13826            "custom-comparator path must not set key_prefix"
13827        );
13828    }
13829}
13830
13831// --- Part 1 acceptance tests: splitSpecial heuristic (DRIFT-1) ---
13832//
13833// JE `IN.splitSpecial` / `Tree.forceSplit`: when all routing decisions during
13834// descent are leftmost (`AllLeft`) or rightmost (`AllRight`), the split index
13835// is forced to 1 or `n-1` respectively instead of `n/2`. This halves the
13836// number of splits for monotonically increasing / decreasing key workloads
13837// (sequential append / prepend) because each split leaves the BIN near-full.
13838//
13839// Ref: `IN.java splitSpecial` ~line 4129, `Tree.java forceSplit` ~line 1907.
13840#[cfg(test)]
13841mod split_special_tests {
13842    use super::*;
13843
13844    /// Test helper: descend the tree to the BIN that holds (or would hold)
13845    /// `key`, returning its arc.  Mirrors the read-path descent used by
13846    /// `Tree::search`; sufficient for unit tests that need to mutate a slot.
13847    fn find_bin_arc_for_key(
13848        node_arc: &Arc<RwLock<TreeNode>>,
13849        key: &[u8],
13850    ) -> Option<Arc<RwLock<TreeNode>>> {
13851        let mut current = node_arc.clone();
13852        loop {
13853            let next = {
13854                let g = current.read();
13855                match &*g {
13856                    TreeNode::Bottom(_) => return Some(current.clone()),
13857                    TreeNode::Internal(n) => {
13858                        if n.entries.is_empty() {
13859                            return None;
13860                        }
13861                        let mut idx = 0usize;
13862                        for (i, e) in n.entries.iter().enumerate() {
13863                            if i == 0 || e.key.as_slice() <= key {
13864                                idx = i;
13865                            } else {
13866                                break;
13867                            }
13868                        }
13869                        n.get_child(idx)?
13870                    }
13871                }
13872            };
13873            current = next;
13874        }
13875    }
13876
13877    /// Count total leaf (BIN) nodes in the tree by DFS.
13878    fn count_bins(node: &Arc<RwLock<TreeNode>>) -> usize {
13879        let g = node.read();
13880        match &*g {
13881            TreeNode::Bottom(_) => 1,
13882            TreeNode::Internal(n) => {
13883                n.resident_children().iter().map(count_bins).sum()
13884            }
13885        }
13886    }
13887
13888    /// Return total key count across all BINs.
13889    fn count_keys(node: &Arc<RwLock<TreeNode>>) -> usize {
13890        let g = node.read();
13891        match &*g {
13892            TreeNode::Bottom(b) => b.entries.len(),
13893            TreeNode::Internal(n) => {
13894                n.resident_children().iter().map(count_keys).sum()
13895            }
13896        }
13897    }
13898
13899    /// Returns the number of entries in the leftmost BIN.
13900    fn leftmost_bin_size(node: &Arc<RwLock<TreeNode>>) -> usize {
13901        let g = node.read();
13902        match &*g {
13903            TreeNode::Bottom(b) => b.entries.len(),
13904            TreeNode::Internal(n) => {
13905                let first_child = n.child_ref(0).expect("child");
13906                leftmost_bin_size(first_child)
13907            }
13908        }
13909    }
13910
13911    /// Returns the number of entries in the rightmost BIN.
13912    fn rightmost_bin_size(node: &Arc<RwLock<TreeNode>>) -> usize {
13913        let g = node.read();
13914        match &*g {
13915            TreeNode::Bottom(b) => b.entries.len(),
13916            TreeNode::Internal(n) => {
13917                let last_child = n
13918                    .child_ref(n.entries.len().saturating_sub(1))
13919                    .expect("child");
13920                rightmost_bin_size(last_child)
13921            }
13922        }
13923    }
13924
13925    /// `splitSpecial` ascending: each right-side split leaves the left BIN
13926    /// near-full (all but one entry stays). Compared to midpoint split
13927    /// the number of BINs created should be significantly fewer relative to
13928    /// keys inserted (more keys per BIN on average).
13929    ///
13930    /// JE criterion: `allRightSideDescent` → `splitIndex = nEntries - 1`.
13931    /// The penultimate entry stays in the left BIN; only one entry goes to
13932    /// the new right sibling, which then absorbs the next insert and fills
13933    /// normally.
13934    #[test]
13935    fn test_split_special_ascending_fewer_bins_than_midpoint() {
13936        let max_entries = 8usize;
13937        let n_keys = 200usize;
13938
13939        // Build tree with splitSpecial (ascending keys trigger AllRight).
13940        let tree_special = Tree::new(1, max_entries);
13941        let lsn = noxu_util::Lsn::new(1, 100);
13942        for i in 0u32..n_keys as u32 {
13943            let key = i.to_be_bytes().to_vec();
13944            tree_special.insert(key, vec![0u8], lsn).expect("insert");
13945        }
13946
13947        let root_special = tree_special.get_root().expect("root must exist");
13948        let bins_special = count_bins(&root_special);
13949        let keys_special = count_keys(&root_special);
13950
13951        // All keys must be present.
13952        assert_eq!(keys_special, n_keys, "all keys must be stored");
13953
13954        // With splitSpecial, each right-side split keeps n-1 entries in the
13955        // left BIN. Ideal: ceil(n_keys / (max_entries - 1)) BINs.
13956        // Without splitSpecial (midpoint): ceil(n_keys / (max_entries / 2)).
13957        // We assert the actual count is below the midpoint-split upper bound.
13958        let midpoint_upper_bound = n_keys.div_ceil(max_entries / 2);
13959        assert!(
13960            bins_special < midpoint_upper_bound,
13961            "splitSpecial should produce fewer BINs than midpoint split: \
13962             got {bins_special}, midpoint upper bound = {midpoint_upper_bound}"
13963        );
13964
13965        // The rightmost BIN must have fewer entries than max_entries
13966        // (the last insert only half-fills it at most), which is expected.
13967        // The IMPORTANT property: rightmost BIN started with exactly 1 entry
13968        // (its first entry was the split-off singleton) then filled up.
13969        // We just verify overall key density > midpoint baseline.
13970        let avg_fill = keys_special as f64 / bins_special as f64;
13971        let midpoint_fill = (max_entries / 2) as f64;
13972        assert!(
13973            avg_fill > midpoint_fill,
13974            "average fill per BIN with splitSpecial ({avg_fill:.1}) should \
13975             exceed midpoint baseline ({midpoint_fill})"
13976        );
13977    }
13978
13979    /// `splitSpecial` descending: all routing decisions are at slot 0
13980    /// (`AllLeft`). Split forces `split_index = 1` so the right sibling
13981    /// gets almost all entries and the left node keeps just one.
13982    ///
13983    /// JE criterion: `allLeftSideDescent` → `splitIndex = 1`.
13984    #[test]
13985    fn test_split_special_descending_fewer_bins_than_midpoint() {
13986        let max_entries = 8usize;
13987        let n_keys = 200usize;
13988
13989        let tree_special = Tree::new(1, max_entries);
13990        let lsn = noxu_util::Lsn::new(1, 100);
13991        for i in (0u32..n_keys as u32).rev() {
13992            let key = i.to_be_bytes().to_vec();
13993            tree_special.insert(key, vec![0u8], lsn).expect("insert");
13994        }
13995
13996        let root_special = tree_special.get_root().expect("root must exist");
13997        let bins_special = count_bins(&root_special);
13998        let keys_special = count_keys(&root_special);
13999
14000        assert_eq!(keys_special, n_keys, "all keys must be stored");
14001
14002        let midpoint_upper_bound = n_keys.div_ceil(max_entries / 2);
14003        assert!(
14004            bins_special < midpoint_upper_bound,
14005            "splitSpecial descending should produce fewer BINs: \
14006             got {bins_special}, midpoint upper bound = {midpoint_upper_bound}"
14007        );
14008    }
14009
14010    /// Random-key inserts must NOT be affected by splitSpecial: with random
14011    /// keys descent will rarely be all-left or all-right, so the split index
14012    /// defaults to midpoint and tree balance is maintained.
14013    #[test]
14014    fn test_split_special_random_inserts_stay_balanced() {
14015        use std::collections::BTreeSet;
14016
14017        let max_entries = 8usize;
14018        // Use a fixed permutation so the test is deterministic.
14019        let mut keys: Vec<u32> = (0u32..200).collect();
14020        // Knuth shuffle with a fixed seed.
14021        let mut rng: u64 = 0xdeadbeef_cafebabe;
14022        for i in (1..keys.len()).rev() {
14023            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
14024            let j = (rng >> 33) as usize % (i + 1);
14025            keys.swap(i, j);
14026        }
14027
14028        let tree = Tree::new(1, max_entries);
14029        let lsn = noxu_util::Lsn::new(1, 100);
14030        let mut inserted = BTreeSet::new();
14031        for k in &keys {
14032            let key = k.to_be_bytes().to_vec();
14033            tree.insert(key, vec![0u8], lsn).expect("insert");
14034            inserted.insert(*k);
14035        }
14036
14037        let root = tree.get_root().expect("root");
14038        let total_keys = count_keys(&root);
14039        assert_eq!(
14040            total_keys,
14041            inserted.len(),
14042            "all random keys must be stored"
14043        );
14044
14045        // Verify every key is findable.
14046        for k in &inserted {
14047            let key = k.to_be_bytes().to_vec();
14048            let found = tree.search(&key);
14049            assert!(
14050                found.map(|r| r.is_exact_match()).unwrap_or(false),
14051                "random key {k} must be findable after insert"
14052            );
14053        }
14054    }
14055
14056    /// TREE-F1: a `known_deleted` BIN slot must read as ABSENT on an exact
14057    /// lookup and must be SKIPPED by scans, matching JE.
14058    ///
14059    /// JE contract:
14060    /// * `IN.findEntry` (IN.java:3197): an exact match that lands on a
14061    ///   known-deleted slot returns -1 (ABSENT).
14062    /// * `CursorImpl.lockAndGetCurrent` (CursorImpl.java:2062-2064): a
14063    ///   step that lands on `isEntryKnownDeleted(index)` returns null, so
14064    ///   the `getNext` loop advances past it (the slot is skipped).
14065    ///
14066    /// KD slots legitimately exist in live BINs during BIN-delta
14067    /// reconstitution (`mutate_to_full_bin` applies delta KD slots) until
14068    /// the compressor reclaims them.  We reach that state directly here by
14069    /// marking a slot known_deleted in the BIN arc, then assert the
14070    /// user-facing read/scan paths do not surface it.
14071    #[test]
14072    fn test_tree_f1_known_deleted_slot_is_absent_and_skipped() {
14073        let tree = Tree::new(1, 8);
14074        // Insert enough keys to populate a BIN with several live slots.
14075        for i in 0..6u32 {
14076            let key = format!("kd{i:04}").into_bytes();
14077            tree.insert(key, vec![i as u8], Lsn::new(1, i)).unwrap();
14078        }
14079
14080        // Pick a middle key and mark its slot known_deleted directly in the
14081        // BIN, modelling a delta-applied tombstone the compressor has not yet
14082        // reclaimed.
14083        let kd_key = b"kd0003".to_vec();
14084        {
14085            let root = tree.get_root().expect("root");
14086            let bin_arc = find_bin_arc_for_key(&root, &kd_key).expect("bin");
14087            let mut g = bin_arc.write();
14088            if let TreeNode::Bottom(b) = &mut *g {
14089                let idx = (0..b.entries.len())
14090                    .find(|&i| {
14091                        b.get_full_key(i).as_deref() == Some(kd_key.as_slice())
14092                    })
14093                    .expect("kd key slot");
14094                b.entries[idx].known_deleted = true;
14095            } else {
14096                panic!("expected BIN");
14097            }
14098        }
14099
14100        // (a) exact lookup via Tree::search must report NOT found.
14101        let sr = tree.search(&kd_key);
14102        assert!(
14103            !sr.map(|r| r.is_exact_match()).unwrap_or(false),
14104            "TREE-F1: Tree::search must report a known_deleted slot as absent \
14105             (IN.findEntry IN.java:3197)"
14106        );
14107
14108        // (a) exact lookup via Tree::search_with_data must report NOT found.
14109        let sf = tree.search_with_data(&kd_key).expect("slot fetch");
14110        assert!(
14111            !sf.found,
14112            "TREE-F1: Tree::search_with_data must report a known_deleted slot \
14113             as absent (IN.findEntry IN.java:3197)"
14114        );
14115
14116        // Live neighbours must still be found.
14117        for live in [b"kd0002".to_vec(), b"kd0004".to_vec()] {
14118            assert!(
14119                tree.search(&live).map(|r| r.is_exact_match()).unwrap_or(false),
14120                "live neighbour must remain findable"
14121            );
14122        }
14123
14124        // (b) a scan-facing BIN dump (descend_to_edge_bin / get_next_bin /
14125        // get_prev_bin) returns slots verbatim WITH the known_deleted flag
14126        // set, so the cursor can skip them (CursorImpl.java:2062-2064).  The
14127        // contract here is: the KD slot is never reported as a LIVE entry.
14128        let root = tree.get_root().expect("root");
14129        let edge = Tree::descend_to_edge_bin(&root, true).expect("edge bin");
14130        assert!(
14131            !edge.iter().any(|(e, _, k)| k == &kd_key && !e.known_deleted),
14132            "TREE-F1: scan must not surface a known_deleted slot as live \
14133             (CursorImpl.java:2062-2064)"
14134        );
14135        for anchor in [b"kd0000".to_vec(), b"kd0005".to_vec()] {
14136            for entries in
14137                [tree.get_next_bin(&anchor), tree.get_prev_bin(&anchor)]
14138                    .into_iter()
14139                    .flatten()
14140            {
14141                assert!(
14142                    !entries
14143                        .iter()
14144                        .any(|(e, _, k)| k == &kd_key && !e.known_deleted),
14145                    "TREE-F1: get_next_bin/get_prev_bin must not surface a \
14146                     known_deleted slot as live"
14147                );
14148            }
14149        }
14150
14151        // first_entry_at_or_after must skip a KD slot at the boundary.
14152        if let Some((k, _, _)) = tree.first_entry_at_or_after(&kd_key) {
14153            assert_ne!(
14154                k, kd_key,
14155                "TREE-F1: first_entry_at_or_after must skip a known_deleted \
14156                 slot (CursorImpl.java:2062-2064)"
14157            );
14158        }
14159
14160        // The compressor KD-iteration path must STILL see the slot — the fix
14161        // only changes the user-facing read predicate, not the maintenance
14162        // iteration that exists to reclaim KD slots.
14163        let kd_bins = tree.collect_bins_with_known_deleted();
14164        assert!(
14165            !kd_bins.is_empty(),
14166            "TREE-F1: collect_bins_with_known_deleted must still observe the \
14167             KD slot so the compressor can reclaim it"
14168        );
14169    }
14170}