Skip to main content

lsm_tree/tree/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-present, fjall-rs
3// Copyright (c) 2026-present, Dmitry Prudnikov
4
5#[cfg(feature = "columnar")]
6pub mod columnar_scan;
7pub mod ingest;
8pub mod inner;
9pub mod sealed;
10
11use crate::path::Path;
12use crate::{
13    AbstractTree, Checksum, KvPair, SeqNo, SequenceNumberCounter, TableId, UserKey, UserValue,
14    ValueType,
15    compaction::{CompactionStrategy, drop_range::OwnedBounds, state::CompactionState},
16    config::Config,
17    format_version::FormatVersion,
18    fs::Fs,
19    iter_guard::{IterGuard, IterGuardImpl},
20    key::InternalKey,
21    manifest::Manifest,
22    memtable::Memtable,
23    range_tombstone::RangeTombstone,
24    scan_since::ScanSinceEvent,
25    slice::Slice,
26    table::Table,
27    value::InternalValue,
28    version::{SuperVersion, SuperVersions, Version, recovery::recover},
29    vlog::BlobFile,
30};
31use alloc::sync::Arc;
32#[cfg(not(feature = "std"))]
33use alloc::{boxed::Box, string::ToString, vec::Vec};
34use core::ops::{Bound, RangeBounds};
35use inner::{FlushGuard, TreeId, TreeInner, VersionsWriteGuard};
36// no-std: spin mirrors parking_lot's Mutex/RwLock API without an allocator.
37// parking_lot wins on the std hot path, so keep it for std.
38#[cfg(feature = "std")]
39use parking_lot::{Mutex, RwLock};
40#[cfg(not(feature = "std"))]
41use spin::{Mutex, RwLock};
42
43#[cfg(feature = "metrics")]
44use crate::metrics::Metrics;
45
46/// Floor for the storage-admission reserved headroom band (see
47/// [`Tree::compute_write_admission`]). Even with an empty active memtable the
48/// gate keeps at least this much room below the budget so the next writes and a
49/// space-reclaiming compaction have somewhere to land. 1 MiB.
50pub const MIN_RESERVED_HEADROOM: u64 = 1024 * 1024;
51
52/// How long a cached disk-free sample stays valid before the admission gate
53/// re-probes. Bounds how stale the physical free-space figure can be when the
54/// filesystem fills from another process between flushes, without issuing a
55/// `statfs`/`statvfs` syscall on every gated write. 1 second.
56const ADMISSION_DISK_FREE_TTL: core::time::Duration = core::time::Duration::from_secs(1);
57
58/// Iterator value guard
59pub struct Guard(crate::Result<(UserKey, UserValue)>);
60
61impl IterGuard for Guard {
62    fn into_inner_if(
63        self,
64        pred: impl Fn(&UserKey) -> bool,
65    ) -> crate::Result<(UserKey, Option<UserValue>)> {
66        let (k, v) = self.0?;
67
68        if pred(&k) {
69            Ok((k, Some(v)))
70        } else {
71            Ok((k, None))
72        }
73    }
74
75    fn key(self) -> crate::Result<UserKey> {
76        self.0.map(|(k, _)| k)
77    }
78
79    fn size(self) -> crate::Result<u32> {
80        #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")]
81        self.into_inner().map(|(_, v)| v.len() as u32)
82    }
83
84    fn into_inner(self) -> crate::Result<(UserKey, UserValue)> {
85        self.0
86    }
87}
88
89/// Trait for monomorphized table point-read results.
90///
91/// Allows `find_in_tables` to operate generically over `InternalValue` (for
92/// `get`) and `(InternalValue, Block)` (for `get_pinned`), generating optimal
93/// code for each path without runtime dispatch or extra refcount overhead.
94trait TablePointLookup: Sized {
95    fn lookup(
96        table: &Table,
97        key: &[u8],
98        seqno: SeqNo,
99        key_hash: u64,
100    ) -> crate::Result<Option<Self>>;
101    fn entry_seqno(&self) -> SeqNo;
102    fn filter_tombstone(self) -> Option<Self>;
103}
104
105/// Lookup result for standard `get()` — entry only, no block retained.
106type TableEntry = InternalValue;
107
108/// One covered key in a batched run resolution: `(input index, key hash,
109/// resolved item)`. Aliased to keep `resolve_run_batched`'s return readable.
110type CoveredKey = (usize, u64, Option<InternalValue>);
111
112/// `(miss_keys, duplicates)` from [`Tree::dedup_sorted_miss_keys`]: `miss_keys`
113/// is `(key_index, bloom_hash)` for the strictly-sorted-unique batched resolver,
114/// `duplicates` is `(duplicate_index, representative_index)` for the fan-out.
115type DedupedMissKeys = (Vec<(usize, u64)>, Vec<(usize, usize)>);
116
117/// The outcome of resolving a key batch against one run (see `resolve_run_batched`).
118struct RunResolve {
119    /// Covered, non-skipped keys with their resolved item, in input order.
120    covered: Vec<CoveredKey>,
121    /// Keys this run does not cover, in input order, for the next run or level.
122    not_covered: Vec<(usize, u64)>,
123}
124
125/// One data block the chunked `multi_get` resolver will read (see
126/// `resolve_level_chunked`): the block, the SST it lives in (`table` + its `file`
127/// handle), the table-local read seqno, whether it needs the special load path
128/// (Page-ECC / columnar), and the ORIGINAL key indices that fall in this block.
129struct BlockTask<'a> {
130    table: &'a crate::Table,
131    file: Arc<dyn crate::fs::FsFile>,
132    handle: crate::table::BlockHandle,
133    table_seqno: SeqNo,
134    special: bool,
135    keys: Vec<usize>,
136}
137
138impl TablePointLookup for TableEntry {
139    fn lookup(
140        table: &Table,
141        key: &[u8],
142        seqno: SeqNo,
143        key_hash: u64,
144    ) -> crate::Result<Option<Self>> {
145        table.get(key, seqno, key_hash)
146    }
147
148    fn entry_seqno(&self) -> SeqNo {
149        self.key.seqno
150    }
151
152    fn filter_tombstone(self) -> Option<Self> {
153        ignore_tombstone_value(self)
154    }
155}
156
157/// Lookup result for `get_pinned()` — entry + block for zero-copy pinning.
158type TableEntryWithBlock = (InternalValue, crate::table::Block);
159
160impl TablePointLookup for TableEntryWithBlock {
161    fn lookup(
162        table: &Table,
163        key: &[u8],
164        seqno: SeqNo,
165        key_hash: u64,
166    ) -> crate::Result<Option<Self>> {
167        table.get_with_block(key, seqno, key_hash)
168    }
169
170    fn entry_seqno(&self) -> SeqNo {
171        self.0.key.seqno
172    }
173
174    fn filter_tombstone(self) -> Option<Self> {
175        ignore_tombstone_value(self.0).map(|iv| (iv, self.1))
176    }
177}
178
179/// Lookup result for the value-returning `get()` path: `(value_type, seqno,
180/// value)`, no key reconstruction (the caller has the needle).
181type TableValue = (ValueType, SeqNo, crate::Slice);
182
183impl TablePointLookup for TableValue {
184    fn lookup(
185        table: &Table,
186        key: &[u8],
187        seqno: SeqNo,
188        key_hash: u64,
189    ) -> crate::Result<Option<Self>> {
190        table.get_value(key, seqno, key_hash)
191    }
192
193    fn entry_seqno(&self) -> SeqNo {
194        self.1
195    }
196
197    fn filter_tombstone(self) -> Option<Self> {
198        if self.0.is_tombstone() {
199            None
200        } else {
201            Some(self)
202        }
203    }
204}
205
206fn ignore_tombstone_value(item: InternalValue) -> Option<InternalValue> {
207    if item.is_tombstone() {
208        None
209    } else {
210        Some(item)
211    }
212}
213
214/// A log-structured merge tree (LSM-tree/LSMT)
215#[derive(Clone)]
216pub struct Tree(#[doc(hidden)] pub Arc<TreeInner>);
217
218impl core::ops::Deref for Tree {
219    type Target = TreeInner;
220
221    fn deref(&self) -> &Self::Target {
222        &self.0
223    }
224}
225
226impl crate::abstract_tree::sealed::Sealed for Tree {}
227
228/// Maps a raw merge-pipeline item into a standard-tree iterator guard.
229fn standard_guard(item: crate::Result<InternalValue>) -> IterGuardImpl {
230    IterGuardImpl::Standard(Guard(item.map(|iv| (iv.key.user_key, iv.value))))
231}
232
233/// A guard carrying only an error: the single item an iterator surface yields
234/// when it fails before it can open a snapshot (no row, no version to bind a
235/// blob guard to), so the failure reaches the consumer through the same
236/// `Result` it already handles per row.
237#[expect(
238    clippy::redundant_pub_crate,
239    reason = "reached from blob_tree as crate::tree::error_guard"
240)]
241pub(crate) fn error_guard(e: crate::Error) -> IterGuardImpl {
242    IterGuardImpl::Standard(Guard(Err(e)))
243}
244
245/// Extract owned user-key bounds from any range.
246#[expect(
247    clippy::redundant_pub_crate,
248    reason = "reached from blob_tree as crate::tree::range_to_user_bounds"
249)]
250pub(crate) fn range_to_user_bounds<K: AsRef<[u8]>, R: RangeBounds<K>>(
251    range: &R,
252) -> (Bound<UserKey>, Bound<UserKey>) {
253    use core::ops::Bound::{Excluded, Included, Unbounded};
254    let lo = match range.start_bound() {
255        Included(x) => Included(x.as_ref().into()),
256        Excluded(x) => Excluded(x.as_ref().into()),
257        Unbounded => Unbounded,
258    };
259    let hi = match range.end_bound() {
260        Included(x) => Included(x.as_ref().into()),
261        Excluded(x) => Excluded(x.as_ref().into()),
262        Unbounded => Unbounded,
263    };
264    (lo, hi)
265}
266
267/// Wraps a [`SeekableTreeIter`](crate::range::SeekableTreeIter) so a standard
268/// tree can expose it as a [`SeekableGuardIter`](crate::iter_guard::SeekableGuardIter).
269struct StandardSeekable {
270    inner: crate::range::SeekableTreeIter,
271}
272
273impl Iterator for StandardSeekable {
274    type Item = IterGuardImpl;
275
276    fn next(&mut self) -> Option<Self::Item> {
277        self.inner.next().map(standard_guard)
278    }
279}
280
281impl DoubleEndedIterator for StandardSeekable {
282    fn next_back(&mut self) -> Option<Self::Item> {
283        self.inner.next_back().map(standard_guard)
284    }
285}
286
287impl crate::iter_guard::SeekableGuardIter for StandardSeekable {
288    fn seek_to(&mut self, key: &[u8]) {
289        self.inner.seek_to(key);
290    }
291
292    fn seek_to_for_prev(&mut self, key: &[u8]) {
293        self.inner.seek_to_for_prev(key);
294    }
295
296    fn peek_key(&mut self) -> Option<crate::Result<crate::UserKey>> {
297        self.inner.peek_key()
298    }
299}
300
301impl AbstractTree for Tree {
302    fn table_file_cache_size(&self) -> usize {
303        self.config
304            .descriptor_table
305            .as_ref()
306            .map_or(0, |dt| dt.len())
307    }
308
309    fn get_version_history_lock(&self) -> VersionsWriteGuard<'_> {
310        self.version_history.write()
311    }
312
313    fn next_table_id(&self) -> TableId {
314        self.0.table_id_counter.get()
315    }
316
317    fn id(&self) -> TreeId {
318        self.id
319    }
320
321    fn blob_file_count(&self) -> usize {
322        0
323    }
324
325    #[cfg(feature = "std")]
326    fn create_checkpoint(
327        &self,
328        target_path: &crate::path::Path,
329    ) -> crate::Result<crate::CheckpointInfo> {
330        crate::checkpoint::run_checkpoint(
331            self,
332            &crate::checkpoint::CheckpointParams {
333                target_root: target_path,
334                target_fs: &self.config.fs,
335                src_root: &self.config.path,
336                src_fs: &self.config.fs,
337                deletion_pause: &self.deletion_pause,
338                visible_seqno: &self.config.visible_seqno,
339                include_blobs: false,
340                runtime_config: self.0.runtime_config.load_full(),
341                encryption: self.0.config.encryption.clone(),
342            },
343        )
344    }
345
346    fn print_trace(&self, key: &[u8]) -> crate::Result<()> {
347        let super_version = self.version_history.read().latest_version();
348
349        let key = Slice::from(key);
350
351        for kv in super_version.active_memtable.range_internal((
352            Bound::Included(InternalKey::new(key.clone(), SeqNo::MAX, ValueType::Value)),
353            Bound::Unbounded,
354        )) {
355            log::info!("[Active] {kv:?}");
356        }
357
358        for mt in super_version.sealed_memtables.iter().rev() {
359            for kv in mt.range_internal((
360                Bound::Included(InternalKey::new(key.clone(), SeqNo::MAX, ValueType::Value)),
361                Bound::Unbounded,
362            )) {
363                log::info!("[Sealed #{}] {kv:?}", mt.id());
364            }
365        }
366
367        for table in super_version
368            .version
369            .iter_levels()
370            .flat_map(|lvl| lvl.iter())
371            .filter_map(|run| run.get_for_key_cmp(&key, self.config.comparator.as_ref()))
372        {
373            for kv in table.range(..) {
374                let kv = kv?;
375
376                if kv.key.user_key != key {
377                    break;
378                }
379
380                log::info!("[Table #{}] {kv:?}", table.id());
381            }
382        }
383
384        Ok(())
385    }
386
387    fn get_internal_entry(&self, key: &[u8], seqno: SeqNo) -> crate::Result<Option<InternalValue>> {
388        let super_version = self.snapshot_for_read(seqno)?;
389
390        Self::get_internal_entry_from_version(
391            &super_version,
392            key,
393            seqno,
394            self.config.comparator.as_ref(),
395        )
396    }
397
398    fn current_version(&self) -> Version {
399        self.version_history
400            .read()
401            .latest_version_ref()
402            .version
403            .clone()
404    }
405
406    #[cfg(feature = "std")]
407    fn refresh_table_checksum(
408        &self,
409        table_id: TableId,
410        checksum: crate::checksum::Checksum,
411        expected_restriction: Option<&crate::UserKey>,
412    ) -> crate::Result<crate::abstract_tree::ChecksumRefreshOutcome> {
413        use crate::abstract_tree::ChecksumRefreshOutcome;
414        // Same lock order as flush / compaction version installs: compaction
415        // state first, then the version history write lock. But the caller (the
416        // patrol reconcile) holds this table's HEAL LOCK across this call, and a
417        // concurrent tight-space compaction acquires that heal lock WHILE holding
418        // `compaction_state`. Blocking on `compaction_state` here would invert the
419        // order (heal_lock -> compaction_state on this path vs
420        // compaction_state -> heal_lock on the compaction path) and deadlock
421        // permanently. `try_lock` instead: a failed acquire means a compaction is
422        // mid-install, so skip this refresh — but report the skip as CONTENDED,
423        // not as a benign no-op: the healed bytes are durable while the manifest
424        // digest stays stale, so a "clean" report would mislead a later
425        // integrity check / checkpoint. The caller keeps the attestation and
426        // surfaces a finding; the next patrol retries once the compaction
427        // releases the state.
428        let Some(mut _compaction_state) = self.compaction_state.try_lock() else {
429            return Ok(ChecksumRefreshOutcome::Contended);
430        };
431        let mut version_lock = self.version_history.write();
432
433        // Under the install lock, resolve the CURRENT view of this table and
434        // reject the refresh if either it is gone (compacted away — nothing to
435        // refresh) or its restriction no longer matches the one `checksum` was
436        // computed for. A tight-space compaction can swap the captured view for a
437        // restricted same-id view (punching its prefix) between the caller's read
438        // and this lock; installing the caller's digest against a different
439        // restriction would record one the punched file can never match. Skipping
440        // leaves the current view's own (compaction-installed) digest in place for
441        // the next patrol to reconcile.
442        let restriction_matches = version_lock
443            .latest_version_ref()
444            .version
445            .iter_tables()
446            .find(|t| t.id() == table_id)
447            .is_some_and(|t| t.restrict_lower_bound() == expected_restriction);
448        if !restriction_matches {
449            // No-op: the manifest digest is unchanged, so the caller must keep the
450            // attestation for the next patrol.
451            return Ok(ChecksumRefreshOutcome::Stale);
452        }
453
454        version_lock
455            .upgrade_version(
456                &self.config.path,
457                |current| {
458                    let mut copy = current.clone();
459                    if let Some(next) = copy
460                        .version
461                        .with_refreshed_table_checksum(table_id, checksum)
462                    {
463                        copy.version = next;
464                    }
465                    Ok(copy)
466                },
467                &self.config.seqno,
468                &self.config.visible_seqno,
469                &*self.config.fs,
470                self.0.runtime_config.load_full(),
471                self.0.config.encryption.clone(),
472                crate::version::RetentionEffect::Keep,
473            )
474            .map(|()| ChecksumRefreshOutcome::Refreshed)
475    }
476
477    fn sync_mode(&self) -> crate::fs::SyncMode {
478        self.config.sync_mode
479    }
480
481    fn prefix_extractor(&self) -> Option<alloc::sync::Arc<dyn crate::prefix::PrefixExtractor>> {
482        self.config.prefix_extractor.clone()
483    }
484
485    fn storage_stats(&self) -> crate::Result<crate::StorageStats> {
486        // One version snapshot reused for the footprint and the full-compaction
487        // estimate below: a second `current_version()` could race a concurrent
488        // flush / compaction and mix two snapshots.
489        let version = self.current_version();
490        // Standard tree: SST values ARE user values (no KV separation).
491        let mut stats =
492            crate::storage_stats::compute_storage_stats(&version, self.is_compacting(), true)?;
493        // Fill the disk-aware capacity figures (quota + free-space probe) the
494        // version-only computation can't know.
495        let (capacity, available, compaction_possible) = self.admission_capacity(stats.used_bytes);
496        stats.capacity_bytes = capacity;
497        stats.available_bytes = available;
498        stats.compaction_possible = compaction_possible;
499        // When admission gating is active and a compaction is not already
500        // running, surface whether a full compaction has working room through the
501        // SAME two-layer check the compaction space gate enforces (logical quota +
502        // physical free per destination volume), so the reported status matches
503        // what the gate will admit. With gating off the gate never runs, so the
504        // status stays `Healthy` even though the backend can report a finite
505        // capacity.
506        if self.storage_admission_enabled()
507            && capacity.is_some()
508            && stats.status == crate::StorageStatus::Healthy
509        {
510            // A full compaction's transient output is bounded by the largest
511            // level's on-disk size, but it LANDS in the last configured level's
512            // volume (`level_count - 1`), which under tiered routing can be a
513            // different filesystem than the largest level. A standard tree has no
514            // blob relocation. Using the per-volume gate (not `available >=
515            // full_compaction_bytes` against the min-volume free) keeps the status
516            // from reporting tight when a routed merge would actually be admitted.
517            let sst_need = crate::storage_stats::full_compaction_demand_bytes(&version)?;
518            // `saturating_sub`: `level_count >= 1` always, so this is the last
519            // level index; the clamp only guards a degenerate zero-level config.
520            let sst_dest_level = self.0.config.level_count.saturating_sub(1);
521            let quota_headroom = self.quota_headroom(stats.used_bytes);
522            let full_fits = crate::compaction::worker::space_fits_two_layer(
523                &self.0.config,
524                quota_headroom,
525                sst_need,
526                sst_dest_level,
527                0,
528            );
529            stats.status = if full_fits {
530                crate::StorageStatus::FullCompactionAvailable
531            } else {
532                crate::StorageStatus::TightCompactionAvailable
533            };
534        }
535        // A closed admission gate is the operator-actionable state, so it takes
536        // precedence over the others (a read-only tree may well be compacting to
537        // reclaim space).
538        if self.is_read_only() {
539            stats.status = crate::StorageStatus::ReadOnlyOutOfSpace;
540        }
541        Ok(stats)
542    }
543
544    fn write_admission(&self) -> crate::Result<()> {
545        self.compute_write_admission()
546    }
547
548    fn write_backpressure(
549        &self,
550        strategy: &dyn crate::compaction::CompactionStrategy,
551    ) -> crate::Backpressure {
552        // Copy the thresholds out (BackpressureThresholds is Copy) so the
553        // arc-swap guard drops immediately; the off check short-circuits before
554        // touching the version, keeping the disabled path free.
555        let thresholds = self.0.runtime_config.load().backpressure;
556        if thresholds.is_off() {
557            return crate::Backpressure::None;
558        }
559        let version = self.current_version();
560        // L0 is the first level; its table (file) count is the count-trigger
561        // signal, matching the leveled `choose` trigger and the L0 term of
562        // `pending_compaction_bytes`.
563        let l0_count = version
564            .iter_levels()
565            .next()
566            .map_or(0, |level| level.table_count());
567        let pending = strategy.pending_compaction_bytes(&version);
568        crate::Backpressure::compute(l0_count, pending, &thresholds)
569    }
570
571    fn get_flush_lock(&self) -> FlushGuard<'_> {
572        self.flush_lock.lock()
573    }
574
575    #[cfg(feature = "metrics")]
576    fn metrics(&self) -> &Arc<crate::Metrics> {
577        &self.0.metrics
578    }
579
580    #[cfg(feature = "metrics")]
581    fn cache_stats(&self) -> crate::CacheStats {
582        let cache = &self.0.config.cache;
583        self.metrics().cache_stats(cache.size(), cache.capacity())
584    }
585
586    fn version_free_list_len(&self) -> usize {
587        self.version_history.read().free_list_len()
588    }
589
590    fn prefix<K: AsRef<[u8]>>(
591        &self,
592        prefix: K,
593        seqno: SeqNo,
594        index: Option<(Arc<Memtable>, SeqNo)>,
595    ) -> Box<dyn DoubleEndedIterator<Item = IterGuardImpl> + Send + 'static> {
596        match self.create_prefix(&prefix, seqno, index) {
597            Ok(iter) => Box::new(iter.map(|kv| IterGuardImpl::Standard(Guard(kv)))),
598            Err(e) => Box::new(core::iter::once(error_guard(e))),
599        }
600    }
601
602    fn range<K: AsRef<[u8]>, R: RangeBounds<K>>(
603        &self,
604        range: R,
605        seqno: SeqNo,
606        index: Option<(Arc<Memtable>, SeqNo)>,
607    ) -> Box<dyn DoubleEndedIterator<Item = IterGuardImpl> + Send + 'static> {
608        match self.create_range(&range, seqno, index) {
609            Ok(iter) => Box::new(iter.map(|kv| IterGuardImpl::Standard(Guard(kv)))),
610            Err(e) => Box::new(core::iter::once(error_guard(e))),
611        }
612    }
613
614    fn range_seekable<K: AsRef<[u8]>, R: RangeBounds<K>>(
615        &self,
616        range: R,
617        seqno: SeqNo,
618        index: Option<(Arc<Memtable>, SeqNo)>,
619    ) -> Box<dyn crate::iter_guard::SeekableGuardIter + 'static> {
620        let (lo, hi) = range_to_user_bounds(&range);
621        match self.create_seekable_range_bounds(lo, hi, seqno, index) {
622            Ok(inner) => Box::new(StandardSeekable { inner }),
623            Err(e) => Box::new(crate::iter_guard::FailedSeekable::new(e)),
624        }
625    }
626
627    fn batch_range_scan<K: AsRef<[u8]>, R: RangeBounds<K> + 'static, I: IntoIterator<Item = R>>(
628        &self,
629        intervals: I,
630        seqno: SeqNo,
631        index: Option<(Arc<Memtable>, SeqNo)>,
632    ) -> Box<dyn Iterator<Item = IterGuardImpl> + Send + 'static>
633    where
634        I::IntoIter: Send + 'static,
635    {
636        // Open the seekable iterator over the whole keyspace once; each interval
637        // is served by repositioning it (single per-SST setup, amortized).
638        let inner = match self.create_seekable_range_bounds(
639            Bound::Unbounded,
640            Bound::Unbounded,
641            seqno,
642            index,
643        ) {
644            Ok(inner) => inner,
645            Err(e) => return Box::new(core::iter::once(error_guard(e))),
646        };
647        let intervals = intervals.into_iter().map(|r| range_to_user_bounds(&r));
648        Box::new(crate::range::BatchRangeScan::new(inner, intervals).map(standard_guard))
649    }
650
651    /// Returns the number of tombstones in the tree.
652    fn tombstone_count(&self) -> u64 {
653        self.current_version()
654            .iter_tables()
655            .map(Table::tombstone_count)
656            .sum()
657    }
658
659    /// Returns the number of weak tombstones (single deletes) in the tree.
660    fn weak_tombstone_count(&self) -> u64 {
661        self.current_version()
662            .iter_tables()
663            .map(Table::weak_tombstone_count)
664            .sum()
665    }
666
667    /// Returns the number of value entries that become reclaimable once weak tombstones can be GC'd.
668    fn weak_tombstone_reclaimable_count(&self) -> u64 {
669        self.current_version()
670            .iter_tables()
671            .map(Table::weak_tombstone_reclaimable)
672            .sum()
673    }
674
675    fn drop_range<K: AsRef<[u8]>, R: RangeBounds<K>>(&self, range: R) -> crate::Result<()> {
676        let (bounds, is_empty) = Self::range_bounds_to_owned_bounds(&range);
677
678        if is_empty {
679            return Ok(());
680        }
681
682        let strategy = Arc::new(crate::compaction::drop_range::Strategy::new(bounds));
683
684        // IMPORTANT: Write lock so we can be the only compaction going on
685        let _lock = self.0.major_compaction_lock.write();
686
687        log::info!("Starting drop_range compaction");
688        self.inner_compact(strategy, 0)?;
689        Ok(())
690    }
691
692    fn clear(&self) -> crate::Result<()> {
693        let config = self.tree_config();
694        let mut versions = self.get_version_history_lock();
695
696        // Pre-clear snapshot: every table + blob file it references becomes
697        // garbage the moment the new empty version is installed.
698        let prior = versions.latest_version();
699
700        versions.upgrade_version(
701            &config.path,
702            |v| {
703                let mut copy = v.clone();
704                copy.active_memtable = Arc::new(Memtable::new(
705                    self.memtable_id_counter.next(),
706                    self.config.comparator.clone(),
707                ));
708                copy.sealed_memtables = Arc::default();
709                copy.version = Version::new(v.version.id() + 1, self.tree_type());
710                Ok(copy)
711            },
712            &config.seqno,
713            &config.visible_seqno,
714            &*config.fs,
715            self.0.runtime_config.load_full(),
716            self.0.config.encryption.clone(),
717            // Every table goes: no snapshot up to this install is servable
718            // after a reopen.
719            crate::version::RetentionEffect::DropsData,
720        )?;
721
722        // Release the history's hold on the now-obsolete versions; only the new
723        // empty version remains. `prior` still holds them, so nothing reaches
724        // refcount zero yet.
725        versions.drain_obsolete_to_latest();
726        drop(versions); // release the version-history lock before any fs work
727
728        // Mark every obsolete table / blob file deleted so the file is
729        // reclaimed (Inner::Drop) once its last reference is released. A
730        // concurrent reader still holding the pre-clear snapshot keeps its own
731        // clone alive, deferring physical deletion until it finishes — the
732        // version-history Arc refcount is the MVCC guard, so reclaim never
733        // races a live read. Tables with no other live reference are reclaimed
734        // as `prior` drops at the end of this call.
735        for table in prior.version.iter_tables() {
736            table.mark_as_deleted();
737        }
738        for blob_file in prior.version.blob_files.iter() {
739            blob_file.mark_as_deleted();
740        }
741
742        Ok(())
743    }
744
745    #[doc(hidden)]
746    fn major_compact(
747        &self,
748        target_size: u64,
749        seqno_threshold: SeqNo,
750    ) -> crate::Result<crate::compaction::CompactionResult> {
751        let strategy = Arc::new(crate::compaction::major::Strategy::new(target_size));
752
753        // IMPORTANT: Write lock so we can be the only compaction going on
754        let _lock = self.0.major_compaction_lock.write();
755
756        log::info!("Starting major compaction");
757        self.inner_compact(strategy, seqno_threshold)
758    }
759
760    fn l0_run_count(&self) -> usize {
761        self.current_version()
762            .level(0)
763            .map(|x| x.run_count())
764            .unwrap_or_default()
765    }
766
767    fn size_of<K: AsRef<[u8]>>(&self, key: K, seqno: SeqNo) -> crate::Result<Option<u32>> {
768        #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")]
769        Ok(self.get(key, seqno)?.map(|x| x.len() as u32))
770    }
771
772    fn filter_size(&self) -> u64 {
773        self.current_version()
774            .iter_tables()
775            .map(Table::filter_size)
776            .map(u64::from)
777            .sum()
778    }
779
780    fn pinned_filter_size(&self) -> usize {
781        self.current_version()
782            .iter_tables()
783            .map(Table::pinned_filter_size)
784            .sum()
785    }
786
787    fn pinned_block_index_size(&self) -> usize {
788        self.current_version()
789            .iter_tables()
790            .map(Table::pinned_block_index_size)
791            .sum()
792    }
793
794    fn sealed_memtable_count(&self) -> usize {
795        self.version_history
796            .read()
797            .latest_version()
798            .sealed_memtables
799            .len()
800    }
801
802    fn flush_to_tables_with_rt(
803        &self,
804        stream: impl Iterator<Item = crate::Result<InternalValue>>,
805        range_tombstones: Vec<crate::range_tombstone::RangeTombstone>,
806    ) -> crate::Result<Option<(Vec<Table>, Option<Vec<BlobFile>>)>> {
807        use crate::table::multi_writer::MultiWriter;
808        use crate::time::Instant;
809
810        let start = Instant::now();
811
812        let (folder, level_fs) = self.config.tables_folder_for_level(0);
813
814        let data_block_size = self.config.data_block_size_policy.get(0);
815
816        let data_block_restart_interval = self.config.data_block_restart_interval_policy.get(0);
817        let index_block_restart_interval = self.config.index_block_restart_interval_policy.get(0);
818
819        let data_block_compression = self.config.data_block_compression_policy.get(0);
820        let index_block_compression = self.config.index_block_compression_policy.get(0);
821
822        let data_block_hash_ratio = self.config.data_block_hash_ratio_policy.get(0);
823
824        let index_partitioning = self.config.index_block_partitioning_policy.get(0);
825        let filter_partitioning = self.config.filter_block_partitioning_policy.get(0);
826
827        // One runtime-config snapshot for the whole flush writer setup. The
828        // index spill threshold, `seqno_in_index`, and the per-KV checksum
829        // policy are all live (toggleable via `update_runtime_config`); reading
830        // `load_full()` per field could straddle a concurrent update and mix two
831        // snapshots into one SST. Compaction is the migration mechanism, so a
832        // toggle takes effect on the next flush / compaction.
833        let rc = self.0.runtime_config.load_full();
834
835        log::debug!(
836            "Flushing memtable(s) to {}, data_block_restart_interval={data_block_restart_interval}, index_block_restart_interval={index_block_restart_interval}, data_block_size={data_block_size}, data_block_compression={data_block_compression:?}, index_block_compression={index_block_compression:?}",
837            folder.display(),
838        );
839
840        let mut table_writer = MultiWriter::new(
841            folder.clone(),
842            self.table_id_counter.clone(),
843            64 * 1_024 * 1_024,
844            0,
845            level_fs.clone(),
846        )?
847        .set_comparator(self.config.comparator.clone())
848        .use_data_block_restart_interval(data_block_restart_interval)
849        .use_index_block_restart_interval(index_block_restart_interval)
850        .use_data_block_compression(data_block_compression)
851        .use_index_block_compression(index_block_compression)
852        .use_data_block_size(data_block_size)
853        .use_data_block_hash_ratio(data_block_hash_ratio)
854        .use_bloom_policy({
855            use crate::config::FilterPolicyEntry::{Bloom, None};
856            use crate::table::filter::BloomConstructionPolicy;
857
858            match self.config.filter_policy.get(0) {
859                Bloom(policy) => policy,
860                None => BloomConstructionPolicy::BitsPerKey(0.0),
861            }
862        });
863
864        if index_partitioning {
865            // Size-adaptive: single-level index for small SSTs (where pinning
866            // the whole index is cheap and a two-level lookup is pure overhead),
867            // spilling to a partitioned index only once the index grows past the
868            // threshold. Recovers the point-read cost of an unconditional
869            // two-level index on small/medium SSTs.
870            table_writer = table_writer.use_adaptive_index(rc.index_partition_spill_threshold);
871        }
872        if filter_partitioning {
873            table_writer = table_writer.use_partitioned_filter();
874        }
875
876        table_writer = table_writer.use_prefix_extractor(self.config.prefix_extractor.clone());
877        table_writer = table_writer.use_encryption(self.config.encryption.clone());
878        // ECC scheme from the live runtime snapshot (same as `seqno_in_index`
879        // / `kv_checksums` below), so a flush after a scheme change writes the
880        // SST with the current scheme rather than the startup one.
881        table_writer = table_writer.use_page_ecc(self.config.page_ecc, rc.ecc_scheme);
882        table_writer = table_writer.use_sync_mode(self.config.sync_mode);
883
884        table_writer = table_writer.use_seqno_in_index(rc.seqno_in_index);
885        table_writer = table_writer.use_zone_map(rc.zone_map);
886        table_writer = table_writer.use_columnar(rc.columnar);
887        table_writer = table_writer.use_disable_cow_on_sst(rc.disable_cow_on_sst_files);
888        // `Off` (default) emits no per-KV footer and leaves the data-block
889        // payload encoding unchanged (the V5 header carries a block_flags byte
890        // and the meta block a descriptor key regardless, so the on-disk bytes
891        // are not identical to a pre-V5 table).
892        table_writer = table_writer.use_kv_checksums(rc.kv_checksums, rc.kv_checksum_algo);
893        // Flush writes level 0; resolve that level's locator policy entry.
894        table_writer = table_writer.use_locator(self.config.locator_policy.get(0));
895
896        #[cfg(zstd_any)]
897        {
898            table_writer = table_writer.use_zstd_dictionary(self.config.zstd_dictionary.clone());
899        }
900
901        // Parallel block compression for the flush writer, on the same pool the
902        // compaction writers use. Engaged only when the per-block transform does
903        // real CPU work (a codec, encryption, or page ECC): with the identity
904        // transform the pipeline's owned buffer + queue hop per block buys
905        // nothing over the serial reusable-buffer path. Safe wherever the host
906        // runs this flush — even on a thread of an injected pool — because the
907        // pipeline's help-first drain executes queued jobs inline instead of
908        // waiting on a saturated pool (see `parallel_compressor`).
909        #[cfg(feature = "std")]
910        {
911            let transform_does_work = data_block_compression != crate::CompressionType::None
912                || self.config.encryption.is_some()
913                || self.config.page_ecc;
914            if transform_does_work {
915                table_writer = table_writer.use_parallel_compression(
916                    self.config.compaction_pool.clone(),
917                    self.config.compaction_threads,
918                );
919            }
920        }
921
922        // Set range tombstones BEFORE writing KV items so that if MultiWriter
923        // rotates to a new table during the write loop, earlier tables already
924        // carry the RT metadata.
925        table_writer.set_range_tombstones(range_tombstones);
926
927        for item in stream {
928            table_writer.write(item?)?;
929        }
930
931        let result = table_writer.finish()?;
932
933        log::debug!("Flushed memtable(s) in {:?}", start.elapsed());
934
935        let pin_filter = self.config.filter_block_pinning_policy.get(0);
936        let pin_index = self.config.index_block_pinning_policy.get(0);
937
938        // Load tables
939        let tables = result
940            .into_iter()
941            .map(|(table_id, checksum)| -> crate::Result<Table> {
942                let mut params = crate::table::RecoverParams::new(
943                    folder.join(table_id.to_string()),
944                    checksum,
945                    table_id,
946                    level_fs.clone(),
947                    self.config.comparator.clone(),
948                    self.config.cache.clone(),
949                );
950                params.tree_id = self.id;
951                params
952                    .descriptor_table
953                    .clone_from(&self.config.descriptor_table);
954                params.pin_filter = pin_filter;
955                params.pin_index = pin_index;
956                params.encryption.clone_from(&self.config.encryption);
957                #[cfg(zstd_any)]
958                {
959                    params
960                        .zstd_dictionary
961                        .clone_from(&self.config.zstd_dictionary);
962                }
963                #[cfg(feature = "metrics")]
964                {
965                    params.metrics = self.metrics.clone();
966                }
967                Table::recover(params)
968            })
969            .collect::<crate::Result<Vec<_>>>()?;
970
971        // Return Some even when tables is empty (RT-only flush): the caller
972        // (AbstractTree::flush) handles empty tables by re-inserting RTs into
973        // the active memtable and still needs to delete sealed memtables.
974        Ok(Some((tables, None)))
975    }
976
977    #[expect(clippy::significant_drop_tightening)]
978    fn register_tables(
979        &self,
980        tables: &[Table],
981        blob_files: Option<&[BlobFile]>,
982        frag_map: Option<crate::blob_tree::FragmentationMap>,
983        sealed_memtables_to_delete: &[crate::tree::inner::MemtableId],
984        gc_watermark: SeqNo,
985    ) -> crate::Result<()> {
986        log::trace!(
987            "Registering {} tables, {} blob files",
988            tables.len(),
989            blob_files.map(<[BlobFile]>::len).unwrap_or_default(),
990        );
991
992        // Wire the tree-wide deletion pause into every fresh table / blob
993        // file so an in-flight checkpoint defers their cleanup if they
994        // later get marked `is_deleted` by compaction.
995        let sinks = crate::table::TableSinks {
996            deletion_pause: &self.deletion_pause,
997            heal_hints: &self.heal_hints,
998            #[cfg(feature = "std")]
999            background_deleter: Some(&self.background_deleter),
1000        };
1001        for table in tables {
1002            table.bind_to_tree(&sinks);
1003        }
1004        for bf in blob_files.unwrap_or(&[]) {
1005            bf.bind_to_tree(&sinks);
1006        }
1007
1008        let mut _compaction_state = self.compaction_state.lock();
1009        let mut version_lock = self.version_history.write();
1010
1011        version_lock.upgrade_version(
1012            &self.config.path,
1013            |current| {
1014                let mut copy = current.clone();
1015
1016                let ctx = crate::version::TransformContext::new(self.config.comparator.as_ref());
1017                copy.version = copy.version.with_new_l0_run(
1018                    tables,
1019                    blob_files,
1020                    frag_map.filter(|x| !x.is_empty()),
1021                    &ctx,
1022                );
1023
1024                for &table_id in sealed_memtables_to_delete {
1025                    log::trace!("releasing sealed memtable #{table_id}");
1026                    copy.sealed_memtables = Arc::new(copy.sealed_memtables.remove(table_id));
1027                }
1028
1029                Ok(copy)
1030            },
1031            &self.config.seqno,
1032            &self.config.visible_seqno,
1033            &*self.config.fs,
1034            self.0.runtime_config.load_full(),
1035            self.0.config.encryption.clone(),
1036            // A flush only adds a run; the watermark below prunes the
1037            // in-memory history, it discards no data.
1038            crate::version::RetentionEffect::Keep,
1039        )?;
1040
1041        if let Err(e) = version_lock.maintenance(&self.config.path, gc_watermark, &*self.config.fs)
1042        {
1043            log::warn!("Version GC failed: {e:?}");
1044        }
1045
1046        Ok(())
1047    }
1048
1049    fn clear_active_memtable(&self) {
1050        use crate::tree::sealed::SealedMemtables;
1051
1052        let mut version_history_lock = self.version_history.write();
1053        let super_version = version_history_lock.latest_version();
1054
1055        if super_version.active_memtable.is_empty() {
1056            return;
1057        }
1058
1059        let mut copy = version_history_lock.latest_version();
1060        copy.active_memtable = Arc::new(Memtable::new(
1061            self.memtable_id_counter.next(),
1062            self.config.comparator.clone(),
1063        ));
1064        copy.sealed_memtables = Arc::new(SealedMemtables::default());
1065
1066        // Rotate does not modify the memtable, so it cannot break snapshots
1067        copy.seqno = super_version.seqno;
1068
1069        version_history_lock.replace_latest_version(copy);
1070
1071        log::trace!("cleared active memtable");
1072    }
1073
1074    fn compact(
1075        &self,
1076        strategy: Arc<dyn CompactionStrategy>,
1077        seqno_threshold: SeqNo,
1078    ) -> crate::Result<crate::compaction::CompactionResult> {
1079        // NOTE: Read lock major compaction lock
1080        // That way, if a major compaction is running, we cannot proceed
1081        // But in general, parallel (non-major) compactions can occur
1082        let _lock = self.0.major_compaction_lock.read();
1083
1084        self.inner_compact(strategy, seqno_threshold)
1085    }
1086
1087    fn get_next_table_id(&self) -> TableId {
1088        self.0.get_next_table_id()
1089    }
1090
1091    fn tree_config(&self) -> &Config {
1092        &self.config
1093    }
1094
1095    fn active_memtable(&self) -> Arc<Memtable> {
1096        self.version_history
1097            .read()
1098            .latest_version_ref()
1099            .active_memtable
1100            .clone()
1101    }
1102
1103    #[expect(clippy::significant_drop_tightening)]
1104    fn rotate_memtable(&self) -> Option<Arc<Memtable>> {
1105        let mut version_history_lock = self.version_history.write();
1106        let super_version = version_history_lock.latest_version();
1107
1108        if super_version.active_memtable.is_empty() {
1109            return None;
1110        }
1111
1112        let yanked_memtable = super_version.active_memtable;
1113
1114        let mut copy = version_history_lock.latest_version();
1115        copy.active_memtable = Arc::new(Memtable::new(
1116            self.memtable_id_counter.next(),
1117            self.config.comparator.clone(),
1118        ));
1119        copy.sealed_memtables =
1120            Arc::new(super_version.sealed_memtables.add(yanked_memtable.clone()));
1121
1122        // Rotate does not modify the memtable so it cannot break snapshots
1123        copy.seqno = super_version.seqno;
1124
1125        version_history_lock.replace_latest_version(copy);
1126
1127        log::trace!(
1128            "rotate: added memtable id={} to sealed memtables",
1129            yanked_memtable.id,
1130        );
1131
1132        Some(yanked_memtable)
1133    }
1134
1135    fn table_count(&self) -> usize {
1136        self.current_version().table_count()
1137    }
1138
1139    fn level_table_count(&self, idx: usize) -> Option<usize> {
1140        self.current_version().level(idx).map(|x| x.table_count())
1141    }
1142
1143    fn approximate_len(&self) -> usize {
1144        let super_version = self.version_history.read().latest_version();
1145
1146        let tables_item_count = self
1147            .current_version()
1148            .iter_tables()
1149            .map(|x| x.metadata.item_count)
1150            .sum::<u64>();
1151
1152        let memtable_count = super_version.active_memtable.len() as u64;
1153        let sealed_count = super_version
1154            .sealed_memtables
1155            .iter()
1156            .map(|mt| mt.len())
1157            .sum::<usize>() as u64;
1158
1159        #[expect(clippy::expect_used, reason = "result should fit into usize")]
1160        (memtable_count + sealed_count + tables_item_count)
1161            .try_into()
1162            .expect("approximate_len too large for usize")
1163    }
1164
1165    fn disk_space(&self) -> u64 {
1166        self.current_version()
1167            .iter_levels()
1168            .map(super::version::Level::size)
1169            .sum()
1170    }
1171
1172    fn approximate_range_stats<K: AsRef<[u8]>, R: core::ops::RangeBounds<K>>(
1173        &self,
1174        range: R,
1175        seqno: SeqNo,
1176    ) -> crate::Result<crate::ApproximateRangeStats> {
1177        use crate::table::block_index::BlockIndex;
1178        use core::ops::Bound;
1179
1180        let lo: Bound<&[u8]> = match range.start_bound() {
1181            Bound::Included(k) => Bound::Included(k.as_ref()),
1182            Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1183            Bound::Unbounded => Bound::Unbounded,
1184        };
1185        let hi: Bound<&[u8]> = match range.end_bound() {
1186            Bound::Included(k) => Bound::Included(k.as_ref()),
1187            Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1188            Bound::Unbounded => Bound::Unbounded,
1189        };
1190        let bounds = (lo, hi);
1191
1192        let mut bytes: u64 = 0;
1193        let mut key_count: u64 = 0;
1194
1195        // Use ONE snapshot at the requested seqno for both the SST and memtable
1196        // contributions, so the estimate reflects the same visibility as a read
1197        // at `seqno` (no entries newer than the snapshot, and a consistent set of
1198        // tables + memtables even during a concurrent flush / compaction).
1199        let comparator = self.config.comparator.as_ref();
1200        let super_version = self
1201            .version_history
1202            .read()
1203            .get_version_for_snapshot(seqno)?;
1204
1205        // SST contribution: interpolate data-block offsets at the boundaries
1206        // (block granularity), no data-block reads. For a KV-separated SST the
1207        // referenced blob bytes are apportioned by the same in-range fraction.
1208        for table in super_version.version.iter_tables() {
1209            // Comparator-aware overlap: a custom user comparator orders keys
1210            // differently from raw bytes, so use the same comparison the read
1211            // path does instead of default byte ordering.
1212            if !table
1213                .metadata
1214                .key_range
1215                .overlaps_with_bounds_cmp(&bounds, comparator)
1216            {
1217                continue;
1218            }
1219            // The block index is keyed by the table-LOCAL seqno; a bulk-ingested
1220            // table carries a non-zero global seqno, so translate the snapshot
1221            // seqno the same way the read path does before seeking it. A snapshot
1222            // below the table's base means the table postdates it and contributes
1223            // nothing to the estimate, so skip it (`checked_sub` yields `None`).
1224            let Some(table_seqno) = seqno.checked_sub(table.global_seqno()) else {
1225                continue;
1226            };
1227            // The translation alone is not the visibility rule: a table with
1228            // `global_seqno == 0` passes it whatever its entries hold, so one
1229            // holding nothing but seqno 100 would charge rows and bytes to a
1230            // query at seqno 50 that reads none of them. Ask the same
1231            // classification the read path uses.
1232            if table.seqno_visibility(seqno) == crate::table::SeqnoVisibility::None {
1233                continue;
1234            }
1235
1236            // data_end = the data section's byte extent = last data block's end.
1237            let Some(last) = table.block_index.iter().next_back() else {
1238                continue;
1239            };
1240            let last = last?;
1241            let data_end = *last.offset() + u64::from(last.size());
1242            if data_end == 0 {
1243                continue;
1244            }
1245
1246            // The data block that would contain `key`, as (start, end) byte
1247            // offsets, or `None` when `key` is past the last block. The full
1248            // extent is returned so the lower bound counts from the block start
1249            // and the upper bound INCLUDES it (a range inside a single block must
1250            // not collapse to zero bytes).
1251            let block_span = |key: &[u8]| -> crate::Result<Option<(u64, u64)>> {
1252                let Some(mut iter) = table.block_index.forward_reader(key, table_seqno) else {
1253                    return Ok(None);
1254                };
1255                let Some(handle) = iter.next() else {
1256                    return Ok(None);
1257                };
1258                let h = handle?;
1259                let start = *h.offset();
1260                Ok(Some((start, (start + u64::from(h.size())).min(data_end))))
1261            };
1262            let off_lo = match lo {
1263                Bound::Included(k) | Bound::Excluded(k) => {
1264                    block_span(k)?.map_or(data_end, |(start, _)| start)
1265                }
1266                Bound::Unbounded => 0,
1267            };
1268            // Tight-space restriction: a restricted table view serves only keys
1269            // at or above its lower bound, with the punched-out prefix served by
1270            // the replacement table. Raise the lower offset to that bound so the
1271            // prefix is not double-counted (matching how scans skip it).
1272            let off_lo = match table.restrict_lower_bound() {
1273                Some(rb) => {
1274                    off_lo.max(block_span(rb.as_ref())?.map_or(data_end, |(start, _)| start))
1275                }
1276                None => off_lo,
1277            };
1278            let off_hi = match hi {
1279                Bound::Included(k) | Bound::Excluded(k) => {
1280                    block_span(k)?.map_or(data_end, |(_, end)| end)
1281                }
1282                Bound::Unbounded => data_end,
1283            };
1284            let idx_bytes = off_hi.saturating_sub(off_lo);
1285            if idx_bytes == 0 {
1286                continue;
1287            }
1288
1289            // fraction = idx_bytes / data_end, in u128 to avoid overflow. For a
1290            // standard tree `idx_bytes` already includes the inline values. For a
1291            // KV-separated SST it covers only the key + pointer bytes, so the
1292            // SST's referenced blob bytes (recorded per-SST at both flush and
1293            // compaction) are apportioned by the same in-range fraction; blob
1294            // files are not key-indexed, so this fraction is the finest estimate
1295            // possible without reading data blocks.
1296            let num = u128::from(idx_bytes);
1297            let den = u128::from(data_end);
1298            let blob_bytes = table.referenced_blob_bytes()?;
1299            // `num <= den` (both offsets are bounded by `data_end`), so
1300            // `x * num / den <= x` and the u128 -> u64 narrowing is total —
1301            // no fallback value to mask a range error with.
1302            #[expect(
1303                clippy::cast_possible_truncation,
1304                reason = "num <= den, so the quotient never exceeds the u64 input"
1305            )]
1306            let sst_blob = (u128::from(blob_bytes) * num / den) as u64;
1307            // Round up to at least one entry: a non-empty byte span over a
1308            // non-empty SST always covers at least one row, so a narrow range
1309            // never reports bytes with a zero key count.
1310            #[expect(
1311                clippy::cast_possible_truncation,
1312                reason = "num <= den, so the quotient never exceeds the u64 input"
1313            )]
1314            let in_range_entries =
1315                ((u128::from(table.metadata.item_count) * num / den) as u64).max(1);
1316            bytes = bytes.saturating_add(idx_bytes).saturating_add(sst_blob);
1317            key_count = key_count.saturating_add(in_range_entries);
1318        }
1319
1320        // Memtable contribution: the in-range fraction of each memtable's
1321        // approximate size. Built from the SAME snapshot and the SAME
1322        // `range_internal` + internal-key bounds the read path uses (range.rs),
1323        // so the counted slice matches what a read at `seqno` would traverse.
1324        let mt_range = (
1325            match lo {
1326                Bound::Included(k) => {
1327                    Bound::Included(InternalKey::new(k, SeqNo::MAX, crate::ValueType::Tombstone))
1328                }
1329                Bound::Excluded(k) => {
1330                    Bound::Excluded(InternalKey::new(k, 0, crate::ValueType::Tombstone))
1331                }
1332                Bound::Unbounded => Bound::Unbounded,
1333            },
1334            match hi {
1335                Bound::Included(k) => {
1336                    Bound::Included(InternalKey::new(k, 0, crate::ValueType::Value))
1337                }
1338                Bound::Excluded(k) => {
1339                    Bound::Excluded(InternalKey::new(k, SeqNo::MAX, crate::ValueType::Value))
1340                }
1341                Bound::Unbounded => Bound::Unbounded,
1342            },
1343        );
1344        let estimate = |mt: &crate::Memtable| -> (u64, u64) {
1345            let total = mt.len() as u64;
1346            if total == 0 {
1347                return (0, 0);
1348            }
1349            // Count only entries visible at the snapshot (the same seqno cutoff
1350            // reads apply), so the estimate excludes writes newer than `seqno`.
1351            let count = mt
1352                .range_internal(mt_range.clone())
1353                .filter(|kv| kv.key.seqno < seqno)
1354                .count() as u64;
1355            if count == 0 {
1356                return (0, 0);
1357            }
1358            // `count <= total` (the counted entries are a filtered subset of
1359            // the memtable), so the quotient never exceeds the u64 `size()`
1360            // and the narrowing is total.
1361            #[expect(
1362                clippy::cast_possible_truncation,
1363                reason = "count <= total, so the quotient never exceeds the u64 input"
1364            )]
1365            let mt_bytes = (u128::from(mt.size()) * u128::from(count) / u128::from(total)) as u64;
1366            (mt_bytes, count)
1367        };
1368        let (b, c) = estimate(&super_version.active_memtable);
1369        bytes = bytes.saturating_add(b);
1370        key_count = key_count.saturating_add(c);
1371        for mt in super_version.sealed_memtables.iter() {
1372            let (b, c) = estimate(mt);
1373            bytes = bytes.saturating_add(b);
1374            key_count = key_count.saturating_add(c);
1375        }
1376
1377        Ok(crate::ApproximateRangeStats { bytes, key_count })
1378    }
1379
1380    fn approximate_range_cardinality<K: AsRef<[u8]>, R: core::ops::RangeBounds<K>>(
1381        &self,
1382        range: R,
1383        seqno: SeqNo,
1384    ) -> crate::Result<crate::RangeCardinality> {
1385        use crate::table::block_index::BlockIndex;
1386        use core::cmp::Ordering;
1387        use core::ops::Bound;
1388
1389        let lo: Bound<&[u8]> = match range.start_bound() {
1390            Bound::Included(k) => Bound::Included(k.as_ref()),
1391            Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1392            Bound::Unbounded => Bound::Unbounded,
1393        };
1394        let hi: Bound<&[u8]> = match range.end_bound() {
1395            Bound::Included(k) => Bound::Included(k.as_ref()),
1396            Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1397            Bound::Unbounded => Bound::Unbounded,
1398        };
1399        let bounds = (lo, hi);
1400        let comparator = self.config.comparator.as_ref();
1401        let super_version = self
1402            .version_history
1403            .read()
1404            .get_version_for_snapshot(seqno)?;
1405
1406        let mut rows: u64 = 0;
1407        let mut total_rows: u64 = 0;
1408
1409        for table in super_version.version.iter_tables() {
1410            // Snapshot visibility is settled BEFORE the denominator grows. A
1411            // table no read at `seqno` can see is not part of the dataset the
1412            // selectivity describes, and counting it there while excluding it
1413            // from `rows` reports a full-keyspace query as selecting half the
1414            // tree. Out-of-RANGE tables do belong in the denominator, which is
1415            // why that check stays below it.
1416            //
1417            // A snapshot below the table's base means the table postdates it
1418            // (`checked_sub` yields `None`).
1419            let Some(table_seqno) = seqno.checked_sub(table.global_seqno()) else {
1420                continue;
1421            };
1422            // Wholly above the snapshot: invisible to a read at `seqno`, so it
1423            // contributes nothing here either (mirrors approximate_range_stats).
1424            if table.seqno_visibility(seqno) == crate::table::SeqnoVisibility::None {
1425                continue;
1426            }
1427            // What this VIEW serves, not what the file holds: a tight-space
1428            // restricted table's metadata still counts the punched-out prefix
1429            // its replacement now owns, while the numerator below starts at the
1430            // restriction — so charging the prefix here would report a
1431            // full-keyspace query as selecting a fraction of the tree.
1432            total_rows = total_rows.saturating_add(table.live_item_count()?);
1433            if !table
1434                .metadata
1435                .key_range
1436                .overlaps_with_bounds_cmp(&bounds, comparator)
1437            {
1438                continue;
1439            }
1440            // Honor a tight-space restricted view: keys below
1441            // `restrict_lower_bound()` are the punched-out prefix served by the
1442            // replacement table, so raise this table's effective lower bound to
1443            // it (mirrors approximate_range_stats) and never charge that prefix.
1444            let eff_lo = effective_lower_bound(
1445                lo,
1446                table.restrict_lower_bound().map(AsRef::as_ref),
1447                comparator,
1448            );
1449            let zone_map = &table.zone_map;
1450            // A COLUMNAR block's per-column bounds are recorded in BYTE order,
1451            // which is the only ordering a value column has. Comparing them
1452            // with a non-lexicographic user comparator reads the recorded
1453            // minimum as a comparator maximum, so the walk stops before blocks
1454            // that overlap the query: the shortcut is skipped for such trees
1455            // and the byte-fraction estimate below answers instead. A row
1456            // block's bounds are the block's first and last key, already in
1457            // comparator order, so the default path is unaffected.
1458            let zone_bounds_ordered = comparator.is_lexicographic() || !table.metadata.columnar;
1459            if !zone_map.is_empty() && zone_bounds_ordered {
1460                // Zone map present: sum the per-block row counts of blocks whose
1461                // key range overlaps the query. A block is past the range once its
1462                // minimum key is above the upper bound; the boundary block at the
1463                // effective lower bound is counted in full (block granularity). A
1464                // range that lands in a key-space gap legitimately yields zero, so
1465                // this path is authoritative and never falls back to the byte fraction.
1466                let reader = match eff_lo {
1467                    Bound::Included(k) | Bound::Excluded(k) => {
1468                        table.block_index.forward_reader(k, table_seqno)
1469                    }
1470                    Bound::Unbounded => Some(table.block_index.iter()),
1471                };
1472                if let Some(reader) = reader {
1473                    for handle in reader {
1474                        let handle = handle?;
1475                        let Some(col) = zone_map
1476                            .columns_for(*handle.offset())
1477                            .and_then(|c| c.first())
1478                        else {
1479                            continue;
1480                        };
1481                        let above_hi = match hi {
1482                            Bound::Included(hk) => {
1483                                comparator.compare(&col.min, hk) == Ordering::Greater
1484                            }
1485                            Bound::Excluded(hk) => {
1486                                comparator.compare(&col.min, hk) != Ordering::Less
1487                            }
1488                            Bound::Unbounded => false,
1489                        };
1490                        if above_hi {
1491                            break;
1492                        }
1493                        rows = rows.saturating_add(u64::from(col.row_count));
1494                    }
1495                }
1496            } else if let Some(last) = table.block_index.iter().next_back() {
1497                // No zone map: apportion item_count by the in-range
1498                // data-block byte fraction, mirroring approximate_range_stats.
1499                let last = last?;
1500                let data_end = *last.offset() + u64::from(last.size());
1501                if data_end > 0 {
1502                    let off = |key: &[u8], end: bool| -> crate::Result<u64> {
1503                        match table.block_index.forward_reader(key, table_seqno) {
1504                            Some(mut it) => match it.next() {
1505                                Some(h) => {
1506                                    let h = h?;
1507                                    Ok(if end {
1508                                        (*h.offset() + u64::from(h.size())).min(data_end)
1509                                    } else {
1510                                        *h.offset()
1511                                    })
1512                                }
1513                                None => Ok(data_end),
1514                            },
1515                            None => Ok(data_end),
1516                        }
1517                    };
1518                    let off_lo = match eff_lo {
1519                        Bound::Included(k) | Bound::Excluded(k) => off(k, false)?,
1520                        Bound::Unbounded => 0,
1521                    };
1522                    let off_hi = match hi {
1523                        Bound::Included(k) | Bound::Excluded(k) => off(k, true)?,
1524                        Bound::Unbounded => data_end,
1525                    };
1526                    let idx_bytes = off_hi.saturating_sub(off_lo);
1527                    if idx_bytes > 0 {
1528                        // `idx_bytes <= data_end` (both offsets are bounded by
1529                        // `data_end`), so the quotient never exceeds the u64
1530                        // `item_count` and the narrowing is total.
1531                        #[expect(
1532                            clippy::cast_possible_truncation,
1533                            reason = "idx_bytes <= data_end, so the quotient never exceeds the u64 input"
1534                        )]
1535                        let est = ((u128::from(table.metadata.item_count) * u128::from(idx_bytes)
1536                            / u128::from(data_end)) as u64)
1537                            .max(1);
1538                        rows = rows.saturating_add(est);
1539                    }
1540                }
1541            }
1542        }
1543
1544        // Memtables: count the in-range, snapshot-visible entries and add them to
1545        // both the matched rows and the total (matching the SST accounting).
1546        let mt_range = (
1547            match lo {
1548                Bound::Included(k) => {
1549                    Bound::Included(InternalKey::new(k, SeqNo::MAX, crate::ValueType::Tombstone))
1550                }
1551                Bound::Excluded(k) => {
1552                    Bound::Excluded(InternalKey::new(k, 0, crate::ValueType::Tombstone))
1553                }
1554                Bound::Unbounded => Bound::Unbounded,
1555            },
1556            match hi {
1557                Bound::Included(k) => {
1558                    Bound::Included(InternalKey::new(k, 0, crate::ValueType::Value))
1559                }
1560                Bound::Excluded(k) => {
1561                    Bound::Excluded(InternalKey::new(k, SeqNo::MAX, crate::ValueType::Value))
1562                }
1563                Bound::Unbounded => Bound::Unbounded,
1564            },
1565        );
1566        let mut add_memtable = |mt: &crate::Memtable| {
1567            // The denominator counts what a read at this snapshot can SEE, the
1568            // same rule the SST loop applies: an entry at or above `seqno` is
1569            // filtered out of the numerator below, so counting it here would
1570            // report a full-keyspace query as selecting half of what it sees.
1571            let visible = mt.iter().filter(|kv| kv.key.seqno < seqno).count() as u64;
1572            total_rows = total_rows.saturating_add(visible);
1573            let in_range = mt
1574                .range_internal(mt_range.clone())
1575                .filter(|kv| kv.key.seqno < seqno)
1576                .count() as u64;
1577            rows = rows.saturating_add(in_range);
1578        };
1579        add_memtable(&super_version.active_memtable);
1580        for mt in super_version.sealed_memtables.iter() {
1581            add_memtable(mt);
1582        }
1583
1584        // selectivity is an approximate ratio; u64 row counts are well within
1585        // f64's exact-integer range (2^52) for any realistic table.
1586        #[expect(
1587            clippy::cast_precision_loss,
1588            reason = "row counts never approach 2^52; the ratio is approximate anyway"
1589        )]
1590        let selectivity = if total_rows == 0 {
1591            0.0
1592        } else {
1593            (rows.min(total_rows) as f64) / (total_rows as f64)
1594        };
1595        Ok(crate::RangeCardinality { rows, selectivity })
1596    }
1597
1598    fn get_highest_memtable_seqno(&self) -> Option<SeqNo> {
1599        let version = self.version_history.read().latest_version();
1600
1601        let active = version.active_memtable.get_highest_seqno();
1602
1603        let sealed = version
1604            .sealed_memtables
1605            .iter()
1606            .map(|mt| mt.get_highest_seqno())
1607            .max()
1608            .flatten();
1609
1610        active.max(sealed)
1611    }
1612
1613    fn get_highest_persisted_seqno(&self) -> Option<SeqNo> {
1614        self.current_version()
1615            .iter_tables()
1616            .map(Table::get_highest_seqno)
1617            .max()
1618    }
1619
1620    fn oldest_retained_seqno(&self) -> SeqNo {
1621        self.version_history.read().oldest_retained_seqno()
1622    }
1623
1624    fn retention_floor(&self) -> SeqNo {
1625        self.version_history
1626            .read()
1627            .latest_version_ref()
1628            .version
1629            .retention_floor()
1630    }
1631
1632    fn get<K: AsRef<[u8]>>(&self, key: K, seqno: SeqNo) -> crate::Result<Option<UserValue>> {
1633        let key = key.as_ref();
1634
1635        let super_version = self.snapshot_for_read(seqno)?;
1636
1637        Self::resolve_or_passthrough(
1638            &super_version,
1639            key,
1640            seqno,
1641            self.config.merge_operator.as_ref(),
1642            self.config.comparator.as_ref(),
1643        )
1644    }
1645
1646    fn get_pinned<K: AsRef<[u8]>>(
1647        &self,
1648        key: K,
1649        seqno: SeqNo,
1650    ) -> crate::Result<Option<crate::PinnableSlice>> {
1651        let key = key.as_ref();
1652
1653        let super_version = self.snapshot_for_read(seqno)?;
1654
1655        Self::resolve_or_passthrough_pinned(
1656            &super_version,
1657            key,
1658            seqno,
1659            self.config.merge_operator.as_ref(),
1660            self.config.comparator.as_ref(),
1661        )
1662    }
1663
1664    #[expect(
1665        clippy::indexing_slicing,
1666        reason = "indices are generated from 0..n range, always in bounds"
1667    )]
1668    fn multi_get<K: AsRef<[u8]>>(
1669        &self,
1670        keys: impl IntoIterator<Item = K>,
1671        seqno: SeqNo,
1672    ) -> crate::Result<Vec<Option<UserValue>>> {
1673        let super_version = self.snapshot_for_read(seqno)?;
1674        let comparator = self.config.comparator.as_ref();
1675        let merge_operator = self.config.merge_operator.as_ref();
1676
1677        // Collect keys up front; bloom hashes computed lazily in Phase 2
1678        let keys: Vec<_> = keys.into_iter().collect();
1679        let n = keys.len();
1680        if n == 0 {
1681            return Ok(Vec::new());
1682        }
1683
1684        // For small batches, use the simple per-key path
1685        if n <= 2 {
1686            return keys
1687                .iter()
1688                .map(|key| {
1689                    Self::resolve_or_passthrough(
1690                        &super_version,
1691                        key.as_ref(),
1692                        seqno,
1693                        merge_operator,
1694                        comparator,
1695                    )
1696                })
1697                .collect();
1698        }
1699
1700        // Phase 1: Check active + sealed memtables (unsorted — memtable lookup
1701        // is O(log n) per key regardless of order, skip sort+hash overhead for
1702        // memtable-only batches).
1703        let mut internal_entries: Vec<Option<InternalValue>> = vec![None; n];
1704        let mut remaining: Vec<usize> = Vec::with_capacity(n);
1705
1706        for idx in 0..n {
1707            let key = keys[idx].as_ref();
1708
1709            // Active memtable
1710            if let Some(entry) = super_version.active_memtable.get(key, seqno) {
1711                internal_entries[idx] = Some(entry);
1712                continue;
1713            }
1714
1715            // Sealed memtables (newest first)
1716            if let Some(entry) =
1717                Self::get_internal_entry_from_sealed_memtables(&super_version, key, seqno)
1718            {
1719                internal_entries[idx] = Some(entry);
1720                continue;
1721            }
1722
1723            remaining.push(idx);
1724        }
1725
1726        // Phase 2: Sort remaining keys + compute bloom hashes only if needed
1727        // (memtable-only batches skip this entirely).
1728        if !remaining.is_empty() {
1729            remaining.sort_by(|&a, &b| comparator.compare(keys[a].as_ref(), keys[b].as_ref()));
1730
1731            // De-duplicate equal query keys (the batched on-disk path requires
1732            // strictly-sorted-unique input) and resolve the misses. Shared with
1733            // the BlobTree path via these helpers so the two cannot drift.
1734            let (miss_keys, duplicates) =
1735                Self::dedup_sorted_miss_keys(&remaining, &keys, comparator);
1736
1737            Self::batch_get_from_tables(
1738                &super_version.version,
1739                &keys,
1740                miss_keys,
1741                seqno,
1742                comparator,
1743                &*self.config.fs,
1744                &mut internal_entries,
1745            )?;
1746
1747            Self::fan_out_duplicates(&duplicates, &mut internal_entries);
1748        }
1749
1750        // Phase 3: Resolve entries (tombstones, RT suppression, merge operands)
1751        let mut results = vec![None; n];
1752        for idx in 0..n {
1753            let entry = internal_entries[idx].take();
1754            results[idx] = Self::resolve_entry(
1755                &super_version,
1756                keys[idx].as_ref(),
1757                entry,
1758                seqno,
1759                merge_operator,
1760                comparator,
1761            )?;
1762        }
1763
1764        Ok(results)
1765    }
1766
1767    fn apply_batch(&self, batch: crate::WriteBatch, seqno: SeqNo) -> crate::Result<(u64, u64)> {
1768        if batch.is_empty() {
1769            return Ok((0, self.active_memtable().size()));
1770        }
1771        Ok(self.append_batch(batch.materialize(seqno)?))
1772    }
1773
1774    fn insert<K: Into<UserKey>, V: Into<UserValue>>(
1775        &self,
1776        key: K,
1777        value: V,
1778        seqno: SeqNo,
1779    ) -> (u64, u64) {
1780        let value = InternalValue::from_components(key, value, seqno, ValueType::Value);
1781        self.append_entry(value)
1782    }
1783
1784    fn merge<K: Into<UserKey>, V: Into<UserValue>>(
1785        &self,
1786        key: K,
1787        operand: V,
1788        seqno: SeqNo,
1789    ) -> (u64, u64) {
1790        let value = InternalValue::new_merge_operand(key, operand, seqno);
1791        self.append_entry(value)
1792    }
1793
1794    fn remove<K: Into<UserKey>>(&self, key: K, seqno: SeqNo) -> (u64, u64) {
1795        let value = InternalValue::new_tombstone(key, seqno);
1796        self.append_entry(value)
1797    }
1798
1799    fn remove_weak<K: Into<UserKey>>(&self, key: K, seqno: SeqNo) -> (u64, u64) {
1800        let value = InternalValue::new_weak_tombstone(key, seqno);
1801        self.append_entry(value)
1802    }
1803
1804    fn remove_range<K: Into<UserKey>>(&self, start: K, end: K, seqno: SeqNo) -> u64 {
1805        // The read guard is held through the insert, like `append_entry`: the
1806        // CDC scan's capture (write side of this lock) must exclude every
1807        // in-flight memtable write, or a backdated range deletion could land
1808        // after the capture yet below the returned watermark and be lost; the
1809        // guard also keeps a concurrent `rotate_memtable()` from sealing the
1810        // memtable mid-insert.
1811        let history = self.version_history.read();
1812
1813        #[cfg(test)]
1814        inner::TestHooks::fire(&self.test_hooks.range_write);
1815
1816        history
1817            .latest_version_ref()
1818            .active_memtable
1819            .insert_range_tombstone(start.into(), end.into(), seqno)
1820    }
1821}
1822
1823impl Tree {
1824    /// Maps a raw internal entry to its change-data-capture event, routing
1825    /// `Indirection` (KV-separated) values through `resolve_indirection`.
1826    ///
1827    /// A standard tree never stores `Indirection` and supplies a resolver that
1828    /// errors; the blob-tree scan path supplies one that reads the blob and
1829    /// returns an [`ScanSinceEvent::Insert`].
1830    fn map_event<F>(
1831        entry: InternalValue,
1832        version: &Version,
1833        resolve_indirection: &F,
1834    ) -> crate::Result<ScanSinceEvent>
1835    where
1836        F: Fn(&Version, InternalValue) -> crate::Result<ScanSinceEvent>,
1837    {
1838        if entry.key.value_type == ValueType::Indirection {
1839            return resolve_indirection(version, entry);
1840        }
1841        let seqno = entry.key.seqno;
1842        let key = entry.key.user_key;
1843        Ok(match entry.key.value_type {
1844            ValueType::Value => ScanSinceEvent::Insert {
1845                key,
1846                value: entry.value,
1847                seqno,
1848            },
1849            ValueType::MergeOperand => ScanSinceEvent::MergeOperand {
1850                key,
1851                operand: entry.value,
1852                seqno,
1853            },
1854            // Weak (single-delete) tombstones keep their own event kind: a
1855            // weak tombstone annihilates exactly its matching put during
1856            // compaction and can then expose an older value, while a regular
1857            // tombstone keeps hiding it — collapsing both into one event
1858            // would make a replica replay a weak delete as a full delete and
1859            // diverge from the source.
1860            ValueType::Tombstone => ScanSinceEvent::PointTombstone { key, seqno },
1861            ValueType::WeakTombstone => ScanSinceEvent::WeakTombstone { key, seqno },
1862            ValueType::Indirection => unreachable!("Indirection handled above"),
1863        })
1864    }
1865
1866    /// Shared CDC aggregation behind [`Self::scan_since_seqno`] and the
1867    /// blob-tree scan path: gathers qualifying entries (`seqno >= target`) plus
1868    /// range tombstones from the active + sealed memtables and every SST (with
1869    /// block-skip), maps each entry to a [`ScanSinceEvent`] — routing
1870    /// `Indirection` values through `resolve_indirection` against the same
1871    /// version snapshot — and returns them in increasing seqno order.
1872    ///
1873    /// # Panics
1874    ///
1875    /// Panics if the internal version-history lock is poisoned.
1876    ///
1877    /// # Errors
1878    ///
1879    /// Returns `Err` if reading the index or a data block fails, or if
1880    /// `resolve_indirection` errors.
1881    pub(crate) fn scan_since_seqno_with<F>(
1882        &self,
1883        target_seqno: SeqNo,
1884        block_skip: bool,
1885        resolve_indirection: F,
1886    ) -> crate::Result<alloc::vec::IntoIter<ScanSinceEvent>>
1887    where
1888        F: Fn(&Version, InternalValue) -> crate::Result<ScanSinceEvent>,
1889    {
1890        self.scan_since_seqno_scoped(target_seqno, block_skip, resolve_indirection, None)
1891    }
1892
1893    /// As [`Self::scan_since_seqno_with`], optionally scoped to a key range
1894    /// (in the tree comparator's order): point events are delivered only for
1895    /// keys INSIDE the bounds, range tombstones when their span OVERLAPS them
1896    /// (a tombstone reaching into the range affects replay within it), and
1897    /// SSTs whose key range cannot intersect the bounds are skipped without
1898    /// being read — which is what makes a post-repair reconciliation over
1899    /// [`RepairReport::lost_coverage`](crate::RepairReport) affordable.
1900    pub(crate) fn scan_since_seqno_scoped<F>(
1901        &self,
1902        target_seqno: SeqNo,
1903        block_skip: bool,
1904        resolve_indirection: F,
1905        key_range: Option<&(Bound<UserKey>, Bound<UserKey>)>,
1906    ) -> crate::Result<alloc::vec::IntoIter<ScanSinceEvent>>
1907    where
1908        F: Fn(&Version, InternalValue) -> crate::Result<ScanSinceEvent>,
1909    {
1910        use core::cmp::Ordering;
1911
1912        let cmp = self.config.comparator.clone();
1913        let in_key_range = |key: &[u8]| -> bool {
1914            let Some((lo, hi)) = key_range else {
1915                return true;
1916            };
1917            (match lo {
1918                Bound::Included(b) => cmp.compare(key, b.as_ref()) != Ordering::Less,
1919                Bound::Excluded(b) => cmp.compare(key, b.as_ref()) == Ordering::Greater,
1920                Bound::Unbounded => true,
1921            }) && (match hi {
1922                Bound::Included(b) => cmp.compare(key, b.as_ref()) != Ordering::Greater,
1923                Bound::Excluded(b) => cmp.compare(key, b.as_ref()) == Ordering::Less,
1924                Bound::Unbounded => true,
1925            })
1926        };
1927        // A range tombstone covers `[start, end)`; it is delivered when that
1928        // span overlaps the scope, since a deletion reaching into the range
1929        // affects replay within it.
1930        let rt_in_range = |rt: &RangeTombstone| -> bool {
1931            let Some((lo, hi)) = key_range else {
1932                return true;
1933            };
1934            (match lo {
1935                // `rt.end` is EXCLUSIVE: the tombstone reaches keys strictly
1936                // below it, so it clears the lower bound only when its end is
1937                // ABOVE the bound key (for an excluded bound this over-includes
1938                // the touching case, which is harmless: replaying an extra
1939                // idempotent deletion event cannot corrupt a consumer).
1940                Bound::Included(b) | Bound::Excluded(b) => {
1941                    cmp.compare(rt.end.as_ref(), b.as_ref()) == Ordering::Greater
1942                }
1943                Bound::Unbounded => true,
1944            }) && (match hi {
1945                Bound::Included(b) => {
1946                    cmp.compare(rt.start.as_ref(), b.as_ref()) != Ordering::Greater
1947                }
1948                Bound::Excluded(b) => cmp.compare(rt.start.as_ref(), b.as_ref()) == Ordering::Less,
1949                Bound::Unbounded => true,
1950            })
1951        };
1952        // The active memtable is the one source a writer can still change, and
1953        // the seqno cap alone does not exclude that: a caller may commit with an
1954        // explicit seqno at or BELOW the cap (`apply_batch` takes the seqno from
1955        // the caller), and a live lock-free walk would then see that write or
1956        // miss it depending on where the node lands relative to the cursor —
1957        // even splitting one batch. A consumer that advanced past the returned
1958        // watermark would lose the change for good.
1959        //
1960        // So freeze it: writers hold the version-history READ guard for their
1961        // whole insert (that is what keeps `rotate_memtable` from sealing
1962        // mid-batch), so taking the WRITE guard excludes them. The cap and the
1963        // active memtable's raw entries are captured under it; everything else —
1964        // sealed memtables, tables — is immutable and needs no coordination.
1965        //
1966        // Mapping runs AFTER the guard drops: it resolves blob indirections,
1967        // which reads a blob file, and no I/O may happen with writers blocked.
1968        let (super_version, end_seqno, active_entries, active_range_tombstones) = {
1969            let guard = self.version_history.write();
1970            let super_version = guard.latest_version();
1971            #[cfg(test)]
1972            inner::TestHooks::fire(&self.test_hooks.scan_freeze);
1973            let end_seqno = {
1974                let active = super_version.active_memtable.get_highest_seqno();
1975                let sealed = super_version
1976                    .sealed_memtables
1977                    .iter()
1978                    .map(|mt| mt.get_highest_seqno())
1979                    .max()
1980                    .flatten();
1981                let tables = super_version
1982                    .version
1983                    .iter_tables()
1984                    .map(Table::get_highest_seqno)
1985                    .max();
1986                active.max(sealed).max(tables)
1987            };
1988            let entries: Vec<InternalValue> = end_seqno.map_or_else(Vec::new, |cap| {
1989                super_version
1990                    .active_memtable
1991                    .iter()
1992                    .filter(|e| e.key.seqno >= target_seqno && e.key.seqno <= cap)
1993                    .collect()
1994            });
1995            let rts = super_version.active_memtable.range_tombstones_sorted();
1996            // Explicit: the guard must outlive the capture above, and writers
1997            // resume the moment it goes.
1998            drop(guard);
1999            (super_version, end_seqno, entries, rts)
2000        };
2001        let version = &super_version.version;
2002        // No entries anywhere ⇒ nothing qualifies, regardless of target.
2003        let Some(end_seqno) = end_seqno else {
2004            return Ok(Vec::new().into_iter());
2005        };
2006
2007        // Events are gathered PER SOURCE, not into one flat list: copies of a
2008        // change across two sources are the same change and collapse, but a
2009        // single source may legitimately hold a byte-identical event more than
2010        // once (a write batch may carry the same merge operand for a key
2011        // twice; both are stored under the batch's shared seqno and both are
2012        // applied on read). See `merge_source_events`.
2013        let mut sources: Vec<Vec<ScanSinceEvent>> = Vec::new();
2014
2015        let in_window = |seqno: SeqNo| seqno >= target_seqno && seqno <= end_seqno;
2016        let range_tombstone_event = |rt: &RangeTombstone| {
2017            in_window(rt.seqno).then(|| ScanSinceEvent::RangeTombstone {
2018                start_key: rt.start.clone(),
2019                end_key: rt.end.clone(),
2020                seqno: rt.seqno,
2021            })
2022        };
2023
2024        // The scope bounds as borrowed slices, for SST key-range pruning.
2025        fn as_ref_bound(b: &Bound<UserKey>) -> Bound<&[u8]> {
2026            match b {
2027                Bound::Included(k) => Bound::Included(k.as_ref()),
2028                Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
2029                Bound::Unbounded => Bound::Unbounded,
2030            }
2031        }
2032        let ref_bounds = key_range.map(|(lo, hi)| (as_ref_bound(lo), as_ref_bound(hi)));
2033
2034        // Active memtable — mapped from the frozen capture above, not walked
2035        // again: a second walk would reintroduce exactly the race the freeze
2036        // closed.
2037        let mut source = Vec::new();
2038        for entry in active_entries {
2039            if !in_key_range(&entry.key.user_key) {
2040                continue;
2041            }
2042            source.push(Self::map_event(entry, version, &resolve_indirection)?);
2043        }
2044        for rt in active_range_tombstones {
2045            if rt_in_range(&rt) {
2046                source.extend(range_tombstone_event(&rt));
2047            }
2048        }
2049        sources.push(source);
2050
2051        // Sealed memtables, NEWEST first: the list is kept in seal order, and
2052        // every source below must be older than the one before it, because the
2053        // merge derives replay precedence from that position.
2054        for memtable in super_version.sealed_memtables.iter().rev() {
2055            let mut source = Vec::new();
2056            for entry in memtable.iter() {
2057                if in_window(entry.key.seqno) && in_key_range(&entry.key.user_key) {
2058                    source.push(Self::map_event(entry, version, &resolve_indirection)?);
2059                }
2060            }
2061            for rt in memtable.range_tombstones_sorted() {
2062                if rt_in_range(&rt) {
2063                    source.extend(range_tombstone_event(&rt));
2064                }
2065            }
2066            sources.push(source);
2067        }
2068
2069        // SSTs. A table whose key range cannot intersect the scope is skipped
2070        // without a single block read — the point of the scoped variant. The
2071        // key range is RANGE-TOMBSTONE-SAFE to prune on: every writer keeps
2072        // tombstone coverage inside it (a flush conservatively widens the
2073        // range over its tombstone spans — see `write_rts_to_writer` — and a
2074        // compaction clips its output's tombstones to the table's
2075        // responsibility range), so a tombstone overlapping the scope always
2076        // sits in a table this loop visits.
2077        for table in version.iter_tables() {
2078            if let Some(bounds) = &ref_bounds
2079                && !table
2080                    .metadata
2081                    .key_range
2082                    .overlaps_with_bounds_cmp(bounds, cmp.as_ref())
2083            {
2084                continue;
2085            }
2086            // An RT-only table's synthetic weak-tombstone sentinel (the
2087            // writer's `finish`) is deliberately NOT filtered out here. It is
2088            // a real on-disk entry the READ path sees: at a seqno TIE between
2089            // the range deletion and an older source's write at the range's
2090            // start key, the sentinel is what makes the read converge to a
2091            // deletion — so the event stream must carry it too, or a consumer
2092            // replaying the stream keeps a value the tree itself does not
2093            // serve (this stream's one rule is mirroring the tree's reads,
2094            // see `merge_source_events`). It surfaces as the weak-tombstone
2095            // event it is on disk; away from that tie a replayed weak delete
2096            // at the range's start under the range deletion's own seqno is a
2097            // no-op.
2098            let mut source = Vec::new();
2099            for entry in table.scan_seqno_range(target_seqno, end_seqno, block_skip)? {
2100                if !in_key_range(&entry.key.user_key) {
2101                    continue;
2102                }
2103                source.push(Self::map_event(entry, version, &resolve_indirection)?);
2104            }
2105            // Clamped to the view's tight-space restriction: the punched
2106            // prefix's deletions are re-emitted by the slice output that
2107            // superseded it, so the raw list would duplicate those events.
2108            for rt in table.visible_range_tombstones() {
2109                if rt_in_range(&rt) {
2110                    source.extend(range_tombstone_event(&rt));
2111                }
2112            }
2113            sources.push(source);
2114        }
2115
2116        Ok(Self::merge_source_events(sources).into_iter())
2117    }
2118
2119    /// Merge per-source event lists into one replay-ordered stream, `sources`
2120    /// ordered NEWEST first.
2121    ///
2122    /// Replay order is increasing seqno, then — for events sharing one seqno —
2123    /// increasing source AGE reversed, so the newest source's event is applied
2124    /// LAST. That matters because two sources can hold different values for one
2125    /// key at one seqno ([`AbstractTree::apply_batch`] takes a caller-chosen
2126    /// seqno and does not require it to be unique), the tree serves the newer
2127    /// one, and a consumer keeps whatever it applies last. Sorting such ties by
2128    /// payload instead would decide precedence by byte order.
2129    ///
2130    /// What happens to byte-identical copies follows ONE rule: the stream must
2131    /// mirror what a read of the same tree does, because a consumer replaying
2132    /// it has to reach the state the tree itself serves.
2133    ///
2134    /// - **Merge operands are all kept.** The read path collects every
2135    ///   physically stored operand for a key and applies them in order — it
2136    ///   never deduplicates by seqno — so two operands are two applications
2137    ///   whether they sit in one source or in two. Seqnos do not disambiguate
2138    ///   here: [`AbstractTree::apply_batch`] takes a caller-chosen seqno and
2139    ///   does not require it to be unique, and one batch may carry the same
2140    ///   operand for a key twice.
2141    /// - **Idempotent events collapse across sources.** A write, a deletion or
2142    ///   a range deletion replayed twice reaches the same state, and the read
2143    ///   path shadows the copies by seqno rather than compounding them. One
2144    ///   committed change can physically live in two published tables — a
2145    ///   manifest-loss repair publishes every surviving SST as its own L0 run,
2146    ///   including both the inputs and the outputs of a compaction that crashed
2147    ///   before deleting its inputs — and delivering it twice would be noise.
2148    ///   Repeats WITHIN one source are still kept: they are separate entries
2149    ///   the source genuinely holds.
2150    fn merge_source_events(sources: Vec<Vec<ScanSinceEvent>>) -> Vec<ScanSinceEvent> {
2151        // Per source, collapse equal neighbours into runs, each tagged with
2152        // its source's recency (0 = newest) and the position of EVERY copy it
2153        // carries.
2154        //
2155        // The POSITIONS are what keep an order-sensitive merge operator
2156        // correct: a source applies the operands of one batch in the order
2157        // they were added, all at one seqno, and grouping them by payload
2158        // would replay them in a different order — an append or a list push
2159        // then converges somewhere the tree never was. Grouping still SORTS
2160        // (equal events have to meet), so each run remembers where each of its
2161        // members sat: one shared position would fold a repeated operand's
2162        // copies onto its twin's slot and replay `B, A, B` as `A, B, B`.
2163        struct Run {
2164            event: ScanSinceEvent,
2165            recency: usize,
2166            /// Original scan position of every copy, ascending.
2167            positions: Vec<usize>,
2168        }
2169        let mut runs: Vec<Run> = Vec::new();
2170        for (recency, source) in sources.into_iter().enumerate() {
2171            let mut indexed: Vec<(usize, ScanSinceEvent)> =
2172                source.into_iter().enumerate().collect();
2173            indexed.sort_by(|(_, a), (_, b)| ScanSinceEvent::grouping_order(a, b));
2174            let mut iter = indexed.into_iter();
2175            let Some((position, mut current)) = iter.next() else {
2176                continue;
2177            };
2178            let mut positions = alloc::vec![position];
2179            for (index, event) in iter {
2180                if event == current {
2181                    positions.push(index);
2182                } else {
2183                    positions.sort_unstable();
2184                    runs.push(Run {
2185                        event: core::mem::replace(&mut current, event),
2186                        recency,
2187                        positions: core::mem::replace(&mut positions, alloc::vec![index]),
2188                    });
2189                }
2190            }
2191            positions.sort_unstable();
2192            runs.push(Run {
2193                event: current,
2194                recency,
2195                positions,
2196            });
2197        }
2198
2199        // Merge the per-source runs. Operands are never collapsed — every copy
2200        // is an application the read path makes, so each keeps ITS source's
2201        // recency and ITS scan position, and the replay order below (oldest
2202        // source first, then position) applies them exactly as the tree does.
2203        // Idempotent events collapse across sources to the count a single
2204        // source holds; the collapsed event keeps the recency of the NEWEST
2205        // source holding it (and that source's positions), which is the slot
2206        // the tree's own precedence gives it.
2207        //
2208        // WEAK tombstones belong to the idempotent class even though a weak
2209        // delete annihilates one put at compaction: its documented contract
2210        // pairs it with a key written at most once, byte-identical copies
2211        // meeting in one compaction stream drain to a single survivor, and a
2212        // consumer cannot materialize multiplicity anyway — replaying the
2213        // same `remove_weak` twice at one seqno lands on ONE internal key in
2214        // its memtable, so a preserved duplicate would replay to the same
2215        // physical state the collapsed stream does.
2216        runs.sort_by(|a, b| ScanSinceEvent::grouping_order(&a.event, &b.event));
2217        let mut merged: Vec<(ScanSinceEvent, usize, usize)> = Vec::new();
2218        let mut iter = runs.into_iter().peekable();
2219        while let Some(run) = iter.next() {
2220            if matches!(run.event, ScanSinceEvent::MergeOperand { .. }) {
2221                // Equal operand runs from OTHER sources follow as their own
2222                // iterations and emit their own copies — no draining here.
2223                for &position in &run.positions {
2224                    merged.push((run.event.clone(), run.recency, position));
2225                }
2226                continue;
2227            }
2228            let Run {
2229                event,
2230                mut recency,
2231                mut positions,
2232            } = run;
2233            let mut count = positions.len();
2234            while let Some(other) = iter.next_if(|next| next.event == event) {
2235                debug_assert_eq!(other.event, event, "next_if matched the same event");
2236                count = count.max(other.positions.len());
2237                if other.recency < recency {
2238                    recency = other.recency;
2239                    positions = other.positions;
2240                }
2241            }
2242            // The winning source may hold fewer copies than the count another
2243            // source did; the extras repeat its last position (they are
2244            // byte-identical, so their relative order carries no information).
2245            while positions.len() < count {
2246                let last = positions.last().copied().unwrap_or_default();
2247                positions.push(last);
2248            }
2249            for &position in positions.iter().take(count) {
2250                merged.push((event.clone(), recency, position));
2251            }
2252        }
2253
2254        // Finally, replay order: seqno, then range deletions, then oldest source
2255        // first, then the position that source gave the event — which is how an
2256        // order-sensitive merge operator converges to what the tree serves.
2257        //
2258        // The range-deletion step comes BEFORE source recency, not after: a tied
2259        // deletion does not suppress the writes it spans (suppression is
2260        // strictly `entry.seqno < tombstone.seqno`), so the tree keeps them, and
2261        // a replay that applied the deletion last would drop them. Ordering by
2262        // recency first would do exactly that whenever the deletion sits in the
2263        // newer source.
2264        merged.sort_by(|(a, a_recency, a_pos), (b, b_recency, b_pos)| {
2265            fn deletion_first(e: &ScanSinceEvent) -> u8 {
2266                u8::from(!matches!(e, ScanSinceEvent::RangeTombstone { .. }))
2267            }
2268            a.seqno()
2269                .cmp(&b.seqno())
2270                .then_with(|| deletion_first(a).cmp(&deletion_first(b)))
2271                .then_with(|| b_recency.cmp(a_recency))
2272                .then_with(|| {
2273                    // Within one source and one seqno, a scan yields a key's
2274                    // versions NEWEST first, and the read path reverses that run
2275                    // to apply them chronologically. Mirror it: same key ⇒ the
2276                    // later scan position replays FIRST. Distinct keys at one
2277                    // seqno touch different state, so their relative order is
2278                    // free — keep it deterministic by scan position.
2279                    //
2280                    // Identity is the engine's one relation (byte equality, see
2281                    // `same_user_key`), which is what the read path this mirrors
2282                    // uses to group a key's versions. Asking the comparator here
2283                    // would answer the same question — its contract makes
2284                    // `Equal` imply byte equality — while letting the two paths
2285                    // disagree the moment a comparator broke that.
2286                    if crate::comparator::same_user_key(a.key(), b.key()) {
2287                        b_pos.cmp(a_pos)
2288                    } else {
2289                        a_pos.cmp(b_pos)
2290                    }
2291                })
2292        });
2293        merged.into_iter().map(|(event, ..)| event).collect()
2294    }
2295
2296    /// Iterate change events with `seqno >= target_seqno`.
2297    ///
2298    /// Returns every change committed at or after `target_seqno` as a stream
2299    /// of [`ScanSinceEvent`]s in increasing seqno order. This is the canonical
2300    /// change-data-capture primitive: a downstream consumer (replica, Kafka
2301    /// connector, Debezium-style pipeline) replays the events in order to
2302    /// reconstruct the source's history. Superseded versions are not collapsed
2303    /// (a key written three times after the target yields three events).
2304    ///
2305    /// # Concurrency
2306    ///
2307    /// The result is a snapshot of the tree as of the call: the active memtable
2308    /// is captured with writers excluded, so a batch committed concurrently is
2309    /// either wholly in the result or wholly absent, never split across it. This
2310    /// holds even for a caller-chosen sequence number at or below the reported
2311    /// watermark, which the seqno bound alone would not exclude. Writers are
2312    /// blocked only while that capture runs — the sealed memtables and SSTs the
2313    /// scan then reads are immutable.
2314    ///
2315    /// # History retention
2316    ///
2317    /// The stream carries what the tree still PHYSICALLY HOLDS. A compaction
2318    /// run with a GC watermark (the `seqno_threshold` passed to
2319    /// [`compact`](crate::AbstractTree::compact) /
2320    /// [`major_compact`](crate::AbstractTree::major_compact)) drops shadowed
2321    /// versions and evicted tombstones below that watermark and may fold
2322    /// merge chains and zero bottommost seqnos — history a later scan cannot
2323    /// resurrect. The result is therefore complete only for a `target_seqno`
2324    /// at or above the highest GC watermark ever applied: a deployment
2325    /// replaying this stream (an external-WAL consumer, a CDC replica) must
2326    /// keep its compaction watermark at or below the lowest cursor it may
2327    /// still rewind to, exactly as `docs/external-wal.md` section 4's
2328    /// GC-coordination rules require. The watermark is caller-supplied and
2329    /// not persisted, so this method cannot detect a violation for you.
2330    ///
2331    /// # Block-skip
2332    ///
2333    /// On SSTs written with the `seqno_bounds` section (`seqno_in_index`), data
2334    /// blocks whose bounds cannot overlap the target window are skipped without
2335    /// being read; SSTs without the section are read and filtered per entry, so
2336    /// mixed trees are handled transparently.
2337    ///
2338    /// # KV-separation
2339    ///
2340    /// Standard trees never store blob-indirected values. On the inner tree of
2341    /// a KV-separated (blob) tree this returns an `Err` for indirected entries:
2342    /// blob resolution into [`ScanSinceEvent::Insert`] is provided by the
2343    /// blob-tree scan path, which owns the blob files.
2344    ///
2345    /// # Corruption resilience
2346    ///
2347    /// The per-block seqno-bounds used for skipping live in the optional
2348    /// `seqno_bounds` SST section, a Block covered by XXH3-128 (+ optional Page
2349    /// ECC) and verified when it is loaded at open, plus a decode that rejects
2350    /// non-ascending offsets and inverted bounds, so a corrupted bound is caught
2351    /// rather than trusted. Even in the impossible case of a fault bypassing
2352    /// those checks, a bad bound can only cause a *missed* record, never a wrong
2353    /// one. Callers who want defense against that hypothetical can use
2354    /// [`Self::scan_since_seqno_full_scan`], which reads every block (slower, no
2355    /// skip).
2356    ///
2357    /// # Panics
2358    ///
2359    /// Panics if the internal version-history lock is poisoned.
2360    ///
2361    /// # Errors
2362    ///
2363    /// Returns `Err` if reading the index or a data block fails, or if an entry
2364    /// is a KV-separated value (see above).
2365    pub fn scan_since_seqno(
2366        &self,
2367        target_seqno: SeqNo,
2368    ) -> crate::Result<impl Iterator<Item = ScanSinceEvent> + use<>> {
2369        // A standard tree never stores blob-indirected values; the resolver
2370        // errors so an indirected entry (only reachable via a blob tree's inner
2371        // index) surfaces as a clear error rather than a wrong event.
2372        self.scan_since_seqno_with(target_seqno, true, |_version, _entry| {
2373            Err(crate::Error::FeatureUnsupported(
2374                "scan_since_seqno on KV-separated values requires the blob-tree scan path",
2375            ))
2376        })
2377    }
2378
2379    /// Paranoid variant of [`Self::scan_since_seqno`] that disables the
2380    /// per-block seqno-bounds skip: every data block is read and filtered per
2381    /// entry, even on seqno-indexed SSTs.
2382    ///
2383    /// # When to use
2384    ///
2385    /// The fast [`Self::scan_since_seqno`] trusts each block's recorded
2386    /// `[seqno_min, seqno_max]` to skip blocks that cannot hold a qualifying
2387    /// record. Those bounds live in the `seqno_bounds` SST section, a Block
2388    /// covered by XXH3-128 (and optional Page ECC) and verified at open, so
2389    /// on-disk corruption is caught, not silently trusted. This method exists
2390    /// for callers who
2391    /// want defense even against a fault that somehow bypassed those checks: a
2392    /// corrupted `seqno_max` can only ever cause a *missed* record (never a
2393    /// wrong one), and a full scan cannot miss. It is slower (no skip), so
2394    /// prefer [`Self::scan_since_seqno`] unless you specifically need this
2395    /// guarantee.
2396    ///
2397    /// # Panics
2398    ///
2399    /// Panics if the internal version-history lock is poisoned.
2400    ///
2401    /// # Errors
2402    ///
2403    /// Same as [`Self::scan_since_seqno`].
2404    pub fn scan_since_seqno_full_scan(
2405        &self,
2406        target_seqno: SeqNo,
2407    ) -> crate::Result<impl Iterator<Item = ScanSinceEvent> + use<>> {
2408        self.scan_since_seqno_with(target_seqno, false, |_version, _entry| {
2409            Err(crate::Error::FeatureUnsupported(
2410                "scan_since_seqno on KV-separated values requires the blob-tree scan path",
2411            ))
2412        })
2413    }
2414
2415    /// Range-scoped variant of [`Self::scan_since_seqno`]: delivers only
2416    /// events whose key falls within `range` (in the tree comparator's
2417    /// order); range-deletion events are delivered when their span OVERLAPS
2418    /// it, since a tombstone reaching into the range affects replay within
2419    /// it. SSTs whose key range cannot intersect the bounds are skipped
2420    /// without a single block read.
2421    ///
2422    /// This is the presence-check primitive for reconciling an external
2423    /// write-ahead log after a repair: [`RepairReport::lost_coverage`] names
2424    /// the affected key ranges, and deciding which retained WAL records to
2425    /// re-apply (in particular, which merge operands SURVIVED and must not be
2426    /// folded twice) only needs the events inside those ranges. See
2427    /// `docs/external-wal.md` § Replay after repair.
2428    ///
2429    /// The history-retention caveat of [`Self::scan_since_seqno`] applies
2430    /// unchanged: the stream is complete only for a `target_seqno` at or
2431    /// above the highest compaction GC watermark ever applied.
2432    ///
2433    /// [`RepairReport::lost_coverage`]: crate::RepairReport::lost_coverage
2434    ///
2435    /// # Panics
2436    ///
2437    /// Panics if the internal version-history lock is poisoned.
2438    ///
2439    /// # Errors
2440    ///
2441    /// Same as [`Self::scan_since_seqno`].
2442    pub fn scan_since_seqno_in_range<K: AsRef<[u8]>, R: RangeBounds<K>>(
2443        &self,
2444        target_seqno: SeqNo,
2445        range: R,
2446    ) -> crate::Result<impl Iterator<Item = ScanSinceEvent> + use<K, R>> {
2447        let bounds = range_to_user_bounds(&range);
2448        self.scan_since_seqno_scoped(
2449            target_seqno,
2450            true,
2451            |_version, _entry| {
2452                Err(crate::Error::FeatureUnsupported(
2453                    "scan_since_seqno on KV-separated values requires the blob-tree scan path",
2454                ))
2455            },
2456            Some(&bounds),
2457        )
2458    }
2459
2460    /// Update the live [`crate::runtime_config::RuntimeConfig`].
2461    ///
2462    /// Mutator runs on a clone of the current snapshot; the new snapshot
2463    /// is then atomically swapped in. Subsequent calls to
2464    /// [`Self::runtime_config`] observe the new snapshot.
2465    ///
2466    /// ## Current scope
2467    ///
2468    /// This API ships the snapshot + atomic-swap mechanism. No write
2469    /// path in the current tree consults `runtime_config` yet — that
2470    /// wiring lands with the V5-batch format features (manifest
2471    /// hardening, per-KV protection, scan-since-seqno) which extend
2472    /// [`RuntimeConfig`](crate::runtime_config::RuntimeConfig) with
2473    /// their own fields and read it at block write / manifest commit /
2474    /// compaction boundaries.
2475    ///
2476    /// ## Designed semantics (effective once wired by V5 features)
2477    ///
2478    /// - Subsequent write paths load the new snapshot lockless on their
2479    ///   next operation.
2480    /// - Existing on-disk data remains in its original format and reads
2481    ///   transparently — every block / manifest is self-describing via
2482    ///   its own header.
2483    /// - Compaction acts as the live-migration mechanism: source blocks
2484    ///   are rewritten per the current snapshot over subsequent cycles,
2485    ///   so all data converges to the current settings without
2486    ///   stop-the-world coordination.
2487    ///
2488    /// ## Concurrency
2489    ///
2490    /// **Reader atomicity:** concurrent readers observe either the old
2491    /// or the new snapshot, never a torn intermediate state.
2492    ///
2493    /// **Writer semantics: last-writer-wins.** Two `update` calls racing
2494    /// from the same starting snapshot will have the second `store`
2495    /// overwrite the first — the first writer's mutation is lost. There
2496    /// is no CAS / RCU merge. Callers that need lost-update avoidance
2497    /// (e.g. two threads concurrently toggling different fields) MUST
2498    /// serialize their `update_runtime_config` calls, typically via a
2499    /// `Mutex` around the call site.
2500    /// # Errors
2501    ///
2502    /// Returns [`crate::Error::PageEccUnsupported`] when the mutator
2503    /// leaves `page_ecc = true` on a binary built without the
2504    /// `page_ecc` cargo feature. The live snapshot stays at its
2505    /// pre-mutation value on error.
2506    pub fn update_runtime_config<F>(&self, mutator: F) -> crate::Result<()>
2507    where
2508        F: FnOnce(&mut crate::runtime_config::RuntimeConfig),
2509    {
2510        // Route through the validating handle path so an invalid
2511        // mutation (currently: `page_ecc = true` on a non-`page_ecc`
2512        // build) is rejected at update time, not silently swallowed
2513        // at the next manifest write.
2514        // Capture this update's `auto_heal` inside the mutation so the read-path
2515        // heal gate reflects exactly the config THIS call commits, rather than a
2516        // separate `load_full()` that could observe a different concurrent
2517        // update's value. Concurrent `update_runtime_config` calls must be
2518        // serialized by the caller (see the last-writer-wins note above); under
2519        // that contract the gate and the committed config stay in sync. On a
2520        // validation error `try_update` does not commit and `?` returns before
2521        // the gate is touched, so it keeps tracking the unchanged config.
2522        let mut auto_heal = false;
2523        self.0.runtime_config.try_update(|c| {
2524            mutator(c);
2525            auto_heal = c.auto_heal;
2526        })?;
2527        self.0.heal_hints.set_enabled(auto_heal);
2528        // Mirror the insert-time digest gate for the write hot path (see
2529        // `TreeInner::kv_digest_at_insert`). Relaxed: a toggle taking effect
2530        // on the next inserts is the documented contract (mixed inserts are
2531        // supported), so no ordering against other memory is needed.
2532        let gate = inner::kv_digest_at_insert_gate(&self.0.runtime_config.load());
2533        self.0
2534            .kv_digest_at_insert
2535            .store(gate, core::sync::atomic::Ordering::Relaxed);
2536        // Drop the cached admission footprint so the next check re-probes
2537        // disk-free: an operator who just raised the budget (or freed disk)
2538        // should see it promptly, not at the next flush.
2539        *self.0.admission_used_cache.lock() = None;
2540        Ok(())
2541    }
2542
2543    /// Snapshot of the current runtime config. Cheap atomic load —
2544    /// safe to call on hot paths.
2545    #[must_use]
2546    pub fn runtime_config(&self) -> Arc<crate::runtime_config::RuntimeConfig> {
2547        self.0.runtime_config.load_full()
2548    }
2549
2550    /// Shared handle to this tree's ECC heal-hint queue.
2551    ///
2552    /// A read that recovers a block from Page-ECC parity records the owning SST
2553    /// here (when the on-disk fault is confirmed persistent). Pass the handle to
2554    /// [`compaction::EccHeal`](crate::compaction::EccHeal) and run that strategy
2555    /// via [`Tree::compact`](crate::AbstractTree::compact) — leader-only in a
2556    /// clustered deployment — to rewrite the flagged SSTs clean. Check
2557    /// [`HealHints::is_empty`](crate::heal_hints::HealHints::is_empty) to skip
2558    /// the pass when nothing is queued.
2559    ///
2560    /// # Examples
2561    ///
2562    /// ```no_run
2563    /// use lsm_tree::{AbstractTree, AnyTree, Config, SequenceNumberCounter, compaction::EccHeal};
2564    /// use std::sync::Arc;
2565    /// # fn main() -> lsm_tree::Result<()> {
2566    /// let AnyTree::Standard(tree) = Config::new(
2567    ///     "/tmp/db",
2568    ///     SequenceNumberCounter::default(),
2569    ///     SequenceNumberCounter::default(),
2570    /// )
2571    /// .open()?
2572    /// else {
2573    ///     return Ok(());
2574    /// };
2575    ///
2576    /// // Opt into rewrite scheduling; reads that recover a block from parity now
2577    /// // flag its SST for healing.
2578    /// tree.update_runtime_config(|c| c.auto_heal = true)?;
2579    ///
2580    /// // Drain the queue, rewriting each flagged SST clean (leader-only in a
2581    /// // clustered deployment).
2582    /// let hints = tree.heal_hints();
2583    /// while !hints.is_empty() {
2584    ///     tree.compact(Arc::new(EccHeal::new(tree.heal_hints(), u64::MAX)), 0)?;
2585    /// }
2586    /// # Ok(())
2587    /// # }
2588    /// ```
2589    #[must_use]
2590    pub fn heal_hints(&self) -> Arc<crate::heal_hints::HealHints> {
2591        Arc::clone(&self.0.heal_hints)
2592    }
2593
2594    /// Shared point-read logic for `get()` and `multi_get()`: finds the newest
2595    /// entry, applies merge resolution or RT suppression, and returns the value.
2596    fn resolve_or_passthrough(
2597        super_version: &SuperVersion,
2598        key: &[u8],
2599        seqno: SeqNo,
2600        merge_operator: Option<&Arc<dyn crate::merge_operator::MergeOperator>>,
2601        comparator: &dyn crate::comparator::UserComparator,
2602    ) -> crate::Result<Option<UserValue>> {
2603        let entry = Self::get_value(super_version, key, seqno, comparator)?;
2604
2605        match entry {
2606            Some((ValueType::MergeOperand, entry_seqno, value)) => {
2607                if let Some(merge_op) = merge_operator {
2608                    // Build a bloom-filtered single-key iterator pipeline that
2609                    // reuses MvccStream for merge/RT/Indirection resolution,
2610                    // eliminating the previous hand-rolled merge collection.
2611                    Self::resolve_merge_via_pipeline(
2612                        super_version.clone(),
2613                        key,
2614                        seqno,
2615                        Arc::clone(merge_op),
2616                    )
2617                } else if Self::is_suppressed_by_range_tombstones(
2618                    super_version,
2619                    key,
2620                    entry_seqno,
2621                    seqno,
2622                    comparator,
2623                ) {
2624                    Ok(None)
2625                } else {
2626                    Ok(Some(value))
2627                }
2628            }
2629            Some((_, _, value)) => Ok(Some(value)),
2630            None => Ok(None),
2631        }
2632    }
2633
2634    /// Shared post-lookup resolution for `get_pinned` and `multi_get`:
2635    /// tombstone filter, range-tombstone suppression, merge operand resolution.
2636    /// Returns `None` if entry is tombstoned or suppressed.
2637    fn resolve_pinned_entry(
2638        super_version: &SuperVersion,
2639        key: &[u8],
2640        entry: InternalValue,
2641        seqno: SeqNo,
2642        merge_operator: Option<&Arc<dyn crate::merge_operator::MergeOperator>>,
2643        comparator: &dyn crate::comparator::UserComparator,
2644        wrap: impl FnOnce(UserValue) -> crate::PinnableSlice,
2645    ) -> crate::Result<Option<crate::PinnableSlice>> {
2646        use crate::PinnableSlice;
2647
2648        let Some(entry) = ignore_tombstone_value(entry) else {
2649            return Ok(None);
2650        };
2651        if Self::is_suppressed_by_range_tombstones(
2652            super_version,
2653            key,
2654            entry.key.seqno,
2655            seqno,
2656            comparator,
2657        ) {
2658            return Ok(None);
2659        }
2660        if entry.key.value_type == ValueType::MergeOperand
2661            && let Some(merge_op) = merge_operator
2662        {
2663            // Merge resolution always produces Owned (pipeline result).
2664            return Self::resolve_merge_via_pipeline(
2665                super_version.clone(),
2666                key,
2667                seqno,
2668                Arc::clone(merge_op),
2669            )
2670            .map(|opt| opt.map(PinnableSlice::owned));
2671        }
2672        Ok(Some(wrap(entry.value)))
2673    }
2674
2675    /// Like [`Tree::resolve_or_passthrough`], but returns a [`PinnableSlice`](crate::PinnableSlice)
2676    /// that may keep the decompressed block buffer alive.
2677    fn resolve_or_passthrough_pinned(
2678        super_version: &SuperVersion,
2679        key: &[u8],
2680        seqno: SeqNo,
2681        merge_operator: Option<&Arc<dyn crate::merge_operator::MergeOperator>>,
2682        comparator: &dyn crate::comparator::UserComparator,
2683    ) -> crate::Result<Option<crate::PinnableSlice>> {
2684        use crate::PinnableSlice;
2685
2686        // Check memtables first — always Owned
2687        if let Some(entry) = super_version.active_memtable.get(key, seqno) {
2688            return Self::resolve_pinned_entry(
2689                super_version,
2690                key,
2691                entry,
2692                seqno,
2693                merge_operator,
2694                comparator,
2695                PinnableSlice::owned,
2696            );
2697        }
2698
2699        // Sealed memtables — always Owned
2700        if let Some(entry) =
2701            Self::get_internal_entry_from_sealed_memtables(super_version, key, seqno)
2702        {
2703            return Self::resolve_pinned_entry(
2704                super_version,
2705                key,
2706                entry,
2707                seqno,
2708                merge_operator,
2709                comparator,
2710                PinnableSlice::owned,
2711            );
2712        }
2713
2714        // Tables — Pinned (value shares decompressed block buffer)
2715        let key_hash = crate::hash::hash64(key);
2716
2717        if let Some((entry, block)) = Self::get_internal_entry_with_block_from_tables(
2718            &super_version.version,
2719            key,
2720            seqno,
2721            key_hash,
2722            comparator,
2723        )? {
2724            return Self::resolve_pinned_entry(
2725                super_version,
2726                key,
2727                entry,
2728                seqno,
2729                merge_operator,
2730                comparator,
2731                |value| PinnableSlice::pinned(block, value),
2732            );
2733        }
2734
2735        Ok(None)
2736    }
2737
2738    /// Like [`Tree::get_internal_entry_from_tables`], but returns the block
2739    /// along with the entry for pinned zero-copy access.
2740    fn get_internal_entry_with_block_from_tables(
2741        version: &Version,
2742        key: &[u8],
2743        seqno: SeqNo,
2744        key_hash: u64,
2745        comparator: &dyn crate::comparator::UserComparator,
2746    ) -> crate::Result<Option<(InternalValue, crate::table::Block)>> {
2747        Self::find_in_tables::<TableEntryWithBlock>(version, key, seqno, key_hash, comparator)
2748    }
2749
2750    /// Resolves merge operands for a point read via a bloom-filtered iterator pipeline.
2751    ///
2752    /// Builds a single-key range (`key..=key`) with bloom pre-filtering, wraps
2753    /// all sources in `Merger → MvccStream`, and takes the first result. This
2754    /// reuses the unified merge/RT/Indirection resolution logic from `MvccStream`
2755    /// instead of duplicating it in a hand-rolled collection loop.
2756    ///
2757    /// Bloom pre-filtering can reject many disk tables at the filter level,
2758    /// which typically improves point-read performance on deep LSM trees.
2759    pub(crate) fn resolve_merge_via_pipeline(
2760        version: SuperVersion,
2761        key: &[u8],
2762        seqno: SeqNo,
2763        merge_operator: Arc<dyn crate::merge_operator::MergeOperator>,
2764    ) -> crate::Result<Option<UserValue>> {
2765        use crate::range::{IterState, TreeIter};
2766
2767        let key_hash = crate::hash::hash64(key);
2768        // NOTE: Slice::from(&[u8]) copies the key (small, typically < 100 bytes).
2769        // This runs once per merge resolution, not per-table — cost is negligible
2770        // compared to the I/O saved by partition-aware bloom filtering.
2771        let bloom_key = crate::Slice::from(key);
2772        let comparator = version.active_memtable.comparator.clone();
2773
2774        let iter_state = IterState {
2775            version,
2776            ephemeral: None,
2777            merge_operator: Some(merge_operator),
2778            comparator,
2779            prefix_hash: None,
2780            key_hash: Some(key_hash),
2781            bloom_key: Some(bloom_key),
2782            #[cfg(feature = "metrics")]
2783            metrics: None,
2784        };
2785
2786        // Point-read fast path: skips eager RT collection, sort+dedup, table-skip,
2787        // and RangeTombstoneFilter wrapper. MvccStream handles merge-internal RT
2788        // suppression; a post-merge linear RT check catches the rest.
2789        let mut iter = TreeIter::create_range_point(iter_state, key, seqno);
2790
2791        match iter.next() {
2792            Some(Ok(entry)) => Ok(Some(entry.value)),
2793            Some(Err(e)) => Err(e),
2794            None => Ok(None),
2795        }
2796    }
2797
2798    #[doc(hidden)]
2799    pub fn create_internal_range<'a, K: AsRef<[u8]> + 'a, R: RangeBounds<K> + 'a>(
2800        version: SuperVersion,
2801        range: &'a R,
2802        seqno: SeqNo,
2803        ephemeral: Option<(Arc<Memtable>, SeqNo)>,
2804        merge_operator: Option<Arc<dyn crate::merge_operator::MergeOperator>>,
2805        comparator: crate::comparator::SharedComparator,
2806    ) -> impl DoubleEndedIterator<Item = crate::Result<InternalValue>> + 'static {
2807        Self::create_internal_range_with_prefix_hash(
2808            version,
2809            range,
2810            seqno,
2811            ephemeral,
2812            merge_operator,
2813            comparator,
2814            None,
2815        )
2816    }
2817
2818    /// Like [`Tree::create_internal_range`], but with an optional prefix hash
2819    /// for prefix bloom filter skipping during prefix scans.
2820    #[doc(hidden)]
2821    pub(crate) fn create_internal_range_with_prefix_hash<
2822        'a,
2823        K: AsRef<[u8]> + 'a,
2824        R: RangeBounds<K> + 'a,
2825    >(
2826        version: SuperVersion,
2827        range: &'a R,
2828        seqno: SeqNo,
2829        ephemeral: Option<(Arc<Memtable>, SeqNo)>,
2830        merge_operator: Option<Arc<dyn crate::merge_operator::MergeOperator>>,
2831        comparator: crate::comparator::SharedComparator,
2832        prefix_hash: Option<u64>,
2833    ) -> impl DoubleEndedIterator<Item = crate::Result<InternalValue>> + 'static {
2834        use crate::range::{IterState, TreeIter};
2835        use core::ops::Bound::{self, Excluded, Included, Unbounded};
2836
2837        let lo: Bound<UserKey> = match range.start_bound() {
2838            Included(x) => Included(x.as_ref().into()),
2839            Excluded(x) => Excluded(x.as_ref().into()),
2840            Unbounded => Unbounded,
2841        };
2842
2843        let hi: Bound<UserKey> = match range.end_bound() {
2844            Included(x) => Included(x.as_ref().into()),
2845            Excluded(x) => Excluded(x.as_ref().into()),
2846            Unbounded => Unbounded,
2847        };
2848
2849        let bounds: (Bound<UserKey>, Bound<UserKey>) = (lo, hi);
2850
2851        let iter_state = IterState {
2852            version,
2853            ephemeral,
2854            merge_operator,
2855            comparator,
2856            prefix_hash,
2857            key_hash: None,
2858            bloom_key: None,
2859            #[cfg(feature = "metrics")]
2860            metrics: None,
2861        };
2862
2863        TreeIter::create_range(iter_state, bounds, seqno)
2864    }
2865
2866    pub(crate) fn get_internal_entry_from_version(
2867        super_version: &SuperVersion,
2868        key: &[u8],
2869        seqno: SeqNo,
2870        comparator: &dyn crate::comparator::UserComparator,
2871    ) -> crate::Result<Option<InternalValue>> {
2872        // Search order: active → sealed → SST (newest first). A point
2873        // tombstone in a newer source is authoritative — no older source
2874        // can contain a newer value, so returning None is correct.
2875        if let Some(entry) = super_version.active_memtable.get(key, seqno) {
2876            let Some(entry) = ignore_tombstone_value(entry) else {
2877                return Ok(None);
2878            };
2879
2880            // Check if any range tombstone suppresses this entry
2881            if Self::is_suppressed_by_range_tombstones(
2882                super_version,
2883                key,
2884                entry.key.seqno,
2885                seqno,
2886                comparator,
2887            ) {
2888                return Ok(None);
2889            }
2890            return Ok(Some(entry));
2891        }
2892
2893        // Now look in sealed memtables
2894        if let Some(entry) =
2895            Self::get_internal_entry_from_sealed_memtables(super_version, key, seqno)
2896        {
2897            let Some(entry) = ignore_tombstone_value(entry) else {
2898                return Ok(None);
2899            };
2900
2901            if Self::is_suppressed_by_range_tombstones(
2902                super_version,
2903                key,
2904                entry.key.seqno,
2905                seqno,
2906                comparator,
2907            ) {
2908                return Ok(None);
2909            }
2910            return Ok(Some(entry));
2911        }
2912
2913        // Now look in tables... this may involve disk I/O
2914        let entry =
2915            Self::get_internal_entry_from_tables(&super_version.version, key, seqno, comparator)?;
2916
2917        if let Some(entry) = entry {
2918            if Self::is_suppressed_by_range_tombstones(
2919                super_version,
2920                key,
2921                entry.key.seqno,
2922                seqno,
2923                comparator,
2924            ) {
2925                return Ok(None);
2926            }
2927            return Ok(Some(entry));
2928        }
2929
2930        Ok(None)
2931    }
2932
2933    /// Value-only mirror of [`Self::get_internal_entry_from_version`].
2934    ///
2935    /// Returns `(value_type, seqno, value)` for the newest visible entry without
2936    /// reconstructing the entry key. Same search order (active -> sealed -> SST,
2937    /// newest first), tombstone filtering, and range-tombstone suppression; only
2938    /// the SST path differs, using the value-only [`TableValue`] lookup that
2939    /// skips the delta-key fusion of the full `InternalValue` path. Used by the
2940    /// value-returning `get` path, which never reads the matched key.
2941    pub(crate) fn get_value(
2942        super_version: &SuperVersion,
2943        key: &[u8],
2944        seqno: SeqNo,
2945        comparator: &dyn crate::comparator::UserComparator,
2946    ) -> crate::Result<Option<(ValueType, SeqNo, crate::Slice)>> {
2947        if let Some(entry) = super_version.active_memtable.get(key, seqno) {
2948            let Some(entry) = ignore_tombstone_value(entry) else {
2949                return Ok(None);
2950            };
2951            if Self::is_suppressed_by_range_tombstones(
2952                super_version,
2953                key,
2954                entry.key.seqno,
2955                seqno,
2956                comparator,
2957            ) {
2958                return Ok(None);
2959            }
2960            return Ok(Some((entry.key.value_type, entry.key.seqno, entry.value)));
2961        }
2962
2963        if let Some(entry) =
2964            Self::get_internal_entry_from_sealed_memtables(super_version, key, seqno)
2965        {
2966            let Some(entry) = ignore_tombstone_value(entry) else {
2967                return Ok(None);
2968            };
2969            if Self::is_suppressed_by_range_tombstones(
2970                super_version,
2971                key,
2972                entry.key.seqno,
2973                seqno,
2974                comparator,
2975            ) {
2976                return Ok(None);
2977            }
2978            return Ok(Some((entry.key.value_type, entry.key.seqno, entry.value)));
2979        }
2980
2981        let key_hash = crate::hash::hash64(key);
2982        let entry = Self::find_in_tables::<TableValue>(
2983            &super_version.version,
2984            key,
2985            seqno,
2986            key_hash,
2987            comparator,
2988        )?;
2989        if let Some((value_type, entry_seqno, value)) = entry {
2990            if Self::is_suppressed_by_range_tombstones(
2991                super_version,
2992                key,
2993                entry_seqno,
2994                seqno,
2995                comparator,
2996            ) {
2997                return Ok(None);
2998            }
2999            return Ok(Some((value_type, entry_seqno, value)));
3000        }
3001
3002        Ok(None)
3003    }
3004
3005    /// Checks if a key at `key_seqno` is suppressed by any range tombstone
3006    /// in the active memtable, sealed memtables, or SST tables, visible at `read_seqno`.
3007    pub(crate) fn is_suppressed_by_range_tombstones(
3008        super_version: &SuperVersion,
3009        key: &[u8],
3010        key_seqno: SeqNo,
3011        read_seqno: SeqNo,
3012        comparator: &dyn crate::comparator::UserComparator,
3013    ) -> bool {
3014        // Check active memtable range tombstones.
3015        // Future optimization: skip lock when memtable has no RTs (atomic count).
3016        if super_version
3017            .active_memtable
3018            .is_key_suppressed_by_range_tombstone(key, key_seqno, read_seqno)
3019        {
3020            return true;
3021        }
3022
3023        // Check sealed memtable range tombstones
3024        for mt in super_version.sealed_memtables.iter().rev() {
3025            if mt.is_key_suppressed_by_range_tombstone(key, key_seqno, read_seqno) {
3026                return true;
3027            }
3028        }
3029
3030        // Check SST table range tombstones.
3031        //
3032        // Per-table RT lists are sorted by start key (using comparator) on load,
3033        // so binary search narrows candidates to RTs with start <= key.
3034        // The key_range early reject uses the comparator so it works with
3035        // non-lexicographic orderings.
3036        for table in super_version
3037            .version
3038            .iter_levels()
3039            .flat_map(|lvl| lvl.iter())
3040            .flat_map(|run| run.iter())
3041            .filter(|t| !t.range_tombstones().is_empty())
3042            .filter(|t| {
3043                // Early reject: skip tables whose key range doesn't contain the key.
3044                let kr = &t.metadata.key_range;
3045                comparator.compare(kr.min(), key) != core::cmp::Ordering::Greater
3046                    && comparator.compare(key, kr.max()) != core::cmp::Ordering::Greater
3047            })
3048        {
3049            let rts = table.range_tombstones();
3050
3051            // Binary search: find the first RT whose start is > key (in comparator order).
3052            // All RTs before that index have start <= key and are candidates.
3053            let candidate_end = rts.partition_point(|rt| {
3054                comparator.compare(&rt.start, key) != core::cmp::Ordering::Greater
3055            });
3056
3057            for rt in rts.iter().take(candidate_end) {
3058                // Check: start <= key < end (in comparator order) AND seqno visibility.
3059                if rt.visible_at(read_seqno)
3060                    && comparator.compare(&rt.start, key) != core::cmp::Ordering::Greater
3061                    && comparator.compare(key, &rt.end) == core::cmp::Ordering::Less
3062                    && key_seqno < rt.seqno
3063                {
3064                    return true;
3065                }
3066            }
3067        }
3068
3069        false
3070    }
3071
3072    /// Resolves a single internal entry into a user value, handling tombstones,
3073    /// range tombstone suppression, and merge operand resolution.
3074    /// Resolves an entry for `multi_get`: tombstone filter, RT suppression,
3075    /// merge operand resolution. Delegates to [`Self::resolve_pinned_entry`] with
3076    /// `Owned` wrapping, then extracts the value.
3077    fn resolve_entry(
3078        super_version: &SuperVersion,
3079        key: &[u8],
3080        entry: Option<InternalValue>,
3081        seqno: SeqNo,
3082        merge_operator: Option<&Arc<dyn crate::merge_operator::MergeOperator>>,
3083        comparator: &dyn crate::comparator::UserComparator,
3084    ) -> crate::Result<Option<UserValue>> {
3085        let Some(entry) = entry else {
3086            return Ok(None);
3087        };
3088        Self::resolve_pinned_entry(
3089            super_version,
3090            key,
3091            entry,
3092            seqno,
3093            merge_operator,
3094            comparator,
3095            crate::PinnableSlice::owned,
3096        )
3097        .map(|opt| opt.map(crate::PinnableSlice::into_value))
3098    }
3099
3100    /// De-duplicates equal query keys in a comparator-sorted `remaining` index
3101    /// list, returning the `(key_index, bloom_hash)` pairs for the batched
3102    /// on-disk resolver (which requires strictly-sorted-unique input) and a
3103    /// `(duplicate_index, representative_index)` map. Pair with
3104    /// [`Self::fan_out_duplicates`] after the batch resolves.
3105    ///
3106    /// Shared by [`Self::multi_get`] and the `BlobTree` multi-get so the two
3107    /// cannot silently diverge: forwarding duplicate miss keys into the
3108    /// strictly-sorted-unique resolver was exactly the regression class this
3109    /// guards against. `remaining` must already be sorted by `comparator`.
3110    #[expect(
3111        clippy::indexing_slicing,
3112        reason = "remaining/miss_keys carry batch-local key indices < keys.len()"
3113    )]
3114    pub(crate) fn dedup_sorted_miss_keys<K: AsRef<[u8]>>(
3115        remaining: &[usize],
3116        keys: &[K],
3117        comparator: &dyn crate::comparator::UserComparator,
3118    ) -> DedupedMissKeys {
3119        let mut miss_keys: Vec<(usize, u64)> = Vec::with_capacity(remaining.len());
3120        let mut duplicates: Vec<(usize, usize)> = Vec::new();
3121        for &idx in remaining {
3122            let key = keys[idx].as_ref();
3123            match miss_keys.last() {
3124                Some(&(rep_idx, _))
3125                    if comparator.compare(keys[rep_idx].as_ref(), key)
3126                        == core::cmp::Ordering::Equal =>
3127                {
3128                    duplicates.push((idx, rep_idx));
3129                }
3130                _ => miss_keys.push((idx, crate::hash::hash64(key))),
3131            }
3132        }
3133        (miss_keys, duplicates)
3134    }
3135
3136    /// Fans each representative's resolved entry out to its duplicate positions,
3137    /// so every input slot carries the same answer the per-key path would have
3138    /// produced. Counterpart to [`Self::dedup_sorted_miss_keys`]; call after the
3139    /// batched resolver fills `internal_entries`.
3140    #[expect(
3141        clippy::indexing_slicing,
3142        reason = "duplicate/representative indices are batch-local key indices < entries.len()"
3143    )]
3144    pub(crate) fn fan_out_duplicates(
3145        duplicates: &[(usize, usize)],
3146        internal_entries: &mut [Option<InternalValue>],
3147    ) {
3148        for &(dup_idx, rep_idx) in duplicates {
3149            let resolved = internal_entries[rep_idx].clone();
3150            internal_entries[dup_idx] = resolved;
3151        }
3152    }
3153
3154    /// Queries tables for multiple keys using sorted access order.
3155    ///
3156    /// `miss_keys` contains `(key_index, bloom_hash)` pairs for keys not yet
3157    /// found, in comparator-sorted order. Keys are looked up individually via
3158    /// `Table::get`, but sorted order improves I/O locality. The precomputed
3159    /// bloom hash in each pair is reused across all table probes. Per-SST
3160    /// batched bloom checks and block walks are tracked in `#223`.
3161    #[expect(
3162        clippy::indexing_slicing,
3163        reason = "miss_keys entries carry batch-local indices; callers must pass a results slice aligned with keys"
3164    )]
3165    pub(crate) fn batch_get_from_tables<K: AsRef<[u8]>>(
3166        version: &Version,
3167        keys: &[K],
3168        miss_keys: Vec<(usize, u64)>,
3169        seqno: SeqNo,
3170        comparator: &dyn crate::comparator::UserComparator,
3171        fs: &dyn crate::fs::Fs,
3172        results: &mut [Option<InternalValue>],
3173    ) -> crate::Result<()> {
3174        debug_assert_eq!(results.len(), keys.len());
3175        debug_assert!(miss_keys.iter().all(|&(i, _)| i < keys.len()));
3176
3177        // Consume the caller's Vec directly — no allocation+copy.
3178        let mut still_remaining = miss_keys;
3179
3180        for (level_idx, level) in version.iter_levels().enumerate() {
3181            if still_remaining.is_empty() {
3182                break;
3183            }
3184
3185            // Warm the cold data blocks this level will read across ALL its SSTs
3186            // in one cross-file batched read, so the serial resolve below hits the
3187            // cache. On io_uring the reads coalesce into one submission and the
3188            // kernel fans them out across the underlying devices. When the cold
3189            // working set is too large to warm without thrashing the cache, this
3190            // signals oversize and warms nothing; the level is then resolved by
3191            // reading its blocks in budget-sized chunks into a scratch and
3192            // point-reading directly (no cache, no eviction).
3193            if Self::prewarm_level_cross_sst(fs, level, &still_remaining, keys, seqno, comparator)
3194                && Self::resolve_level_chunked(
3195                    fs,
3196                    level,
3197                    &mut still_remaining,
3198                    keys,
3199                    seqno,
3200                    comparator,
3201                    results,
3202                )?
3203            {
3204                continue;
3205            }
3206
3207            if level_idx == 0 {
3208                // L0: must check ALL runs, keep highest seqno per key. Track keys
3209                // at the seqno ceiling (seqno + 1 == read_seqno): no other L0 run
3210                // can beat them, so skip them in subsequent runs. The bitmap is
3211                // dense over 0..keys.len().
3212                let mut at_ceiling = vec![false; keys.len()];
3213
3214                for run in level.iter() {
3215                    // `at_ceiling` is read as this run's skip set (a key is visited
3216                    // once per run, so the updates below only affect later runs)
3217                    // and mutated from the returned outcomes: never both at once.
3218                    let resolved = Self::resolve_run_batched(
3219                        run,
3220                        &still_remaining,
3221                        keys,
3222                        seqno,
3223                        comparator,
3224                        |idx| at_ceiling[idx],
3225                    )?;
3226                    for (idx, _hash, item) in resolved.covered {
3227                        let Some(item) = item else { continue };
3228                        match &results[idx] {
3229                            Some(current) if current.key.seqno >= item.key.seqno => {}
3230                            _ => {
3231                                if item.key.seqno.checked_add(1) == Some(seqno) {
3232                                    at_ceiling[idx] = true;
3233                                }
3234                                results[idx] = Some(item);
3235                            }
3236                        }
3237                    }
3238                    // Uncovered keys stay in `still_remaining`; the retain below
3239                    // prunes the ones any run resolved.
3240                }
3241
3242                // Remove found keys (both values and tombstones)
3243                still_remaining.retain(|&(idx, _)| results[idx].is_none());
3244            } else {
3245                // L1+ runs have non-overlapping key ranges within a level. A
3246                // covering run resolves a key definitively: a hit sets the result,
3247                // a covering miss drops it to lower levels (`covered_miss`), and an
3248                // uncovered key tries the next run in this level (`not_covered`).
3249                let mut covered_miss: Vec<(usize, u64)> = Vec::new();
3250
3251                for run in level.iter() {
3252                    let resolved = Self::resolve_run_batched(
3253                        run,
3254                        &still_remaining,
3255                        keys,
3256                        seqno,
3257                        comparator,
3258                        |_| false,
3259                    )?;
3260                    for (idx, hash, item) in resolved.covered {
3261                        if let Some(item) = item {
3262                            results[idx] = Some(item);
3263                        } else {
3264                            // Covering run found, key absent: no other run in this
3265                            // level can have it. Keep for lower levels.
3266                            covered_miss.push((idx, hash));
3267                        }
3268                    }
3269                    still_remaining = resolved.not_covered;
3270                }
3271
3272                // Merge back: keys without a covering run + keys with a covering
3273                // miss both proceed to lower levels. Re-sort to preserve
3274                // comparator order for the next level's sequential scan.
3275                let needs_sort = !covered_miss.is_empty();
3276                still_remaining.extend(covered_miss);
3277                if needs_sort {
3278                    still_remaining.sort_by(|&(a, _), &(b, _)| {
3279                        comparator.compare(keys[a].as_ref(), keys[b].as_ref())
3280                    });
3281                }
3282            }
3283        }
3284
3285        Ok(())
3286    }
3287
3288    /// Resolves `remaining` (sorted ascending under `comparator`) against a
3289    /// single run with per-table batched gets instead of a per-key `table.get`:
3290    /// consecutive keys covered by the same table within the run share one
3291    /// [`Table::batch_get`], so co-located keys decode their data block once.
3292    /// Byte-identical to per-key resolution (the same point reads, the same
3293    /// values). `skip(idx)` omits a key (e.g. one already pinned at the L0 seqno
3294    /// ceiling, where no later run can beat it).
3295    ///
3296    /// Returns, per covered non-skipped key, `(idx, hash, resolved item)` in
3297    /// input order, plus the keys this run does not cover (also in input order)
3298    /// for the caller to pass to the next run or level.
3299    #[expect(
3300        clippy::indexing_slicing,
3301        reason = "i < remaining.len() is loop-checked; idx values are valid key indices (caller's keys/results are aligned, same as batch_get_from_tables)"
3302    )]
3303    fn resolve_run_batched<K: AsRef<[u8]>>(
3304        run: &crate::version::Run<crate::Table>,
3305        remaining: &[(usize, u64)],
3306        keys: &[K],
3307        seqno: SeqNo,
3308        comparator: &dyn crate::comparator::UserComparator,
3309        skip: impl Fn(usize) -> bool,
3310    ) -> crate::Result<RunResolve> {
3311        let mut covered: Vec<CoveredKey> = Vec::new();
3312        let mut not_covered: Vec<(usize, u64)> = Vec::new();
3313
3314        // One pair of buffers reused across the run's tables, cleared per
3315        // table. Each is consumed before the next table fills it, so a fresh
3316        // Vec per table would only re-allocate the capacity the previous one
3317        // just released — once per table on a batched read path.
3318        let mut batch: Vec<(&[u8], u64)> = Vec::new();
3319        let mut batch_keys: Vec<(usize, u64)> = Vec::new();
3320
3321        let mut i = 0;
3322        while i < remaining.len() {
3323            let (idx, hash) = remaining[i];
3324            if skip(idx) {
3325                i += 1;
3326                continue;
3327            }
3328            let key = keys[idx].as_ref();
3329            let Some(table) = run.get_for_key_cmp(key, comparator) else {
3330                not_covered.push((idx, hash));
3331                i += 1;
3332                continue;
3333            };
3334
3335            // Gather the contiguous, non-skipped keys covered by THIS table. The
3336            // input is sorted and a run's tables partition the key space, so the
3337            // keys for one table form a contiguous slice; one `batch_get` drains
3338            // them with a single block decode for co-located keys.
3339            let table_id = table.id();
3340            batch.clear();
3341            batch_keys.clear();
3342            while i < remaining.len() {
3343                let (jdx, jhash) = remaining[i];
3344                if skip(jdx) {
3345                    i += 1;
3346                    continue;
3347                }
3348                let jkey = keys[jdx].as_ref();
3349                match run.get_for_key_cmp(jkey, comparator) {
3350                    Some(t) if t.id() == table_id => {
3351                        batch.push((jkey, jhash));
3352                        batch_keys.push((jdx, jhash));
3353                        i += 1;
3354                    }
3355                    _ => break,
3356                }
3357            }
3358
3359            // `drain`, not `into_iter`: the buffer is reused by the next table,
3360            // so its capacity has to survive the walk. The lint assumes the
3361            // vector is dead afterwards, which is exactly what this is not.
3362            #[expect(
3363                clippy::iter_with_drain,
3364                reason = "buffer is reused across tables; into_iter would consume it"
3365            )]
3366            for ((kidx, khash), item) in batch_keys.drain(..).zip(table.batch_get(&batch, seqno)?) {
3367                covered.push((kidx, khash, item));
3368            }
3369        }
3370
3371        Ok(RunResolve {
3372            covered,
3373            not_covered,
3374        })
3375    }
3376
3377    /// Warms an entire level's COLD data blocks across ALL its SSTs in one
3378    /// cross-file batched read ([`crate::fs::Fs::read_blocks_batched`]), so the
3379    /// serial resolve walk that follows hits the cache. On `io_uring` the reads of
3380    /// many SSTs (and, on a multi-device filesystem, many physical devices)
3381    /// coalesce into one submission and overlap in flight.
3382    ///
3383    /// Purely best-effort: it never changes a query result (the resolve walk
3384    /// re-reads authoritatively), and it is size-bounded to at most half the
3385    /// shared cache so the warmed blocks survive until the walk reads them.
3386    ///
3387    /// Returns `true` when the level's cold working set EXCEEDS that half-cache
3388    /// bound: warming would thrash the cache, so nothing is warmed and the caller
3389    /// resolves the level with the chunked read-into-scratch path instead
3390    /// ([`Tree::resolve_level_chunked`]). Returns `false` when it warmed the
3391    /// blocks (or had nothing to warm), i.e. the serial resolve should run.
3392    #[expect(
3393        clippy::indexing_slicing,
3394        reason = "planned[ti] and all_buffers[k..end] indices are built from `planned` itself, so they are in range by construction"
3395    )]
3396    fn prewarm_level_cross_sst<K: AsRef<[u8]>>(
3397        fs: &dyn crate::fs::Fs,
3398        level: &crate::version::Level,
3399        remaining: &[(usize, u64)],
3400        keys: &[K],
3401        seqno: SeqNo,
3402        comparator: &dyn crate::comparator::UserComparator,
3403    ) -> bool {
3404        // Gather per-table prewarm plans across the level's runs (group remaining
3405        // keys by covering table, mirroring resolve_run_batched's walk).
3406        let mut planned: Vec<(
3407            &crate::Table,
3408            Arc<dyn crate::fs::FsFile>,
3409            Vec<crate::table::BlockHandle>,
3410        )> = Vec::new();
3411        // Reused across the level's tables, cleared per table: `plan_prewarm`
3412        // reads it and returns before the next table fills it.
3413        let mut batch: Vec<(&[u8], u64)> = Vec::new();
3414        for run in level.iter() {
3415            let mut i = 0;
3416            while i < remaining.len() {
3417                let (idx, _) = remaining[i];
3418                let key = keys[idx].as_ref();
3419                let Some(table) = run.get_for_key_cmp(key, comparator) else {
3420                    i += 1;
3421                    continue;
3422                };
3423                let table_id = table.id();
3424                batch.clear();
3425                while i < remaining.len() {
3426                    let (jdx, jhash) = remaining[i];
3427                    let jkey = keys[jdx].as_ref();
3428                    match run.get_for_key_cmp(jkey, comparator) {
3429                        Some(t) if t.id() == table_id => {
3430                            batch.push((jkey, jhash));
3431                            i += 1;
3432                        }
3433                        _ => break,
3434                    }
3435                }
3436                if let Some((file, handles)) = table.plan_prewarm(&batch, seqno) {
3437                    planned.push((table, file, handles));
3438                }
3439            }
3440        }
3441
3442        let total_cold: usize = planned.iter().map(|(_, _, h)| h.len()).sum();
3443        if total_cold < 2 {
3444            return false;
3445        }
3446        // Eviction-avoiding bound: warm at most half the (shared) cache.
3447        let Some((first_table, _, _)) = planned.first() else {
3448            return false;
3449        };
3450        let cap = first_table.cache_capacity();
3451        let total_bytes: u64 = planned
3452            .iter()
3453            .flat_map(|(_, _, h)| h.iter().map(|x| u64::from(x.size())))
3454            .sum();
3455        if cap == 0 {
3456            return false;
3457        }
3458        if total_bytes > cap / 2 {
3459            // Cold working set too large to warm without thrash: signal the caller
3460            // to resolve this level via the chunked read-into-scratch path.
3461            return true;
3462        }
3463
3464        // One buffer per cold block, in (table, block) order.
3465        //
3466        // One buffer per cold block, in (table, block) order.
3467        let mut all_buffers: Vec<Vec<u8>> = planned
3468            .iter()
3469            .flat_map(|(_, _, handles)| handles.iter().map(|h| vec![0u8; h.size() as usize]))
3470            .collect();
3471
3472        {
3473            // Paired directly with the plan rather than through a parallel
3474            // (table index, offset) vector: `all_buffers` is already in
3475            // (table, block) order, so zipping the two walks needs no third
3476            // collection to remember which file each buffer belongs to.
3477            let mut bufs = all_buffers.iter_mut();
3478            let mut reqs: Vec<crate::fs::BlockRead<'_>> = planned
3479                .iter()
3480                .flat_map(|(_, file, handles)| {
3481                    handles.iter().map(move |h| (file.as_ref(), *h.offset()))
3482                })
3483                .zip(&mut bufs)
3484                .map(|((file, offset), buf)| crate::fs::BlockRead {
3485                    file,
3486                    offset,
3487                    buf: crate::fs::BlockBuf::new(&mut buf[..]),
3488                })
3489                .collect();
3490            // Best-effort: a batched-read failure just leaves the blocks for the
3491            // resolve walk to read normally.
3492            if fs.read_blocks_batched(&mut reqs).is_err() {
3493                return false;
3494            }
3495            // Independently of what the call returned: an implementation that
3496            // reported success without filling a request leaves it short, and
3497            // a block decoded from a buffer nobody wrote would be decoded from
3498            // whatever the allocation held.
3499            if !reqs.iter().all(|r| r.buf.is_full()) {
3500                return false;
3501            }
3502        }
3503
3504        // Views, not owned copies: the decode reads these bytes and builds its
3505        // own block out of them, so staging them into an owning type would copy
3506        // every block a second time.
3507        let all_buffers: Vec<&[u8]> = all_buffers.iter().map(Vec::as_slice).collect();
3508
3509        // Decode each table's blocks (its contiguous slice of all_buffers).
3510        let mut k = 0;
3511        for (table, _, handles) in &planned {
3512            let end = k + handles.len();
3513            table.decode_prewarmed(handles, &all_buffers[k..end]);
3514            k = end;
3515        }
3516        false
3517    }
3518
3519    /// Plans every data block this level's SSTs will read for `remaining`,
3520    /// grouping keys by covering table per run (mirrors `resolve_run_batched`'s
3521    /// walk). Each task carries the ORIGINAL key indices (into `keys`).
3522    ///
3523    /// # Errors
3524    ///
3525    /// Propagates a table-side planning failure ([`Table::plan_block_tasks`]) so
3526    /// the resolver surfaces it instead of letting a stale lower level answer.
3527    #[expect(
3528        clippy::indexing_slicing,
3529        reason = "i < remaining.len() loop-checked; idx/jdx are valid key indices; batch_idx[pos] is in range (pos came from this table's own plan)"
3530    )]
3531    fn plan_level_block_tasks<'a, K: AsRef<[u8]>>(
3532        level: &'a crate::version::Level,
3533        remaining: &[(usize, u64)],
3534        keys: &[K],
3535        seqno: SeqNo,
3536        comparator: &dyn crate::comparator::UserComparator,
3537    ) -> crate::Result<Vec<BlockTask<'a>>> {
3538        let mut tasks: Vec<BlockTask<'a>> = Vec::new();
3539        // Reused across the level's tables, cleared per table: both are read by
3540        // the plan below and are done with before the next table fills them.
3541        let mut batch: Vec<(&[u8], u64)> = Vec::new();
3542        let mut batch_idx: Vec<usize> = Vec::new();
3543        for run in level.iter() {
3544            let mut i = 0;
3545            while i < remaining.len() {
3546                let (idx, _) = remaining[i];
3547                let key = keys[idx].as_ref();
3548                let Some(table) = run.get_for_key_cmp(key, comparator) else {
3549                    i += 1;
3550                    continue;
3551                };
3552                let table_id = table.id();
3553                batch.clear();
3554                batch_idx.clear();
3555                while i < remaining.len() {
3556                    let (jdx, jhash) = remaining[i];
3557                    let jkey = keys[jdx].as_ref();
3558                    match run.get_for_key_cmp(jkey, comparator) {
3559                        Some(t) if t.id() == table_id => {
3560                            batch.push((jkey, jhash));
3561                            batch_idx.push(jdx);
3562                            i += 1;
3563                        }
3564                        _ => break,
3565                    }
3566                }
3567                if let Some((file, table_seqno, special, blocks)) =
3568                    table.plan_block_tasks(&batch, seqno)?
3569                {
3570                    for (handle, positions) in blocks {
3571                        let task_keys: Vec<usize> =
3572                            positions.iter().map(|&pos| batch_idx[pos]).collect();
3573                        tasks.push(BlockTask {
3574                            table,
3575                            file: Arc::clone(&file),
3576                            handle,
3577                            table_seqno,
3578                            special,
3579                            keys: task_keys,
3580                        });
3581                    }
3582                }
3583            }
3584        }
3585        Ok(tasks)
3586    }
3587
3588    /// Resolves an ENTIRE level by reading its blocks in chunks into a scratch and
3589    /// point-reading directly (no cache, no eviction). Called after
3590    /// [`Tree::prewarm_level_cross_sst`] signals the cold working set is too large
3591    /// to warm. Returns `Ok(true)` when it resolved the level (results updated,
3592    /// found keys dropped from `still_remaining`); `Ok(false)` when the level has
3593    /// no blocks to read for this batch (every key bloom-skips) or holds a
3594    /// Page-ECC / columnar table, in which cases the caller falls through to the
3595    /// serial resolve.
3596    #[expect(
3597        clippy::indexing_slicing,
3598        reason = "start/end stay within tasks by construction"
3599    )]
3600    fn resolve_level_chunked<K: AsRef<[u8]>>(
3601        fs: &dyn crate::fs::Fs,
3602        level: &crate::version::Level,
3603        still_remaining: &mut Vec<(usize, u64)>,
3604        keys: &[K],
3605        seqno: SeqNo,
3606        comparator: &dyn crate::comparator::UserComparator,
3607        results: &mut [Option<InternalValue>],
3608    ) -> crate::Result<bool> {
3609        let tasks = Self::plan_level_block_tasks(level, still_remaining, keys, seqno, comparator)?;
3610        let Some(first) = tasks.first() else {
3611            return Ok(false);
3612        };
3613        // A Page-ECC / columnar table covers some of these keys (only possible
3614        // when the columnar/ECC policy differs between the SSTs in this level).
3615        // The scratch decode path is row-format only, so hand the whole level to
3616        // the serial resolve, which loads those blocks through their format-aware
3617        // path. The scratch fast path stays homogeneous and row-only.
3618        if tasks.iter().any(|t| t.special) {
3619            return Ok(false);
3620        }
3621        // Read blocks in chunks of at most half the shared cache, so a chunk's
3622        // scratch never dwarfs the cache it is meant to spare. `.max(1)` keeps the
3623        // chunk loop's `end > start` guard the sole progress condition when the
3624        // cache is disabled (capacity 0).
3625        let budget = (first.table.cache_capacity() / 2).max(1);
3626
3627        let mut start = 0;
3628        while start < tasks.len() {
3629            let mut bytes = 0u64;
3630            let mut end = start;
3631            while end < tasks.len() {
3632                let sz = u64::from(tasks[end].handle.size());
3633                if end > start && bytes + sz > budget {
3634                    break;
3635                }
3636                bytes += sz;
3637                end += 1;
3638            }
3639            Self::resolve_block_task_chunk(fs, &tasks[start..end], keys, results)?;
3640            start = end;
3641        }
3642        still_remaining.retain(|&(idx, _)| results[idx].is_none());
3643        Ok(true)
3644    }
3645
3646    /// Reads one chunk of block-tasks in ONE cross-file `read_blocks_batched`,
3647    /// decodes each from its scratch buffer, and point-reads its keys, keeping the
3648    /// highest-seqno hit per key in `results`. Every task is row-format (the caller
3649    /// routes any level with a Page-ECC / columnar table to the serial resolve).
3650    #[expect(
3651        clippy::indexing_slicing,
3652        reason = "buffers is built from chunk so indices align; key indices are valid (caller's keys/results aligned)"
3653    )]
3654    fn resolve_block_task_chunk<K: AsRef<[u8]>>(
3655        fs: &dyn crate::fs::Fs,
3656        chunk: &[BlockTask<'_>],
3657        keys: &[K],
3658        results: &mut [Option<InternalValue>],
3659    ) -> crate::Result<()> {
3660        let mut buffers: Vec<Vec<u8>> = chunk
3661            .iter()
3662            .map(|t| vec![0u8; t.handle.size() as usize])
3663            .collect();
3664        {
3665            let mut reqs: Vec<crate::fs::BlockRead<'_>> = chunk
3666                .iter()
3667                .zip(buffers.iter_mut())
3668                .map(|(t, buf)| crate::fs::BlockRead {
3669                    file: t.file.as_ref(),
3670                    offset: *t.handle.offset(),
3671                    buf: crate::fs::BlockBuf::new(&mut buf[..]),
3672                })
3673                .collect();
3674            fs.read_blocks_batched(&mut reqs)?;
3675            // An implementation that reported success without filling a request
3676            // leaves it short; refuse to decode a block out of bytes it never
3677            // wrote.
3678            if !reqs.iter().all(|r| r.buf.is_full()) {
3679                return Err(crate::Error::Io(crate::io::Error::new(
3680                    crate::io::ErrorKind::UnexpectedEof,
3681                    "read_blocks_batched reported success on an unfilled block",
3682                )));
3683            }
3684        }
3685
3686        for (task, buf) in chunk.iter().zip(buffers.iter()) {
3687            if let Some(block) = task.table.decode_data_block_from_bytes(buf)? {
3688                for &kidx in &task.keys {
3689                    if let Some(item) = task.table.point_read_translated(
3690                        &block,
3691                        keys[kidx].as_ref(),
3692                        task.table_seqno,
3693                    )? {
3694                        Self::keep_highest(results, kidx, item);
3695                    }
3696                }
3697            }
3698        }
3699        Ok(())
3700    }
3701
3702    /// Keeps the higher-seqno of an existing result and a new candidate (the L0
3703    /// newest-version-wins merge; correct for L1+ too, where each key has one
3704    /// candidate).
3705    #[expect(
3706        clippy::indexing_slicing,
3707        reason = "idx is a valid key index aligned with results"
3708    )]
3709    fn keep_highest(results: &mut [Option<InternalValue>], idx: usize, item: InternalValue) {
3710        match &results[idx] {
3711            Some(current) if current.key.seqno >= item.key.seqno => {}
3712            _ => results[idx] = Some(item),
3713        }
3714    }
3715
3716    fn get_internal_entry_from_tables(
3717        version: &Version,
3718        key: &[u8],
3719        seqno: SeqNo,
3720        comparator: &dyn crate::comparator::UserComparator,
3721    ) -> crate::Result<Option<InternalValue>> {
3722        let key_hash = crate::hash::hash64(key);
3723        Self::find_in_tables::<TableEntry>(version, key, seqno, key_hash, comparator)
3724    }
3725
3726    /// Generic level-walk for point reads, monomorphized over the lookup result type.
3727    ///
3728    /// L0: check ALL runs, keep highest seqno (runs may not be newest-first).
3729    /// L1+: at most one run contains the key — return on first match.
3730    /// Once a level yields a match, lower levels cannot have newer data.
3731    fn find_in_tables<T: TablePointLookup>(
3732        version: &Version,
3733        key: &[u8],
3734        seqno: SeqNo,
3735        key_hash: u64,
3736        comparator: &dyn crate::comparator::UserComparator,
3737    ) -> crate::Result<Option<T>> {
3738        for (level_idx, level) in version.iter_levels().enumerate() {
3739            if level_idx == 0 {
3740                let mut best: Option<T> = None;
3741
3742                for run in level.iter() {
3743                    if let Some(table) = run.get_for_key_cmp(key, comparator)
3744                        && let Some(item) = T::lookup(table, key, seqno, key_hash)?
3745                    {
3746                        match &best {
3747                            Some(current) if current.entry_seqno() >= item.entry_seqno() => {}
3748                            _ => {
3749                                // Short-circuit: point reads use exclusive upper bound,
3750                                // so the highest visible seqno is read_seqno - 1.
3751                                // If matched, no other L0 run can have a higher one.
3752                                if item.entry_seqno().checked_add(1) == Some(seqno) {
3753                                    return Ok(item.filter_tombstone());
3754                                }
3755                                best = Some(item);
3756                            }
3757                        }
3758                    }
3759                }
3760
3761                if let Some(entry) = best {
3762                    return Ok(entry.filter_tombstone());
3763                }
3764            } else {
3765                // L1+ runs have non-overlapping key ranges. Once we find the
3766                // covering run (get_for_key_cmp returns Some), no other run in
3767                // this level can contain the key — break regardless of hit/miss.
3768                for run in level.iter() {
3769                    if let Some(table) = run.get_for_key_cmp(key, comparator) {
3770                        if let Some(item) = T::lookup(table, key, seqno, key_hash)? {
3771                            return Ok(item.filter_tombstone());
3772                        }
3773                        break;
3774                    }
3775                }
3776            }
3777        }
3778
3779        Ok(None)
3780    }
3781
3782    pub(crate) fn get_internal_entry_from_sealed_memtables(
3783        super_version: &SuperVersion,
3784        key: &[u8],
3785        seqno: SeqNo,
3786    ) -> Option<InternalValue> {
3787        for mt in super_version.sealed_memtables.iter().rev() {
3788            if let Some(entry) = mt.get(key, seqno) {
3789                return Some(entry);
3790            }
3791        }
3792
3793        None
3794    }
3795
3796    /// Resolves the super-version serving snapshot `seqno`; see
3797    /// [`SuperVersions::get_version_for_snapshot`](crate::version::SuperVersions::get_version_for_snapshot)
3798    /// for the retention error.
3799    pub(crate) fn get_version_for_snapshot(&self, seqno: SeqNo) -> crate::Result<SuperVersion> {
3800        self.version_history.read().get_version_for_snapshot(seqno)
3801    }
3802
3803    /// The snapshot for one point read, without a clone when it is the latest.
3804    ///
3805    /// Lock-free fast path: when reading at or beyond the latest installed
3806    /// version (always the case for `MAX_SEQNO`, and the common case), the
3807    /// mirrored latest [`SuperVersion`] is exactly what `get_version_for_snapshot`
3808    /// would return (it yields the latest iff `latest.seqno < seqno`), so
3809    /// load it without taking the history `RwLock` or cloning a deque entry.
3810    /// Recent inserts stay visible because they mutate the shared
3811    /// `active_memtable` behind a stable Arc; the back only changes on
3812    /// flush / compaction, which refresh this mirror under the write lock.
3813    ///
3814    /// Historical snapshot reads (seqno <= latest.seqno) consult the locked
3815    /// version history for the correct point-in-time [`SuperVersion`].
3816    ///
3817    /// Point reads only: a guard held across a long scan would delay the
3818    /// mirror's writers, so iterators keep their own clones. no-std has no
3819    /// mirror (`arc-swap` is std-only) and always clones out of the locked
3820    /// history, as before.
3821    ///
3822    /// # Errors
3823    ///
3824    /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
3825    /// when the history no longer retains a version for `seqno`. The fast path
3826    /// cannot hit it: a snapshot above the latest version is always served.
3827    ///
3828    /// Kept to the mirror load plus one compare so it inlines into the point
3829    /// reads; the locked history walk lives in
3830    /// [`historical_snapshot_for_read`](Self::historical_snapshot_for_read),
3831    /// which is deliberately NOT inlined. Folding the two together made this
3832    /// function large enough to stay a call, and a call returning
3833    /// `Result<SnapshotRef>` (an `arc-swap` guard or a `SuperVersion`, plus
3834    /// the error payload) costs the caller a measurable move per read.
3835    #[inline]
3836    pub(crate) fn snapshot_for_read(
3837        &self,
3838        seqno: SeqNo,
3839    ) -> crate::Result<crate::version::SnapshotRef> {
3840        #[cfg(feature = "std")]
3841        {
3842            let latest = self.latest_super_version.load();
3843            if seqno > latest.seqno {
3844                return Ok(crate::version::SnapshotRef::Latest(latest));
3845            }
3846        }
3847        self.historical_snapshot_for_read(seqno)
3848    }
3849
3850    /// The locked-history half of [`snapshot_for_read`](Self::snapshot_for_read):
3851    /// a point-in-time read that the latest-version mirror cannot serve.
3852    ///
3853    /// `#[inline(never)]` keeps it out of every point read's instruction
3854    /// stream. It is NOT `#[cold]`: a historical read is a normal operation
3855    /// (`AS OF` queries, a lagging consumer), just not the common one.
3856    #[inline(never)]
3857    fn historical_snapshot_for_read(
3858        &self,
3859        seqno: SeqNo,
3860    ) -> crate::Result<crate::version::SnapshotRef> {
3861        self.version_history
3862            .read()
3863            .get_version_for_snapshot(seqno)
3864            .map(crate::version::SnapshotRef::Owned)
3865    }
3866
3867    /// Normalizes a user-provided range into owned `Bound<Slice>` values.
3868    ///
3869    /// Returns a tuple containing:
3870    /// - the `OwnedBounds` that mirror the original bounds semantics (including
3871    ///   inclusive/exclusive markers and unbounded endpoints), and
3872    /// - a `bool` flag indicating whether the normalized range is logically
3873    ///   empty (e.g., when the lower bound is greater than the upper bound).
3874    ///
3875    /// Callers can use the flag to detect empty ranges and skip further work
3876    /// while still having access to the normalized bounds for non-empty cases.
3877    fn range_bounds_to_owned_bounds<K: AsRef<[u8]>, R: RangeBounds<K>>(
3878        range: &R,
3879    ) -> (OwnedBounds, bool) {
3880        use Bound::{Excluded, Included, Unbounded};
3881
3882        let start = match range.start_bound() {
3883            Included(key) => Included(Slice::from(key.as_ref())),
3884            Excluded(key) => Excluded(Slice::from(key.as_ref())),
3885            Unbounded => Unbounded,
3886        };
3887
3888        let end = match range.end_bound() {
3889            Included(key) => Included(Slice::from(key.as_ref())),
3890            Excluded(key) => Excluded(Slice::from(key.as_ref())),
3891            Unbounded => Unbounded,
3892        };
3893
3894        let is_empty =
3895            if let (Included(lo) | Excluded(lo), Included(hi) | Excluded(hi)) = (&start, &end) {
3896                lo.as_ref() > hi.as_ref()
3897            } else {
3898                false
3899            };
3900
3901        (OwnedBounds { start, end }, is_empty)
3902    }
3903
3904    /// Opens an LSM-tree in the given directory.
3905    ///
3906    /// Will recover previous state if the folder was previously
3907    /// occupied by an LSM-tree, including the previous configuration.
3908    /// If not, a new tree will be initialized with the given config.
3909    ///
3910    /// After recovering a previous state, use `Tree::set_active_memtable`
3911    /// to fill the memtable with data from a write-ahead log for full durability.
3912    ///
3913    /// # Errors
3914    ///
3915    /// Returns error, if an IO error occurred.
3916    pub(crate) fn open(config: Config) -> crate::Result<Self> {
3917        log::debug!("Opening LSM-tree at {}", config.path.display());
3918
3919        // Resolve the per-tree compaction compression pool once, at open: if the
3920        // caller supplied no shared pool but asked for >1 thread, build the
3921        // default rayon-backed pool now so every compaction reuses it (building
3922        // a pool per compaction would spawn threads on each run). A caller-
3923        // supplied pool is left untouched. Shadowed under `parallel` only, so
3924        // non-parallel builds don't carry an unused `mut`.
3925        #[cfg(feature = "parallel")]
3926        let config = {
3927            let mut config = config;
3928            if config.compaction_pool.is_none() && config.compaction_threads > 1 {
3929                config.compaction_pool = Some(Arc::new(
3930                    crate::table::writer::RayonSpawner::with_threads(config.compaction_threads)?,
3931                ));
3932            }
3933            config
3934        };
3935
3936        // Gate on the `page_ecc` cargo feature: caller asked for ECC
3937        // but the build does not link the Reed-Solomon codec. We have
3938        // no way to verify or recover RS parity without the codec, so
3939        // refuse to open rather than silently downgrade integrity.
3940        // Two surfaces to check:
3941        //   - `Config::page_ecc(true)`  → SST data-block ECC
3942        //   - `Config::with_runtime_config(RuntimeConfig { page_ecc: true, .. })`
3943        //     → manifest-Block ECC (consumed by manifest_blocks::writer)
3944        // Both silently no-op without the feature; refusing here is
3945        // the only place callers see a typed error.
3946        if (config.page_ecc || config.initial_runtime_config.page_ecc)
3947            && !cfg!(feature = "page_ecc")
3948        {
3949            return Err(crate::Error::PageEccUnsupported);
3950        }
3951
3952        // Acquire the cross-process directory lock BEFORE any manifest access
3953        // (the `CURRENT` probe + `has_existing_version_state` check below, and
3954        // the recover / create paths). Acquiring it here makes `open()`
3955        // exclusive end-to-end: a concurrent opener fails fast with
3956        // `Error::Locked` instead of racing through the probe and observing a
3957        // peer's half-created directory (which would surface as the InvalidData
3958        // "half-written checkpoint" path rather than `Locked`). The `LOCK` file
3959        // needs its directory to exist, so create the root directory first
3960        // (idempotent; `create_new` re-creates the `tables/` subtree). The lock
3961        // is threaded into the constructor so it lives for the tree's lifetime.
3962        #[cfg(feature = "std")]
3963        let directory_lock = {
3964            config.fs.create_dir_all(&config.path)?;
3965            crate::config::acquire_directory_lock(&*config.fs, &config.path, config.directory_lock)?
3966        };
3967
3968        // Check for old version
3969        if config.fs.exists(&config.path.join("version"))? {
3970            log::error!(
3971                "refusing to open: this directory has a `version` marker file, which only the \
3972                 retired V1 layout wrote. V5 is the only on-disk format THIS engine decodes, and \
3973                 it ships no conversion tooling. If the directory is a V1 database the data is \
3974                 not lost: open it with the engine that wrote it, or convert it there. If the \
3975                 file is unrelated to this store, move it aside and retry"
3976            );
3977            // The marker's contents are deliberately not read: no legacy
3978            // decode path exists to act on them, so parsing it could only
3979            // change the wording of this error. That makes the presence of
3980            // the file evidence, not proof, hence "has a marker" rather than
3981            // "is a V1 database", and the second escape hatch for a directory
3982            // that merely happens to carry the name.
3983            //
3984            // Scoped to this engine on purpose. The operator reads this line
3985            // while deciding what to do with the directory, and "no conversion
3986            // path" full stop would read as "unrecoverable" — which is wrong,
3987            // and the opposite of what the repair gate documents (an
3988            // unsupported version needs offline conversion or a matching
3989            // binary; see `is_repairable_open_error`).
3990            //
3991            // Literal discriminant: V1 is a retired format and FormatVersion
3992            // carries no legacy variants (V5-only contract).
3993            return Err(crate::Error::InvalidVersion(1));
3994        }
3995
3996        // Decide between recovery and fresh creation atomically by attempting
3997        // to read the CURRENT version file. This avoids a TOCTOU race that
3998        // would occur if we probed with exists() first.
3999        let tree = match crate::version::recovery::get_current_version(
4000            &config.path,
4001            &*config.fs,
4002            config.encryption.clone(),
4003        ) {
4004            Ok(_) => Self::recover(
4005                config,
4006                #[cfg(feature = "std")]
4007                directory_lock,
4008            ),
4009            Err(crate::Error::Io(e)) if e.kind() == crate::io::ErrorKind::NotFound => {
4010                // Missing CURRENT MUST coincide with a directory that
4011                // has no version artifacts; otherwise we are looking at
4012                // a half-written checkpoint (or other interrupted
4013                // sealing). Silently calling `create_new` in that case
4014                // would overwrite the partial state with an empty tree,
4015                // turning a recoverable failure into data loss.
4016                if has_existing_version_state(&config.path, &*config.fs)? {
4017                    // Return Error::Io(InvalidData, ...) rather than
4018                    // Error::Unrecoverable so callers that don't read
4019                    // logs still get a programmatic surface with the
4020                    // path and remediation hint embedded. `log::error!`
4021                    // stays for human ops who DO watch logs and want
4022                    // the full context at the moment of failure (the
4023                    // structured error is what propagates up the call
4024                    // chain; the log line records the diagnosis next
4025                    // to the timestamp).
4026                    let msg = format!(
4027                        "Tree::open: refusing to recover {} — `current` pointer is missing \
4028                         but the directory still holds version artifacts (tables/, blobs/, \
4029                         or vN). This is the on-disk signature of a half-written checkpoint \
4030                         or interrupted sealing. Remove the partial directory and retry the \
4031                         checkpoint, or restore `current` from a backup before reopening.",
4032                        config.path.display(),
4033                    );
4034                    log::error!("{msg}");
4035                    return Err(crate::Error::from(crate::io::Error::new(
4036                        crate::io::ErrorKind::InvalidData,
4037                        msg,
4038                    )));
4039                }
4040                Self::create_new(
4041                    config,
4042                    #[cfg(feature = "std")]
4043                    directory_lock,
4044                )
4045            }
4046            Err(e) => Err(e),
4047        }?;
4048
4049        Ok(tree)
4050    }
4051
4052    /// Returns `true` if there are some tables that are being compacted.
4053    #[doc(hidden)]
4054    #[must_use]
4055    pub fn is_compacting(&self) -> bool {
4056        !self.compaction_state.lock().hidden_set().is_empty()
4057    }
4058
4059    /// Computed storage admission predicate backing
4060    /// [`AbstractTree::write_admission`].
4061    ///
4062    /// Cheap: reads in-memory size accounting only (no syscall). Returns
4063    /// `Ok(())` unless admission control is enabled AND a budget is set AND the
4064    /// live footprint plus reserved headroom exceeds it.
4065    /// Best-effort minimum free space across every filesystem this tree writes
4066    /// to: the primary data path AND each per-level route (`Config::level_routes`
4067    /// can place cold-level SSTs on separate volumes). The admission gate must
4068    /// reflect the tightest volume, since a full routed disk fails compaction /
4069    /// flush targeting it even while the primary still has room.
4070    ///
4071    /// A backend that cannot report free space (or an I/O hiccup) yields
4072    /// `u64::MAX` = "no disk pressure", so a probe failure never falsely drives
4073    /// the tree read-only.
4074    fn probe_disk_free(&self) -> u64 {
4075        self.0.config.min_available_space()
4076    }
4077
4078    /// Disk-aware capacity figures for [`AbstractTree::storage_stats`], given the
4079    /// live footprint `used`: `(capacity, available, compaction_possible)`.
4080    ///
4081    /// `capacity` is the tighter of the configured quota and the physical disk
4082    /// headroom (`free + used`) — the same effective limit
4083    /// [`Self::compute_write_admission`] gates against — reported regardless of
4084    /// whether the admission gate is enabled (introspection is always available).
4085    /// `None` capacity/available means unbounded (no quota AND the backend
4086    /// cannot report free space). `compaction_possible` is `true` when unbounded
4087    /// or when at least [`MIN_RESERVED_HEADROOM`] of working room remains.
4088    pub(crate) fn admission_capacity(&self, used: u64) -> (Option<u64>, Option<u64>, bool) {
4089        let quota = self
4090            .0
4091            .runtime_config
4092            .load()
4093            .storage_limit_bytes
4094            .unwrap_or(u64::MAX);
4095        let free = self.probe_disk_free();
4096        // `free == u64::MAX` is the "backend can't report free space" sentinel:
4097        // adding `used` would overflow, so treat capacity as quota-only (the
4098        // explicit branch avoids the overflow without masking it with saturation).
4099        // Otherwise `free + used` ≤ ~2× disk capacity and cannot overflow u64.
4100        let capacity = if free == u64::MAX {
4101            quota
4102        } else {
4103            quota.min(free + used)
4104        };
4105        if capacity == u64::MAX {
4106            return (None, None, true);
4107        }
4108        // `available = max(0, capacity - used)`: an operator quota set below the
4109        // live footprint makes `capacity < used`, and available space cannot be
4110        // negative. The clamp-to-zero IS the intended semantics here.
4111        let available = capacity.saturating_sub(used);
4112        (
4113            Some(capacity),
4114            Some(available),
4115            available >= MIN_RESERVED_HEADROOM,
4116        )
4117    }
4118
4119    /// The logical partition-quota headroom for the two-layer space model:
4120    /// `max(0, storage_limit_bytes - used)`, or `u64::MAX` when no quota is set.
4121    ///
4122    /// This is Layer 1 (volume-agnostic) of [`crate::compaction::worker::space_fits_two_layer`];
4123    /// the physical free-space probe is Layer 2. An operator quota set below the
4124    /// live footprint leaves zero headroom — the clamp-to-zero is the intended
4125    /// min-semantics, not masking.
4126    pub(crate) fn quota_headroom(&self, used: u64) -> u64 {
4127        self.0
4128            .runtime_config
4129            .load()
4130            .storage_limit_bytes
4131            .map_or(u64::MAX, |limit| limit.saturating_sub(used))
4132    }
4133
4134    /// Whether the opt-in storage admission gate is active (a near-full disk or
4135    /// configured quota can drive the tree read-only and gate compaction space).
4136    /// Capacity introspection figures are reported regardless; this only governs
4137    /// whether the gate actually enforces.
4138    pub(crate) fn storage_admission_enabled(&self) -> bool {
4139        self.0.runtime_config.load().storage_admission_check
4140    }
4141
4142    #[expect(
4143        clippy::significant_drop_tightening,
4144        reason = "the admission cache lock intentionally spans the recompute \
4145                  (stat + disk-free probe) so concurrent admission checks \
4146                  coalesce on a single probe rather than each issuing a syscall"
4147    )]
4148    fn compute_write_admission(&self) -> crate::Result<()> {
4149        let rc = self.0.runtime_config.load();
4150        if !rc.storage_admission_check {
4151            return Ok(());
4152        }
4153
4154        // Take ONE coherent snapshot of the latest super-version and derive
4155        // BOTH the on-disk footprint and the pending-memtable bytes from it.
4156        // Reading them from two separate `latest_version()` loads would be a
4157        // TOCTOU bug: a flush installing a new version between the two reads
4158        // could pair an old (larger) disk usage with new (smaller) pending
4159        // bytes — or vice versa — and open the gate incorrectly.
4160        let super_version = self.version_history.read().latest_version();
4161        let vid = super_version.version.id();
4162
4163        // True physical footprint, including blob files — the SAME basis
4164        // `storage_stats()` reports, so the gate and the reported usage agree.
4165        // NOT `disk_space()` (metadata Level::size, which omits blob files and
4166        // undercounts the physical file by the meta block / footer).
4167        //
4168        // Cached so gated writes don't re-stat every live file or re-probe disk
4169        // on every call. `used_bytes` only changes when a new version is
4170        // installed (flush / compaction), so it is recomputed on a version
4171        // change. `disk_free` can change under us (another process writing the
4172        // same filesystem), so it is ALSO re-probed once its sample is older
4173        // than `ADMISSION_DISK_FREE_TTL` — bounding staleness without a syscall
4174        // per write. `update_runtime_config` resets the entry for an immediate
4175        // re-probe. The values live behind one mutex as a coherent unit (see
4176        // `TreeInner::admission_used_cache`).
4177        //
4178        // The TTL fast-path is std-only: under `no_std` there is no monotonic
4179        // clock (`crate::time::Instant::elapsed` is a zero stub), so an
4180        // elapsed-time window cannot bound staleness — a same-version sample
4181        // would otherwise look fresh forever and a filling disk would never be
4182        // re-probed. Under `no_std` the fast-path is skipped, so `disk_free` is
4183        // re-probed on every gated write (the `used` footprint stays cached by
4184        // version either way), keeping admission safe without a monotonic clock.
4185        let now = crate::time::Instant::now();
4186        let (used, disk_free) = {
4187            let mut cache = self.0.admission_used_cache.lock();
4188            match *cache {
4189                // Fresh: same version AND disk sample within the TTL. std-only —
4190                // `cfg!(feature = "std")` is `false` under `no_std`, so the guard
4191                // short-circuits there and the next arm re-probes every call.
4192                Some((cvid, used, free, at))
4193                    if cvid == vid
4194                        && cfg!(feature = "std")
4195                        && at.elapsed() < ADMISSION_DISK_FREE_TTL =>
4196                {
4197                    (used, free)
4198                }
4199                // Same version, stale disk sample: keep `used`, re-probe disk.
4200                Some((cvid, used, _, _)) if cvid == vid => {
4201                    let free = self.probe_disk_free();
4202                    *cache = Some((vid, used, free, now));
4203                    (used, free)
4204                }
4205                // New version (or unset): recompute footprint and re-probe disk.
4206                _ => {
4207                    let used = crate::storage_stats::compute_used_bytes(&super_version.version)?;
4208                    let free = self.probe_disk_free();
4209                    *cache = Some((vid, used, free, now));
4210                    (used, free)
4211                }
4212            }
4213        };
4214
4215        // Effective limit is the tighter of the configured quota and the
4216        // physical disk headroom (free + what we already occupy): the disk can
4217        // fill from other processes even below a generous quota, and a tree with
4218        // no quota at all must still stop before ENOSPC. `None` quota = unbounded
4219        // by configuration; disk-free then alone bounds it.
4220        //
4221        // `disk_free` is the MINIMUM free across every volume the tree writes to
4222        // (`probe_disk_free` mins the primary path and all `level_routes`). The
4223        // `+ used` here is NOT an accounting of one volume's usage against
4224        // another's free space — it cancels out of the disk branch of the gate:
4225        // passing requires `used + reserved <= disk_free + used`, i.e.
4226        // `reserved <= disk_free`. So a passing gate guarantees the TIGHTEST
4227        // volume alone has at least `reserved` free — a conservative per-volume
4228        // headroom, never the sum of an empty routed volume's slack plus an
4229        // unrelated full volume's occupancy. A route that drops below `reserved`
4230        // free drives the whole tree read-only, exactly so a later flush /
4231        // compaction targeting that route cannot hit ENOSPC.
4232        let quota = rc.storage_limit_bytes.unwrap_or(u64::MAX);
4233        // `disk_free == u64::MAX` is the "backend can't report" sentinel; adding
4234        // `used` would overflow, so treat the limit as quota-only (explicit
4235        // branch, no saturation masking). Otherwise `disk_free + used` ≤ ~2× disk
4236        // capacity and cannot overflow u64.
4237        let limit = if disk_free == u64::MAX {
4238            quota
4239        } else {
4240            quota.min(disk_free + used)
4241        };
4242        // Both sources unbounded → nothing to gate.
4243        if limit == u64::MAX {
4244            return Ok(());
4245        }
4246
4247        // Reserved headroom keeps the soft budget from becoming a hard wall:
4248        // enough to flush every pending memtable (plus a margin for the
4249        // index/filter/footer overhead a flush adds) so a queued flush always
4250        // fits at the limit, with a floor for compaction working space.
4251        // Internal flush / compaction are never gated, so this band is the
4252        // engine's room to reclaim.
4253        //
4254        // Count ALL pending memtable bytes in this snapshot — the active one AND
4255        // any sealed (rotated) memtables awaiting flush — not just the active
4256        // one: after a rotation the active memtable is empty but the sealed
4257        // memtable's queued flush will still consume disk, so it must be
4258        // reserved for. Memtable sizes are bounded by RAM, so the sum (and the
4259        // +1/8 overhead margin below) cannot overflow u64 → plain arithmetic.
4260        let pending_memtable_bytes: u64 = super_version.active_memtable.size()
4261            + super_version
4262                .sealed_memtables
4263                .iter()
4264                .map(|m| m.size())
4265                .sum::<u64>();
4266
4267        let reserved =
4268            (pending_memtable_bytes + pending_memtable_bytes / 8).max(MIN_RESERVED_HEADROOM);
4269        // `used` (disk) + `reserved` (RAM-bounded) cannot realistically overflow,
4270        // but keep the comparison fail-closed with checked arithmetic: any
4271        // overflow means "definitely over budget", so deny.
4272        match used.checked_add(reserved) {
4273            Some(total) if total <= limit => Ok(()),
4274            _ => Err(crate::Error::StorageFull { used, limit }),
4275        }
4276    }
4277
4278    fn inner_compact(
4279        &self,
4280        strategy: Arc<dyn CompactionStrategy>,
4281        mvcc_gc_watermark: SeqNo,
4282    ) -> crate::Result<crate::compaction::CompactionResult> {
4283        use crate::compaction::worker::{Options, do_compaction};
4284
4285        let mut opts = Options::from_tree(self, strategy);
4286        opts.mvcc_gc_watermark = mvcc_gc_watermark;
4287
4288        let result = do_compaction(&opts)?;
4289
4290        log::debug!("Compaction run over");
4291
4292        Ok(result)
4293    }
4294
4295    /// Iterator over the whole tree at snapshot `seqno`.
4296    ///
4297    /// # Errors
4298    ///
4299    /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
4300    /// when the history no longer retains a version for `seqno`; the error is
4301    /// raised here, before any I/O, rather than as an iterator item.
4302    #[doc(hidden)]
4303    pub fn create_iter(
4304        &self,
4305        seqno: SeqNo,
4306        ephemeral: Option<(Arc<Memtable>, SeqNo)>,
4307    ) -> crate::Result<impl DoubleEndedIterator<Item = crate::Result<KvPair>> + 'static> {
4308        self.create_range::<UserKey, _>(&.., seqno, ephemeral)
4309    }
4310
4311    /// Iterator over `range` at snapshot `seqno`.
4312    ///
4313    /// # Errors
4314    ///
4315    /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
4316    /// when the history no longer retains a version for `seqno`; the error is
4317    /// raised here, before any I/O, rather than as an iterator item.
4318    #[doc(hidden)]
4319    pub fn create_range<'a, K: AsRef<[u8]> + 'a, R: RangeBounds<K> + 'a>(
4320        &self,
4321        range: &'a R,
4322        seqno: SeqNo,
4323        ephemeral: Option<(Arc<Memtable>, SeqNo)>,
4324    ) -> crate::Result<impl DoubleEndedIterator<Item = crate::Result<KvPair>> + 'static> {
4325        let super_version = self
4326            .version_history
4327            .read()
4328            .get_version_for_snapshot(seqno)?;
4329
4330        Ok(Self::create_internal_range(
4331            super_version,
4332            range,
4333            seqno,
4334            ephemeral,
4335            self.config.merge_operator.clone(),
4336            self.config.comparator.clone(),
4337        )
4338        .map(|item| match item {
4339            Ok(kv) => Ok((kv.key.user_key, kv.value)),
4340            Err(e) => Err(e),
4341        }))
4342    }
4343
4344    /// Build a [`SeekableTreeIter`](crate::range::SeekableTreeIter) over
4345    /// `[lo, hi)`. Source collection (Phase 1) runs once; repositions reuse it.
4346    ///
4347    /// # Errors
4348    ///
4349    /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
4350    /// when the history no longer retains a version for `seqno`.
4351    #[doc(hidden)]
4352    pub fn create_seekable_range_bounds(
4353        &self,
4354        lo: Bound<UserKey>,
4355        hi: Bound<UserKey>,
4356        seqno: SeqNo,
4357        ephemeral: Option<(Arc<Memtable>, SeqNo)>,
4358    ) -> crate::Result<crate::range::SeekableTreeIter> {
4359        use crate::range::{IterState, SeekableTreeIter};
4360
4361        let super_version = self
4362            .version_history
4363            .read()
4364            .get_version_for_snapshot(seqno)?;
4365
4366        let iter_state = IterState {
4367            version: super_version,
4368            ephemeral,
4369            merge_operator: self.config.merge_operator.clone(),
4370            comparator: self.config.comparator.clone(),
4371            prefix_hash: None,
4372            key_hash: None,
4373            bloom_key: None,
4374            #[cfg(feature = "metrics")]
4375            metrics: Some(self.0.metrics.clone()),
4376        };
4377
4378        Ok(SeekableTreeIter::create(iter_state, lo, hi, seqno))
4379    }
4380
4381    /// Iterator over the keys starting with `prefix` at snapshot `seqno`.
4382    ///
4383    /// # Errors
4384    ///
4385    /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention)
4386    /// when the history no longer retains a version for `seqno`; the error is
4387    /// raised here, before any I/O, rather than as an iterator item.
4388    #[doc(hidden)]
4389    pub fn create_prefix<'a, K: AsRef<[u8]> + 'a>(
4390        &self,
4391        prefix: K,
4392        seqno: SeqNo,
4393        ephemeral: Option<(Arc<Memtable>, SeqNo)>,
4394    ) -> crate::Result<impl DoubleEndedIterator<Item = crate::Result<KvPair>> + 'static> {
4395        use crate::prefix::compute_prefix_hash;
4396        use crate::range::{IterState, TreeIter, prefix_to_range};
4397
4398        let prefix_bytes = prefix.as_ref();
4399
4400        let prefix_hash = compute_prefix_hash(self.config.prefix_extractor.as_ref(), prefix_bytes);
4401
4402        let range = prefix_to_range(prefix_bytes);
4403
4404        let super_version = self
4405            .version_history
4406            .read()
4407            .get_version_for_snapshot(seqno)?;
4408
4409        let iter_state = IterState {
4410            version: super_version,
4411            ephemeral,
4412            merge_operator: self.config.merge_operator.clone(),
4413            comparator: self.config.comparator.clone(),
4414            prefix_hash,
4415            key_hash: None,
4416            bloom_key: None,
4417            #[cfg(feature = "metrics")]
4418            metrics: Some(self.0.metrics.clone()),
4419        };
4420
4421        Ok(
4422            TreeIter::create_range(iter_state, range, seqno).map(|item| match item {
4423                Ok(kv) => Ok((kv.key.user_key, kv.value)),
4424                Err(e) => Err(e),
4425            }),
4426        )
4427    }
4428
4429    /// Adds an item to the active memtable.
4430    ///
4431    /// Returns the added item's size and new size of the memtable.
4432    #[doc(hidden)]
4433    #[must_use]
4434    pub fn append_entry(&self, value: InternalValue) -> (u64, u64) {
4435        // Per-KV residence digest (KvChecksumComputePoint::AtInsert): compute
4436        // the entry's 4-byte logical-content digest now, so a RAM bit-flip
4437        // while it sits in the memtable is caught at flush. The digest covers
4438        // the OWNED `value` and is independent of which active memtable
4439        // receives it, so computing it before taking the version-history guard
4440        // is correct (a concurrent rotation just routes the same value+digest
4441        // into the new active memtable) AND keeps the hash out of the read-lock
4442        // critical section. The gate is one relaxed byte mirrored from the
4443        // runtime config (`TreeInner::kv_digest_at_insert`); under the default
4444        // `AtBlockCompile` (or `Off`) it is `0` and no digest is computed.
4445        let gate = self
4446            .0
4447            .kv_digest_at_insert
4448            .load(core::sync::atomic::Ordering::Relaxed);
4449        let kv_digest = inner::kv_digest_algo_from_gate(gate).and_then(|algo| {
4450            crate::table::block::kv_checksum::kv_digest(&value, algo).map(|d| {
4451                #[expect(
4452                    clippy::cast_possible_truncation,
4453                    reason = "AtInsert is config-validated to a 4-byte algorithm; the digest fits u32"
4454                )]
4455                let lo = d as u32;
4456                (lo, algo)
4457            })
4458        });
4459
4460        // The `.read()` guard is a temporary that lives until the end of this
4461        // statement, so the insert runs under the version-history read lock:
4462        // `value` + its digest land in the current active memtable atomically,
4463        // and a concurrent `rotate_memtable()` cannot seal it mid-insert.
4464        self.version_history
4465            .read()
4466            .latest_version_ref()
4467            .active_memtable
4468            .insert_with_kv_digest(value, kv_digest)
4469    }
4470
4471    /// Adds multiple items to the active memtable in bulk.
4472    ///
4473    /// Acquires the version-history lock once and delegates to
4474    /// [`Memtable::insert_batch`] for batch size accounting.
4475    ///
4476    /// Returns the total bytes added and new size of the memtable.
4477    #[doc(hidden)]
4478    #[must_use]
4479    pub(crate) fn append_batch(&self, items: Vec<InternalValue>) -> (u64, u64) {
4480        // Per-KV residence digest under AtInsert (see `append_entry`): pass the
4481        // algorithm so the bulk path fixes each entry's digest at insert. The
4482        // default path passes `None` and is unchanged.
4483        let kv_algo = inner::kv_digest_algo_from_gate(
4484            self.0
4485                .kv_digest_at_insert
4486                .load(core::sync::atomic::Ordering::Relaxed),
4487        );
4488
4489        // Hold the read guard for the entire insert to prevent rotate_memtable()
4490        // from sealing this memtable mid-batch (which could cause data loss if
4491        // a concurrent flush persists only a prefix of the batch).
4492        self.version_history
4493            .read()
4494            .latest_version_ref()
4495            .active_memtable
4496            .insert_batch_with_kv_algo(items, kv_algo)
4497    }
4498
4499    /// Recovers previous state, by loading the level manifest, tables and blob files.
4500    ///
4501    /// # Errors
4502    ///
4503    /// Returns error, if an IO error occurred.
4504    #[expect(
4505        clippy::too_many_lines,
4506        reason = "Tree::recover threads the whole open sequence (CURRENT validation, \
4507                  Manifest decode, encryption + runtime plumbing, version recovery, \
4508                  TreeInner assembly) — splitting it would create helper functions whose \
4509                  only caller is this one site"
4510    )]
4511    fn recover(
4512        mut config: Config,
4513        // The cross-process directory lock acquired by `Tree::open` before the
4514        // manifest probe; held for the tree's lifetime via
4515        // `TreeInner::_directory_lock`.
4516        #[cfg(feature = "std")] directory_lock: Option<Box<dyn crate::fs::FsFile>>,
4517    ) -> crate::Result<Self> {
4518        use crate::stop_signal::StopSignal;
4519        use inner::get_next_tree_id;
4520
4521        log::info!("Recovering LSM-tree at {}", config.path.display());
4522
4523        // Validate manifest metadata (format version, comparator name)
4524        // BEFORE recover_levels, so a rejected open is side-effect free
4525        // — recover_levels loads tables and cleans up orphans.
4526        // Tree type is checked after recovery (needs the Version object).
4527        // NOTE: the version file is read twice (here for metadata, then inside
4528        // recover_levels for table/blob data). This is intentional — metadata
4529        // validation must complete before any disk-mutating recovery work.
4530        // Version id of the on-disk snapshot CURRENT references. This is the
4531        // base the edit log replays on top of; the live version id can be higher
4532        // (it has no `v{id}` file of its own). Threaded into the version history
4533        // so the next persist appends to / rotates the right snapshot's log.
4534        let snapshot_id = crate::version::recovery::get_current_version(
4535            &config.path,
4536            &*config.fs,
4537            config.encryption.clone(),
4538        )?;
4539        {
4540            let version_id = snapshot_id;
4541            let manifest_path = config.path.join(format!("v{version_id}"));
4542            // Open the manifest with a default runtime snapshot:
4543            // ECC awareness is captured per-Block via the header
4544            // (`ECC_PARITY` flag) so the reader doesn't actually
4545            // need to know which ECC mode the writer used. The
4546            // captured runtime here is a placeholder; once we want
4547            // runtime-driven decisions on the read path (e.g.
4548            // checksum_algo dispatch per #298) we'll seed it from
4549            // Config + persisted format-version fields.
4550            let mut archive_reader = crate::manifest_blocks::reader::ManifestArchiveReader::open(
4551                &manifest_path,
4552                &*config.fs,
4553                alloc::sync::Arc::new(crate::runtime_config::RuntimeConfig::default()),
4554                config.encryption.clone(),
4555            )?;
4556            let manifest = Manifest::decode_from(&mut archive_reader)?;
4557
4558            // V5 is the only variant `FormatVersion` can decode to (the
4559            // engine reads exactly one on-disk format, no legacy paths), so
4560            // anything else already failed above: on its framing if the
4561            // manifest is not shaped like the current one, on the version
4562            // field if it is. This match stays as the explicit gate the
4563            // format contract documents — and as the compile-time hook that
4564            // forces a review of the open path when a new variant is added.
4565            match manifest.version {
4566                FormatVersion::V5 => {}
4567            }
4568
4569            let supplied_name = config.comparator.name();
4570            if manifest.comparator_name != supplied_name {
4571                log::warn!(
4572                    "Comparator mismatch: tree was created with {:?} but opened with {:?}",
4573                    manifest.comparator_name,
4574                    supplied_name,
4575                );
4576                return Err(crate::Error::ComparatorMismatch {
4577                    stored: manifest.comparator_name,
4578                    supplied: supplied_name,
4579                });
4580            }
4581
4582            // IMPORTANT: Restore persisted config
4583            config.level_count = manifest.level_count;
4584        }
4585
4586        let tree_id = get_next_tree_id();
4587
4588        #[cfg(feature = "metrics")]
4589        let metrics = Arc::new(Metrics::default());
4590
4591        let version = Self::recover_levels(
4592            &config.path,
4593            tree_id,
4594            &config,
4595            #[cfg(feature = "metrics")]
4596            &metrics,
4597        )?;
4598
4599        {
4600            let requested_tree_type = match config.kv_separation_opts {
4601                Some(_) => crate::TreeType::Blob,
4602                None => crate::TreeType::Standard,
4603            };
4604
4605            if version.tree_type() != requested_tree_type {
4606                log::error!(
4607                    "Tried to open a {requested_tree_type:?}Tree, but the existing tree is of type {:?}Tree. This indicates a misconfiguration or corruption.",
4608                    version.tree_type(),
4609                );
4610                // A dedicated error, NOT `Unrecoverable`: the auto-repair path
4611                // answers `Unrecoverable` with a manifest rebuild, and a
4612                // rebuild under the mismatched type commits the wrong tree
4613                // shape (a Standard rebuild of a blob tree strands its blob
4614                // files for the orphan sweep). A configuration error must
4615                // propagate to the caller instead.
4616                return Err(crate::Error::TreeTypeMismatch {
4617                    requested: requested_tree_type,
4618                    actual: version.tree_type(),
4619                });
4620            }
4621        }
4622
4623        let highest_table_id = version
4624            .iter_tables()
4625            .map(Table::id)
4626            .max()
4627            .unwrap_or_default();
4628
4629        let comparator = config.comparator.clone();
4630
4631        let deletion_pause = crate::deletion_pause::DeletionPause::new_shared();
4632        #[cfg(feature = "std")]
4633        let background_deleter = Arc::new(crate::BackgroundDeleter::new(None));
4634        let heal_hints =
4635            crate::heal_hints::HealHints::new_shared(config.initial_runtime_config.auto_heal);
4636
4637        // Clone the seed snapshot BEFORE moving config into the Arc
4638        // below — the runtime handle initializer needs it after the
4639        // move.
4640        let initial_runtime = config.initial_runtime_config.clone();
4641        let sync_mode = config.sync_mode;
4642        let super_versions = SuperVersions::new(
4643            version,
4644            &comparator,
4645            sync_mode,
4646            snapshot_id,
4647            config.manifest_log_rotate_bytes,
4648        );
4649        #[cfg(feature = "std")]
4650        let latest_super_version = super_versions.latest_handle();
4651        let inner = TreeInner {
4652            id: tree_id,
4653            memtable_id_counter: SequenceNumberCounter::new(1),
4654            table_id_counter: SequenceNumberCounter::new(highest_table_id + 1),
4655            blob_file_id_counter: SequenceNumberCounter::default(),
4656            version_history: Arc::new(RwLock::new(super_versions)),
4657            #[cfg(feature = "std")]
4658            latest_super_version,
4659            stop_signal: StopSignal::default(),
4660            config: Arc::new(config),
4661            major_compaction_lock: RwLock::default(),
4662            flush_lock: Mutex::default(),
4663            #[cfg(feature = "std")]
4664            _directory_lock: directory_lock,
4665            compaction_state: Arc::new(Mutex::new(CompactionState::default())),
4666            deletion_pause: Arc::clone(&deletion_pause),
4667            #[cfg(feature = "std")]
4668            background_deleter: Arc::clone(&background_deleter),
4669            heal_hints: Arc::clone(&heal_hints),
4670            kv_digest_at_insert: portable_atomic::AtomicU8::new(inner::kv_digest_at_insert_gate(
4671                &initial_runtime,
4672            )),
4673            runtime_config: Arc::new(crate::runtime_config::handle::RuntimeConfigHandle::new(
4674                initial_runtime,
4675            )),
4676            admission_used_cache: Mutex::new(None),
4677
4678            #[cfg(feature = "metrics")]
4679            metrics,
4680
4681            #[cfg(test)]
4682            test_hooks: inner::TestHooks::default(),
4683        };
4684
4685        // Install the pause on every recovered table / blob file so their
4686        // Drop impls consult it when a checkpoint is in flight. Snapshot
4687        // the Arc handles into owned collections so the read lock is
4688        // released before iterating (avoids `significant_drop_tightening`).
4689        // Snapshot the version under the read lock, then drop the lock before
4690        // collecting so the version_history lock isn't held across the clones.
4691        let version = inner.version_history.read().latest_version().version;
4692        let recovered_tables: Vec<Table> = version.iter_tables().cloned().collect();
4693        let recovered_blobs: Vec<BlobFile> = version.blob_files.iter().cloned().collect();
4694
4695        let sinks = crate::table::TableSinks {
4696            deletion_pause: &deletion_pause,
4697            heal_hints: &heal_hints,
4698            #[cfg(feature = "std")]
4699            background_deleter: Some(&background_deleter),
4700        };
4701        for table in &recovered_tables {
4702            table.bind_to_tree(&sinks);
4703        }
4704        for blob_file in &recovered_blobs {
4705            blob_file.bind_to_tree(&sinks);
4706        }
4707
4708        // Re-arm the tight-space reclaims a previous session could not finish.
4709        // A reclaim deferred because a checkpoint still hard-linked the file
4710        // lived only in that session's queue, and the unrestricted view that
4711        // could re-arm it is gone once the process restarts, so the consumed
4712        // prefix would stay allocated for the table's lifetime. Nothing is
4713        // persisted for this: the intent is DERIVABLE, and the extent is
4714        // exactly the prefix the committed bound cuts away. A prefix already
4715        // punched re-punches as a no-op, so a completed reclaim costs one
4716        // call, and only on a restricted table.
4717        #[cfg(feature = "std")]
4718        {
4719            for table in &recovered_tables {
4720                let Some(bound) = table.restrict_lower_bound() else {
4721                    continue;
4722                };
4723                // Reclaiming frees space; it never decides what the tree
4724                // serves. A table that opened cleanly must not be denied to
4725                // readers because its punch offset cannot be re-derived, so a
4726                // failure here is reported and skipped. An ENVIRONMENTAL fault
4727                // still propagates: it says nothing about this table, and a
4728                // retry can re-read it.
4729                let offset = match table.punch_offset_for(bound) {
4730                    Ok(offset) => offset,
4731                    Err(e) if e.is_environmental() => return Err(e),
4732                    Err(e) => {
4733                        log::warn!(
4734                            "table {} carries a committed restriction whose punch offset \
4735                             could not be re-derived ({e}); its consumed prefix stays \
4736                             allocated until the next tight-space compaction",
4737                            table.id(),
4738                        );
4739                        continue;
4740                    }
4741                };
4742                if offset > 0 {
4743                    deletion_pause.retain_reclaim(
4744                        Arc::clone(&table.fs),
4745                        (*table.path).clone(),
4746                        alloc::vec![(0, offset)],
4747                    );
4748                }
4749            }
4750            // Blob files carry the same deferred intent, in their committed
4751            // frontier rather than a restriction bound.
4752            for blob in &recovered_blobs {
4753                let extent = match blob.committed_reclaimable_prefix() {
4754                    Ok(Some(extent)) => extent,
4755                    Ok(None) => continue,
4756                    Err(e) if e.is_environmental() => return Err(e),
4757                    Err(e) => {
4758                        log::warn!(
4759                            "blob file {:?} carries a committed frontier whose reclaimable \
4760                             extent could not be re-derived ({e}); its consumed prefix stays \
4761                             allocated until the next relocation",
4762                            blob.id(),
4763                        );
4764                        continue;
4765                    }
4766                };
4767                deletion_pause.retain_reclaim(
4768                    Arc::clone(&blob.0.fs),
4769                    blob.0.path.clone(),
4770                    alloc::vec![extent],
4771                );
4772            }
4773            deletion_pause.retry_pending_reclaims();
4774        }
4775
4776        Ok(Self(Arc::new(inner)))
4777    }
4778
4779    /// Creates a new LSM-tree in a directory.
4780    fn create_new(
4781        config: Config,
4782        // The cross-process directory lock acquired by `Tree::open`, held for
4783        // the tree's lifetime.
4784        #[cfg(feature = "std")] directory_lock: Option<Box<dyn crate::fs::FsFile>>,
4785    ) -> crate::Result<Self> {
4786        use crate::file::fsync_directory;
4787
4788        let path = config.path.clone();
4789        log::trace!("Creating LSM-tree at {}", path.display());
4790
4791        let sync_mode = config.sync_mode;
4792
4793        (*config.fs).create_dir_all(&path)?;
4794
4795        // Create tables directories for all configured paths (primary + routes).
4796        // create_dir_all may create both <route> and <route>/tables.
4797        // Fsync the tables dir, its parent (route dir), AND the route's parent
4798        // to make all newly-created directory entries durable on POSIX.
4799        for (table_folder_path, folder_fs) in config.all_tables_folders() {
4800            folder_fs.create_dir_all(&table_folder_path)?;
4801            fsync_directory(&table_folder_path, &*folder_fs, sync_mode)?;
4802            if let Some(parent) = table_folder_path.parent() {
4803                fsync_directory(parent, &*folder_fs, sync_mode)?;
4804                if let Some(grandparent) = parent.parent() {
4805                    fsync_directory(grandparent, &*folder_fs, sync_mode)?;
4806                }
4807            }
4808        }
4809
4810        // IMPORTANT: fsync primary folder on Unix
4811        fsync_directory(&path, &*config.fs, sync_mode)?;
4812
4813        let inner = TreeInner::create_new(
4814            config,
4815            #[cfg(feature = "std")]
4816            directory_lock,
4817        )?;
4818        Ok(Self(Arc::new(inner)))
4819    }
4820
4821    /// Recovers the level manifest, loading all tables from disk.
4822    ///
4823    /// When [`level_routes`](Config::level_routes) is configured, all
4824    /// configured table folders are scanned so tables on different storage
4825    /// tiers are discovered correctly.
4826    #[expect(
4827        clippy::too_many_lines,
4828        reason = "recovery logic is inherently complex"
4829    )]
4830    fn recover_levels<P: AsRef<Path>>(
4831        tree_path: P,
4832        tree_id: TreeId,
4833        config: &Config,
4834        #[cfg(feature = "metrics")] metrics: &Arc<Metrics>,
4835    ) -> crate::Result<Version> {
4836        use crate::{TableId, file::fsync_directory};
4837
4838        let tree_path = tree_path.as_ref();
4839
4840        let recovery = recover(
4841            tree_path,
4842            &*config.fs,
4843            config.manifest_recovery_mode,
4844            config.encryption.clone(),
4845        )?;
4846
4847        // The on-disk snapshot CURRENT points at — the generation orphan cleanup
4848        // must preserve. Intermediate versions live only in the edit log, so the
4849        // latest version id (`version.id()`) has no `v{id}` file of its own.
4850        let snapshot_id = recovery.snapshot_id;
4851
4852        let mut table_map = {
4853            let mut result: crate::HashMap<TableId, (u8 /* Level index */, Checksum, SeqNo)> =
4854                crate::HashMap::default();
4855
4856            for (level_idx, table_ids) in recovery.table_ids.iter().enumerate() {
4857                for run in table_ids {
4858                    for table in run {
4859                        #[expect(
4860                            clippy::expect_used,
4861                            reason = "there are always less than 256 levels"
4862                        )]
4863                        result.insert(
4864                            table.id,
4865                            (
4866                                level_idx
4867                                    .try_into()
4868                                    .expect("there are less than 256 levels"),
4869                                table.checksum,
4870                                table.global_seqno,
4871                            ),
4872                        );
4873                    }
4874                }
4875            }
4876
4877            result
4878        };
4879
4880        let cnt = table_map.len();
4881
4882        // Immutable snapshot of every table id the manifest knows. `table_map`
4883        // is drained as tables are recovered below, so it cannot answer "is this
4884        // id live?" order-independently; this set can. Used to sweep an orphaned
4885        // `.heal-attest` sidecar whose SST was retired.
4886        let manifest_ids: crate::HashSet<TableId> = table_map.keys().copied().collect();
4887
4888        log::debug!("Recovering {cnt} tables from {}", tree_path.display());
4889
4890        let progress_mod = match cnt {
4891            _ if cnt <= 20 => 1,
4892            _ if cnt <= 100 => 10,
4893            _ => 100,
4894        };
4895
4896        let mut tables = vec![];
4897        // Track recovered table IDs so duplicate sightings (via symlinks,
4898        // junctions, or case-insensitive aliases of the same directory) are
4899        // skipped rather than orphan-deleted.
4900        let mut recovered_table_ids: crate::HashSet<TableId> = crate::HashSet::default();
4901        let mut orphaned_tables: Vec<(crate::path::PathBuf, Arc<dyn crate::fs::Fs>)> = vec![];
4902        // Copies the digest PROVED stale. Left on disk they are worse than
4903        // clutter: once the winning route is removed or temporarily unmounted,
4904        // such a file is the only sighting of its id, arbitration is skipped
4905        // for want of a duplicate, and the stale generation is served instead
4906        // of the missing route being reported. They are swept only after a
4907        // winner for that id is established, so a scan that never finds one
4908        // keeps every copy it has.
4909        let mut rejected_copies: Vec<(TableId, crate::path::PathBuf, Arc<dyn crate::fs::Fs>)> =
4910            Vec::new();
4911        // Where an ambiguous id was accepted from, so a LATER sighting of it can
4912        // be judged against the winner rather than merely skipped.
4913        let mut accepted_copies: crate::HashMap<
4914            TableId,
4915            (crate::path::PathBuf, Arc<dyn crate::fs::Fs>),
4916        > = crate::HashMap::default();
4917        // First recovery failure per manifest id, kept until a later routed
4918        // folder yields a copy that opens. Whatever is left once every folder
4919        // is scanned is a table the manifest names and no copy delivers, so
4920        // its error is the open's.
4921        let mut unrecovered_sightings: crate::HashMap<TableId, crate::Error> =
4922            crate::HashMap::default();
4923        // Repair replacements whose authority could not be settled against the
4924        // damaged original beside them. Held until every folder is scanned: a
4925        // later routed copy that opens against the manifest settles it (the temp
4926        // is then provably NOT what the manifest names, so it is swept there and
4927        // then), and only an id no copy delivers surfaces its ambiguity.
4928        // A LIST, not a per-id map: routing can put a temp for the same id in
4929        // more than one folder, and each one is a file that has to be settled.
4930        let mut deferred_temps: Vec<(
4931            TableId,
4932            crate::path::PathBuf,
4933            Arc<dyn crate::fs::Fs>,
4934            crate::Error,
4935        )> = Vec::new();
4936
4937        // Scan all configured table folders (primary + level routes).
4938        let all_folders = config.all_tables_folders();
4939
4940        // One listing per folder, taken up front. The recovery loop has to know
4941        // whether an id is sighted in MORE THAN ONE routed folder BEFORE it
4942        // accepts the first sighting, and answering that later would mean
4943        // listing every folder a second time.
4944        let mut folder_scans: Vec<(
4945            &crate::path::PathBuf,
4946            &Arc<dyn crate::fs::Fs>,
4947            Vec<crate::fs::FsDirEntry>,
4948        )> = Vec::with_capacity(all_folders.len());
4949        // Ids present in more than one folder. Only these are digest-arbitrated
4950        // at open: a post-commit sweep that failed leaves an INTACT stale twin,
4951        // and `Table::recover` parses structure without re-deriving the
4952        // manifest's digest, so it opens the stale generation just as happily.
4953        // Ids sighted once cannot be ambiguous, and hashing them would turn
4954        // every open into a full read of the tree.
4955        let mut folder_sightings: crate::HashMap<TableId, usize> = crate::HashMap::default();
4956        for (table_base_folder, folder_fs) in &all_folders {
4957            if !folder_fs.exists(table_base_folder)? {
4958                folder_fs.create_dir_all(table_base_folder)?;
4959                fsync_directory(table_base_folder, &**folder_fs, config.sync_mode)?;
4960                if let Some(parent) = table_base_folder.parent() {
4961                    fsync_directory(parent, &**folder_fs, config.sync_mode)?;
4962                    if let Some(grandparent) = parent.parent() {
4963                        fsync_directory(grandparent, &**folder_fs, config.sync_mode)?;
4964                    }
4965                }
4966            }
4967
4968            // Pending repair swaps resolve FIRST: a committed swap's `{id}` file
4969            // is the superseded source until the rename lands, so adopting it
4970            // before the temp entry is processed would hand this session a
4971            // handle onto bytes the finished swap then replaces on disk.
4972            let mut dirents = folder_fs.read_dir(table_base_folder)?;
4973            dirents.sort_by_key(|e| {
4974                !matches!(
4975                    crate::file::TableDirEntry::classify(&e.file_name),
4976                    crate::file::TableDirEntry::RepairTmp(_)
4977                )
4978            });
4979            for dirent in &dirents {
4980                if let crate::file::TableDirEntry::Table(id) =
4981                    crate::file::TableDirEntry::classify(&dirent.file_name)
4982                {
4983                    *folder_sightings.entry(id).or_insert(0) += 1;
4984                }
4985            }
4986            folder_scans.push((table_base_folder, folder_fs, dirents));
4987        }
4988        let ambiguous_ids: crate::HashSet<TableId> = folder_sightings
4989            .into_iter()
4990            .filter(|(_, count)| *count > 1)
4991            .map(|(id, _)| id)
4992            .collect();
4993
4994        for (table_base_folder, folder_fs, dirents) in folder_scans {
4995            for dirent in dirents {
4996                let crate::fs::FsDirEntry {
4997                    path: table_file_path,
4998                    file_name,
4999                    is_dir,
5000                } = dirent;
5001
5002                let table_file_name = &file_name;
5003                if is_dir {
5004                    log::warn!(
5005                        "Skipping unexpected directory in tables folder: {}",
5006                        table_file_path.display()
5007                    );
5008                    continue;
5009                }
5010
5011                // One grammar decides what each name IS (`TableDirEntry`, shared
5012                // with the repair scan so the two can never disagree on
5013                // ownership); this match is the open's POLICY for each kind.
5014                let table_id = match crate::file::TableDirEntry::classify(table_file_name) {
5015                    // An in-place heal's detach copy, renamed over the live path
5016                    // on success: a survivor is a crash leftover no manifest ever
5017                    // references. Sweep it.
5018                    crate::file::TableDirEntry::HealTmp(_) => {
5019                        log::warn!(
5020                            "Removing abandoned heal copy: {}",
5021                            table_file_path.display()
5022                        );
5023                        Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5024                        continue;
5025                    }
5026                    // A crashed attestation publish: either the live sidecar it
5027                    // would have replaced still bridges the crash window, or the
5028                    // heal is re-run. Disposable.
5029                    crate::file::TableDirEntry::HealAttestTmp(_) => {
5030                        log::warn!(
5031                            "Removing abandoned heal-attest temp: {}",
5032                            table_file_path.display()
5033                        );
5034                        Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5035                        continue;
5036                    }
5037                    // A LIVE table's pending attestation is preserved (the next
5038                    // scrub reconciles a crashed digest refresh through it). One
5039                    // whose SST left the manifest is unreconcilable forever:
5040                    // sweep the orphan rather than re-process it on every open.
5041                    crate::file::TableDirEntry::HealAttest(attest_id) => {
5042                        if !manifest_ids.contains(&attest_id) {
5043                            log::warn!(
5044                                "Removing orphaned heal attestation (its table is gone): {}",
5045                                table_file_path.display()
5046                            );
5047                            Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5048                        }
5049                        continue;
5050                    }
5051                    // A crashed bound publish. Disposable.
5052                    crate::file::TableDirEntry::RestrictBoundTmp(_) => {
5053                        log::warn!(
5054                            "Removing abandoned restrict-bound temp: {}",
5055                            table_file_path.display()
5056                        );
5057                        Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5058                        continue;
5059                    }
5060                    // A LIVE table's restriction bound is preserved (manifest
5061                    // repair reads it); an ORPHAN one is swept so a reused id
5062                    // cannot later pick up a stale restriction.
5063                    crate::file::TableDirEntry::RestrictBound(bound_id) => {
5064                        if !manifest_ids.contains(&bound_id) {
5065                            log::warn!(
5066                                "Removing orphaned restrict-bound sidecar (its table is gone): {}",
5067                                table_file_path.display()
5068                            );
5069                            Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5070                        }
5071                        continue;
5072                    }
5073                    // A repair's replacement still at its temp name. The manifest
5074                    // is the authority on what it is — but it names the id in
5075                    // BOTH crash cases (before the commit its entry still
5076                    // describes the SOURCE beside the temp), so the entry's
5077                    // checksum decides: only a committed repair recorded the
5078                    // temp's digest, and it died before the swap — this file is
5079                    // what the manifest describes, so the swap is finished.
5080                    // Any other temp is an abandoned build (possibly truncated
5081                    // mid-write), and swapping it in would destroy the source
5082                    // the manifest names: it is garbage. An open resolves this
5083                    // exactly as a re-run of the repair would, from the durable
5084                    // manifest alone.
5085                    crate::file::TableDirEntry::RepairTmp(tmp_id) => {
5086                        #[cfg(feature = "std")]
5087                        {
5088                            let published = match table_map.get(&tmp_id) {
5089                                Some(&(_, manifest_checksum, _)) => {
5090                                    match crate::repair::repair_tmp_is_published(
5091                                        config,
5092                                        folder_fs,
5093                                        &table_file_path,
5094                                        tmp_id,
5095                                        manifest_checksum,
5096                                        recovery.restrictions.get(&tmp_id),
5097                                    ) {
5098                                        Ok(published) => published,
5099                                        // A refused mount / missing key says
5100                                        // nothing about which copy the manifest
5101                                        // names: surface it.
5102                                        Err(e) if e.is_environmental() => return Err(e),
5103                                        // Neither this temp nor the damaged
5104                                        // original beside it can prove it is the
5105                                        // manifest's copy. A LATER routed folder
5106                                        // may still hold that copy, and deciding
5107                                        // here would end the open on a leftover a
5108                                        // failed post-commit sweep left behind.
5109                                        // Defer: leave the temp untouched, keep
5110                                        // the ambiguity, and let the end of the
5111                                        // scan decide (see `deferred_temps`).
5112                                        Err(e) => {
5113                                            log::warn!(
5114                                                "repair replacement {} is ambiguous ({e}); \
5115                                                 deferring until every routed folder is scanned",
5116                                                table_file_path.display(),
5117                                            );
5118                                            deferred_temps.push((
5119                                                tmp_id,
5120                                                table_file_path,
5121                                                Arc::clone(folder_fs),
5122                                                e,
5123                                            ));
5124                                            continue;
5125                                        }
5126                                    }
5127                                }
5128                                None => false,
5129                            };
5130                            if published {
5131                                log::warn!(
5132                                    "Finishing a repair's pending swap of table {tmp_id}: {}",
5133                                    table_file_path.display()
5134                                );
5135                                crate::repair::commit_repair_tmp(
5136                                    folder_fs.as_ref(),
5137                                    &table_file_path,
5138                                    &table_base_folder.join(tmp_id.to_string()),
5139                                    config.sync_mode,
5140                                    recovery.restrictions.contains_key(&tmp_id),
5141                                )?;
5142                            } else {
5143                                log::warn!(
5144                                    "Removing abandoned repair replacement: {}",
5145                                    table_file_path.display()
5146                                );
5147                                // A RESTRICTED salvage also wrote
5148                                // `{temp}.restrict-bound`; that companion must
5149                                // go with the temp — its name classifies as
5150                                // Foreign and would fail this very open.
5151                                let companion =
5152                                    crate::restrict_bound::sidecar_path(&table_file_path);
5153                                if folder_fs.exists(&companion)? {
5154                                    Self::sweep_artifact(folder_fs.as_ref(), &companion)?;
5155                                }
5156                                Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5157                            }
5158                        }
5159                        // Without the repair module nothing here can verify or
5160                        // finish a swap the manifest may describe, and sweeping
5161                        // the temp could destroy the only copy of what the
5162                        // manifest names.
5163                        #[cfg(not(feature = "std"))]
5164                        {
5165                            if manifest_ids.contains(&tmp_id) {
5166                                log::error!(
5167                                    "Table {tmp_id} exists only as an unpublished repair \
5168                                     replacement; run a repair to finish it: {}",
5169                                    table_file_path.display()
5170                                );
5171                                return Err(crate::Error::Unrecoverable);
5172                            }
5173                            log::warn!(
5174                                "Removing abandoned repair replacement: {}",
5175                                table_file_path.display()
5176                            );
5177                            // Same companion rule as the std arm above (the
5178                            // `restrict_bound` helper is std-gated, so the
5179                            // name is spelled out).
5180                            let companion = table_base_folder.join(alloc::format!(
5181                                "{tmp_id}{}.restrict-bound",
5182                                crate::file::REPAIR_TMP_SUFFIX
5183                            ));
5184                            if folder_fs.exists(&companion)? {
5185                                Self::sweep_artifact(folder_fs.as_ref(), &companion)?;
5186                            }
5187                            Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5188                        }
5189                        continue;
5190                    }
5191                    // The companion's fate followed its temp when that entry
5192                    // resolved (first, by sort order): a finished swap renamed
5193                    // it into place, an abandoned build's sweep removed it. A
5194                    // survivor here is an orphan (its temp is gone) — remove
5195                    // it rather than reject the whole open over it.
5196                    crate::file::TableDirEntry::RepairTmpCompanion(_) => {
5197                        if folder_fs.exists(&table_file_path)? {
5198                            log::warn!(
5199                                "Removing orphaned repair-replacement sidecar: {}",
5200                                table_file_path.display()
5201                            );
5202                            Self::sweep_artifact(folder_fs.as_ref(), &table_file_path)?;
5203                        }
5204                        continue;
5205                    }
5206                    // Not a shape the engine names, so not engine state: passed
5207                    // over untouched. Refusing the store here would let any
5208                    // stray file (an operator's note, a backup, a desktop
5209                    // environment's directory metadata) make it unopenable,
5210                    // which is why scanners used to carry a list of foreign
5211                    // names to tolerate. The grammar answers it instead.
5212                    crate::file::TableDirEntry::Foreign => {
5213                        log::debug!(
5214                            "Ignoring {table_file_name:?} in the tables folder: not an engine file"
5215                        );
5216                        continue;
5217                    }
5218                    crate::file::TableDirEntry::Table(id) => id,
5219                };
5220
5221                // Remove from map to prevent duplicate recovery if the same
5222                // table file exists in multiple scanned folders.
5223                if let Some(entry) = table_map.remove(&table_id) {
5224                    let (level_idx, checksum, global_seqno) = entry;
5225                    let pin_filter = config.filter_block_pinning_policy.get(level_idx.into());
5226                    let pin_index = config.index_block_pinning_policy.get(level_idx.into());
5227
5228                    let table = {
5229                        let mut params = crate::table::RecoverParams::new(
5230                            table_file_path.clone(),
5231                            checksum,
5232                            table_id,
5233                            folder_fs.clone(),
5234                            config.comparator.clone(),
5235                            config.cache.clone(),
5236                        );
5237                        params.global_seqno = global_seqno;
5238                        params.tree_id = tree_id;
5239                        params.descriptor_table.clone_from(&config.descriptor_table);
5240                        params.pin_filter = pin_filter;
5241                        params.pin_index = pin_index;
5242                        params.encryption.clone_from(&config.encryption);
5243                        #[cfg(zstd_any)]
5244                        {
5245                            params.zstd_dictionary.clone_from(&config.zstd_dictionary);
5246                        }
5247                        #[cfg(feature = "metrics")]
5248                        {
5249                            params.metrics = metrics.clone();
5250                        }
5251                        match Table::recover(params) {
5252                            Ok(table) => table,
5253                            Err(e) => {
5254                                // A routed tree can hold this id in more than
5255                                // one folder, and repair's post-commit sweep of
5256                                // the copy it displaced can fail after the
5257                                // manifest is already durable. Ending the search
5258                                // on the first sighting would then shut the tree
5259                                // on a table that is present and intact one
5260                                // folder over. Put the id back, remember the
5261                                // failure, and keep scanning; if no folder
5262                                // yields the manifest's copy, this error is what
5263                                // the open reports.
5264                                log::warn!(
5265                                    "table {table_id} at {} did not recover ({e}); \
5266                                     looking for another routed copy",
5267                                    table_file_path.display(),
5268                                );
5269                                table_map.insert(table_id, entry);
5270                                unrecovered_sightings.entry(table_id).or_insert(e);
5271                                continue;
5272                            }
5273                        }
5274                    };
5275
5276                    // Opening proves the file is STRUCTURALLY sound, never that
5277                    // it is the generation the manifest committed: recover parses
5278                    // metadata and loads data blocks lazily. Where two folders
5279                    // hold this id, take the digest as the arbiter, exactly as
5280                    // repair does, so a stale twin an interrupted sweep left
5281                    // behind cannot win on scan order.
5282                    if ambiguous_ids.contains(&table_id) {
5283                        // The candidate is UNRESTRICTED here: the manifest's
5284                        // restrictions are attached when the version is built,
5285                        // which is after this scan. Digesting it whole would
5286                        // compare against a live-suffix digest and reject every
5287                        // copy of a restricted id, so the bound is supplied
5288                        // explicitly.
5289                        let live =
5290                            match table.suffix_checksum_for(recovery.restrictions.get(&table_id)) {
5291                                Ok(live) => live,
5292                                // A fault in the ENVIRONMENT says nothing about
5293                                // these bytes, so it propagates and a retry can
5294                                // re-read them.
5295                                Err(e) if e.is_environmental() => return Err(e),
5296                                // Anything else is damage to THIS copy: metadata
5297                                // can parse while a data extent is unreadable, and
5298                                // ending the scan on it would let a damaged
5299                                // displaced copy permanently block the intact one a
5300                                // folder over. Same treatment as a failed recover.
5301                                Err(e) => {
5302                                    log::warn!(
5303                                        "table {table_id} at {} could not be digested ({e}); \
5304                                     looking for another routed copy",
5305                                        table_file_path.display(),
5306                                    );
5307                                    table_map.insert(table_id, entry);
5308                                    unrecovered_sightings.entry(table_id).or_insert(e);
5309                                    continue;
5310                                }
5311                            };
5312                        if live != checksum {
5313                            log::warn!(
5314                                "table {table_id} at {} is not the generation the manifest \
5315                                 committed; looking for another routed copy",
5316                                table_file_path.display(),
5317                            );
5318                            table_map.insert(table_id, entry);
5319                            unrecovered_sightings.entry(table_id).or_insert(
5320                                crate::Error::ChecksumMismatch {
5321                                    got: live,
5322                                    expected: checksum,
5323                                },
5324                            );
5325                            rejected_copies.push((
5326                                table_id,
5327                                table_file_path,
5328                                Arc::clone(folder_fs),
5329                            ));
5330                            continue;
5331                        }
5332                        accepted_copies
5333                            .insert(table_id, (table_file_path.clone(), Arc::clone(folder_fs)));
5334                    }
5335
5336                    tables.push(table);
5337                    recovered_table_ids.insert(table_id);
5338                    unrecovered_sightings.remove(&table_id);
5339
5340                    if tables.len() % progress_mod == 0 {
5341                        log::debug!("Recovered {}/{cnt} tables", tables.len());
5342                    }
5343                } else if recovered_table_ids.contains(&table_id) {
5344                    // Duplicate sighting of an already-recovered manifest table
5345                    // (e.g., via symlink or case-insensitive alias). Never an
5346                    // orphan: that would delete the live SST. But an id the
5347                    // digest arbitrated has a KNOWN answer, so a later sighting
5348                    // is judged rather than merely skipped, or a stale twin
5349                    // scanned after the winner would outlive the open.
5350                    let stale = match accepted_copies.get(&table_id) {
5351                        Some((winner, winner_fs)) if *winner != table_file_path => {
5352                            differs_byte_for_byte(
5353                                &**winner_fs,
5354                                winner,
5355                                folder_fs.as_ref(),
5356                                &table_file_path,
5357                            )?
5358                        }
5359                        _ => false,
5360                    };
5361                    if stale {
5362                        rejected_copies.push((
5363                            table_id,
5364                            table_file_path.clone(),
5365                            Arc::clone(folder_fs),
5366                        ));
5367                    }
5368                    log::warn!(
5369                        "Skipping duplicate sighting of manifest table {table_id} in {}",
5370                        table_file_path.display(),
5371                    );
5372                } else {
5373                    orphaned_tables.push((table_file_path, folder_fs.clone()));
5374                }
5375            }
5376        }
5377
5378        // Every folder is scanned, so an id that never recovered has no copy
5379        // left to try. Report the failure that stopped it — a missing-table
5380        // diagnostic would hide the reason the file on disk could not be read.
5381        if let Some((_, e)) = unrecovered_sightings
5382            .into_iter()
5383            .find(|(id, _)| table_map.contains_key(id))
5384        {
5385            return Err(e);
5386        }
5387
5388        // A deferred replacement whose id RECOVERED from some folder is settled:
5389        // that copy matched the manifest, so this temp is provably NOT what the
5390        // manifest names — an abandoned build, removed here exactly as the
5391        // in-scan branch removes one. One whose id never arrived is still
5392        // ambiguous: the temp may be the only copy of what the manifest
5393        // describes, so the ambiguity is what the open reports.
5394        #[cfg(feature = "std")]
5395        for (tmp_id, temp_path, temp_fs, ambiguity) in deferred_temps {
5396            if !recovered_table_ids.contains(&tmp_id) {
5397                return Err(ambiguity);
5398            }
5399            log::warn!(
5400                "Removing abandoned repair replacement for table {tmp_id} (the copy this open \
5401                 recovered is what the manifest names): {}",
5402                temp_path.display(),
5403            );
5404            // The restricted-salvage companion classifies as Foreign and would
5405            // fail the next open, so it goes with the temp.
5406            let companion = crate::restrict_bound::sidecar_path(&temp_path);
5407            if temp_fs.exists(&companion)? {
5408                Self::sweep_artifact(temp_fs.as_ref(), &companion)?;
5409            }
5410            Self::sweep_artifact(temp_fs.as_ref(), &temp_path)?;
5411        }
5412
5413        if tables.len() < cnt {
5414            // Route configuration is NOT persisted.  This is a best-effort
5415            // heuristic: it checks each missing table's level against the
5416            // current routes, but cannot detect same-level path changes
5417            // (e.g., L0 routed to /hot_old → /hot_new).  Persisting route
5418            // provenance per-table in the manifest would enable exact
5419            // detection but requires a format change.
5420            //
5421            // - Level IS covered by a current route → its directory was scanned
5422            //   and the file was not found → data corruption / deletion.
5423            // - Level is NOT covered → falls back to primary (always scanned).
5424            //   If the table isn't there, it was likely in a route that has
5425            //   since been removed from the config.
5426            //
5427            // Return RouteMismatch only when ALL missing tables are on levels
5428            // not covered by any current route.  If ANY missing table is on a
5429            // covered level, at least one SST was genuinely lost.
5430            if let Some(routes) = &config.level_routes {
5431                let all_missing_uncovered = table_map
5432                    .values()
5433                    .all(|(level, _, _)| !routes.iter().any(|r| r.levels.contains(level)));
5434
5435                if all_missing_uncovered {
5436                    let found = tables.len();
5437                    let missing_ids: Vec<_> = table_map.keys().collect();
5438
5439                    log::error!(
5440                        "Route mismatch: expected {cnt} tables but found {found} — \
5441                         level_routes do not cover all previously used levels. \
5442                         Missing table IDs: {missing_ids:?}",
5443                    );
5444                    return Err(crate::Error::RouteMismatch {
5445                        expected: cnt,
5446                        found,
5447                    });
5448                }
5449            }
5450
5451            log::error!(
5452                "Recovered less tables than expected: {:?}",
5453                table_map.keys(),
5454            );
5455            return Err(crate::Error::Unrecoverable);
5456        }
5457
5458        log::debug!("Successfully recovered {} tables", tables.len());
5459
5460        // Pair each blob file with its live-data frontier: a file whose consumed
5461        // prefix was reclaimed in place records a checksum over the suffix only,
5462        // so the recovered view must carry the offset that digest starts at.
5463        let blob_ids_with_frontier: Vec<(crate::vlog::BlobFileId, crate::Checksum, u64)> = recovery
5464            .blob_file_ids
5465            .iter()
5466            .map(|&(id, checksum)| {
5467                (
5468                    id,
5469                    checksum,
5470                    recovery.blob_restrictions.get(&id).copied().unwrap_or(0),
5471                )
5472            })
5473            .collect();
5474        let (blob_files, orphaned_blob_files) = crate::vlog::recover_blob_files(
5475            &tree_path.join(crate::file::BLOBS_FOLDER),
5476            &blob_ids_with_frontier,
5477            tree_id,
5478            config.descriptor_table.as_ref(),
5479            &config.fs,
5480        )?;
5481
5482        let version = Version::from_recovery(recovery, &tables, &blob_files)?;
5483
5484        // Republish any restriction sidecar that never landed. The sidecar is
5485        // written AFTER its slice commits, so a failure (or a crash) in that
5486        // window leaves a committed restriction with no `.restrict-bound` file.
5487        // The manifest still describes it, so a normal open is unaffected — but
5488        // a later manifest-loss repair would find an unrestricted input beside
5489        // the slice output and publish BOTH histories, applying every merge
5490        // operand of the consumed prefix twice (operands are deliberately never
5491        // deduplicated across sources). The manifest is the authority for the
5492        // bound, so derive the missing sidecar from it here and the window
5493        // closes at the next open rather than staying open forever.
5494        #[cfg(feature = "std")]
5495        Self::republish_missing_restriction_sidecars(&version, config)?;
5496
5497        // NOTE: Cleanup old versions
5498        // But only after we definitely recovered the latest version.
5499        // Preserve the snapshot CURRENT references (and its `edits-` log) — the
5500        // latest version id has no file of its own under the incremental
5501        // manifest, so cleaning by it would delete the live snapshot.
5502        Self::cleanup_orphaned_version(tree_path, snapshot_id, &*config.fs)?;
5503
5504        // A copy the digest rejected goes only once its id has a winner: with
5505        // one it is provably superseded, without one it may be all that is
5506        // left. Its `.restrict-bound` companion goes with it, or the next scan
5507        // classifies that name as foreign and fails the open.
5508        for (table_id, path, copy_fs) in rejected_copies {
5509            if !recovered_table_ids.contains(&table_id) {
5510                continue;
5511            }
5512            log::warn!(
5513                "Removing superseded copy of table {table_id}: {}",
5514                path.display()
5515            );
5516            #[cfg(feature = "std")]
5517            {
5518                let companion = crate::restrict_bound::sidecar_path(&path);
5519                if copy_fs.exists(&companion)? {
5520                    Self::sweep_artifact(copy_fs.as_ref(), &companion)?;
5521                }
5522            }
5523            Self::sweep_artifact(copy_fs.as_ref(), &path)?;
5524        }
5525
5526        for (table_path, orphan_fs) in orphaned_tables {
5527            log::debug!("Deleting orphaned table {}", table_path.display());
5528            orphan_fs.remove_file(&table_path)?;
5529        }
5530
5531        for blob_file_path in orphaned_blob_files {
5532            log::debug!("Deleting orphaned blob file {}", blob_file_path.display());
5533            (*config.fs).remove_file(&blob_file_path)?;
5534        }
5535
5536        Ok(version)
5537    }
5538
5539    /// Writes the `.restrict-bound` sidecar of every restricted table in
5540    /// `version` that has none, so the bound the manifest holds is recoverable
5541    /// without it. An existing sidecar is REREAD rather than assumed good: its
5542    /// mere presence proves nothing, and a repair that later trusts a corrupt
5543    /// one derives a conservative bound (dropping up to one live block), while
5544    /// a valid-but-STALE one — the shape a second slice leaves when its own
5545    /// write fails — restricts LESS than reality and resurrects consumed rows.
5546    /// A sidecar that already records this table and this bound is left alone,
5547    /// so a healthy tree does not churn the file on every open. Failures
5548    /// propagate — an unrecoverable restriction is exactly what this closes, so
5549    /// silently skipping it would keep the window open.
5550    ///
5551    /// The sidecar is a filesystem artifact of the tight-space reclaim path, so
5552    /// this is a no-op without `std` — a build without it never writes one.
5553    #[cfg(feature = "std")]
5554    fn republish_missing_restriction_sidecars(
5555        version: &Version,
5556        config: &Config,
5557    ) -> crate::Result<()> {
5558        for table in version.iter_tables() {
5559            let Some(bound) = table.restrict_lower_bound() else {
5560                continue;
5561            };
5562            let recorded =
5563                crate::restrict_bound::read(&*table.fs, &table.path, config.encryption.as_deref())?;
5564            let state = match &recorded {
5565                crate::restrict_bound::SidecarRead::Present(id, recorded_bound)
5566                    if *id == table.metadata.id && recorded_bound.as_slice() == bound.as_ref() =>
5567                {
5568                    continue;
5569                }
5570                crate::restrict_bound::SidecarRead::Present(..) => {
5571                    "disagrees with the manifest (stale bound or another table)"
5572                }
5573                crate::restrict_bound::SidecarRead::Corrupt => "unreadable",
5574                crate::restrict_bound::SidecarRead::Missing => "absent",
5575            };
5576            log::warn!(
5577                "table {} carries a committed restriction whose sidecar is {state}; \
5578                 republishing it from the manifest",
5579                table.id(),
5580            );
5581            table.write_restrict_sidecar(bound, config.sync_mode)?;
5582        }
5583        Ok(())
5584    }
5585
5586    /// Removes a recovery-swept artifact (an abandoned heal copy / temp / marker),
5587    /// treating a concurrent removal (`NotFound`) as success. A benign race (a
5588    /// retry, or another scanner that already swept it) must not fail recovery,
5589    /// matching [`Self::cleanup_orphaned_version`].
5590    fn sweep_artifact(fs: &dyn Fs, path: &Path) -> crate::Result<()> {
5591        match fs.remove_file(path) {
5592            Ok(()) => Ok(()),
5593            Err(e) if e.kind() == crate::io::ErrorKind::NotFound => Ok(()),
5594            Err(e) => Err(e.into()),
5595        }
5596    }
5597
5598    /// Removes stale manifest files left by older generations: every `v{id}`
5599    /// snapshot except the live one (`v{snapshot_id}`) and every `edits-{id}`
5600    /// log except the live snapshot's (`edits-{snapshot_id}`). A crashed
5601    /// rotation can leak an old snapshot or its log; this sweeps them on open.
5602    /// The live snapshot and log are exactly the generation `CURRENT` points at.
5603    ///
5604    /// # Behavior change vs pre-Fs-trait code
5605    ///
5606    /// The previous implementation used `std::fs::read_dir` + `to_string_lossy()`,
5607    /// which silently skipped non-UTF-8 filenames. `Fs::read_dir` returns
5608    /// `InvalidData` for such entries instead (see [`FsDirEntry`](crate::fs::FsDirEntry) docs), so this
5609    /// function now fails fast on non-UTF-8 names. This is intentional: version
5610    /// files are always `v{u64}` — any non-UTF-8 entry indicates filesystem
5611    /// corruption and should surface as an error rather than be silently ignored.
5612    fn cleanup_orphaned_version(
5613        path: &Path,
5614        snapshot_id: crate::version::VersionId,
5615        fs: &dyn crate::fs::Fs,
5616    ) -> crate::Result<()> {
5617        let snapshot_str = format!("v{snapshot_id}");
5618        let log_str = format!("edits-{snapshot_id}");
5619
5620        for dirent in fs.read_dir(path)? {
5621            if dirent.is_dir {
5622                continue;
5623            }
5624
5625            let name = &dirent.file_name;
5626            let is_orphan_snapshot = name.starts_with('v') && *name != snapshot_str;
5627            let is_orphan_log = name.starts_with("edits-") && *name != log_str;
5628            if is_orphan_snapshot || is_orphan_log {
5629                log::trace!("Cleanup orphaned manifest file {name}");
5630                match fs.remove_file(&dirent.path) {
5631                    Ok(()) => {}
5632                    Err(e) if e.kind() == crate::io::ErrorKind::NotFound => {}
5633                    Err(e) => return Err(e.into()),
5634                }
5635            }
5636        }
5637
5638        Ok(())
5639    }
5640}
5641
5642/// Returns `true` if the directory contains version-related artifacts
5643/// (a `tables/` subdir, a `blobs/` subdir, or any `vN` manifest file).
5644///
5645/// Used by [`Tree::open`] to distinguish a genuinely fresh directory
5646/// (safe to `create_new`) from a half-written checkpoint or other
5647/// interrupted sealing (must error rather than silently overwrite).
5648///
5649/// A missing parent directory is treated as "no state" — `create_new`
5650/// is what creates the directory in the first place, so callers may
5651/// invoke `Tree::open` against a path that does not exist yet.
5652fn has_existing_version_state(folder: &Path, fs: &dyn Fs) -> crate::Result<bool> {
5653    if fs.exists(&folder.join(crate::file::TABLES_FOLDER))?
5654        || fs.exists(&folder.join(crate::file::BLOBS_FOLDER))?
5655    {
5656        return Ok(true);
5657    }
5658    let entries = match fs.read_dir(folder) {
5659        Ok(entries) => entries,
5660        Err(e) if e.kind() == crate::io::ErrorKind::NotFound => return Ok(false),
5661        Err(e) => return Err(e.into()),
5662    };
5663    for entry in entries {
5664        let name = &entry.file_name;
5665        if name.starts_with('v') && name.len() > 1 && name[1..].bytes().all(|c| c.is_ascii_digit())
5666        {
5667            return Ok(true);
5668        }
5669    }
5670    Ok(false)
5671}
5672
5673/// Whether two sightings of one table id PROVABLY hold different bytes.
5674///
5675/// Used to judge a duplicate scanned after its id was already accepted: the
5676/// winner matched the manifest, so a copy whose bytes differ from it cannot
5677/// also be the committed generation and is safe to sweep. Identical bytes mean
5678/// an alias or an exact copy, which is kept.
5679///
5680/// Only a completed comparison is proof. A read that fails on the BYTES proves
5681/// nothing about which generation this is, so it answers `false` and the file
5682/// stays; an ENVIRONMENTAL fault propagates, since a retry can re-read it.
5683///
5684/// # Errors
5685///
5686/// Propagates an environmental read failure of either file.
5687fn differs_byte_for_byte(
5688    winner_fs: &dyn crate::fs::Fs,
5689    winner: &Path,
5690    candidate_fs: &dyn crate::fs::Fs,
5691    candidate: &Path,
5692) -> crate::Result<bool> {
5693    let digest = |fs: &dyn crate::fs::Fs, path: &Path| -> crate::Result<Option<u128>> {
5694        match crate::file::checksum_from_with_overrides(fs, path, 0, &[]) {
5695            Ok(d) => Ok(Some(d)),
5696            Err(e) if e.is_environmental() => Err(e),
5697            Err(_) => Ok(None),
5698        }
5699    };
5700    let (Some(a), Some(b)) = (digest(winner_fs, winner)?, digest(candidate_fs, candidate)?) else {
5701        return Ok(false);
5702    };
5703    Ok(a != b)
5704}
5705
5706/// Raises a query's lower bound to a table's tight-space restriction, if any.
5707///
5708/// Keys below `restriction` are the punched-out prefix served by the
5709/// replacement table, so a range estimate must not charge them to the
5710/// restricted view. Returns `lo` unchanged when the table is unrestricted or
5711/// the restriction is at or below `lo`.
5712fn effective_lower_bound<'a>(
5713    lo: core::ops::Bound<&'a [u8]>,
5714    restriction: Option<&'a [u8]>,
5715    cmp: &dyn crate::comparator::UserComparator,
5716) -> core::ops::Bound<&'a [u8]> {
5717    use core::cmp::Ordering;
5718    use core::ops::Bound;
5719    match (lo, restriction) {
5720        (Bound::Unbounded, Some(rb)) => Bound::Included(rb),
5721        (Bound::Included(k) | Bound::Excluded(k), Some(rb))
5722            if cmp.compare(rb, k) == Ordering::Greater =>
5723        {
5724            Bound::Included(rb)
5725        }
5726        _ => lo,
5727    }
5728}
5729
5730#[cfg(test)]
5731mod cardinality_tests;
5732
5733#[cfg(test)]
5734#[expect(clippy::expect_used, reason = "test code")]
5735mod scan_since_freeze_tests;
5736
5737#[cfg(all(test, feature = "metrics"))]
5738mod cache_stats_tests;
5739
5740#[cfg(all(test, feature = "std"))]
5741#[expect(clippy::expect_used, reason = "test code")]
5742mod restricted_reclaim_tests;