Skip to main content

lsm_tree/blob_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
5pub mod field_row;
6mod gc;
7pub mod handle;
8pub mod ingest;
9#[cfg(feature = "std")]
10pub(crate) mod links;
11pub(crate) mod overwritten;
12pub(crate) mod released;
13
14#[doc(hidden)]
15pub use gc::{FragmentationEntry, FragmentationMap};
16
17use crate::path::PathBuf;
18use crate::tree::inner::{FlushGuard, VersionsWriteGuard};
19use crate::{
20    Config, Memtable, ScanSinceEvent, SeqNo, TableId, TreeId, UserKey, UserValue,
21    abstract_tree::{AbstractTree, RangeItem},
22    coding::{Decode, Encode},
23    iter_guard::{IterGuard, IterGuardImpl},
24    table::Table,
25    tree::inner::MemtableId,
26    value::InternalValue,
27    version::Version,
28    vlog::{
29        Accessor, BlobFile, BlobFileWriter,
30        blob_file::value_encoder::{Separated, ValueEncoder},
31    },
32};
33use alloc::sync::Arc;
34#[cfg(not(feature = "std"))]
35use alloc::{boxed::Box, string::ToString, vec::Vec};
36use core::ops::RangeBounds;
37use handle::BlobIndirection;
38
39/// Iterator value guard
40pub struct Guard {
41    tree: crate::BlobTree,
42    version: Version,
43    /// Whether resolving the value caches it, as the tree said when the scan
44    /// began.
45    fill: crate::config::BlobCacheFill,
46    /// The values of this item the scan read ahead for it, when it does not
47    /// cache them: resolved from here, not read again.
48    prefetched: Vec<(crate::vlog::ValueHandle, UserValue)>,
49    kv: crate::Result<InternalValue>,
50    /// Present when this guard came from a scan that can read ahead; resolving
51    /// a value through it is what arms that read-ahead.
52    prefetch: Option<Arc<ScanPrefetch>>,
53    /// Counts `prefetched` against its scan's read-ahead bound until this
54    /// guard is resolved or dropped: held for its drop alone.
55    _held: Option<HeldReadAhead>,
56}
57
58/// Bytes of read-ahead values a guard holds, released with the guard: what
59/// lets a scan see that the guards it handed out still hold a read's worth.
60struct HeldReadAhead {
61    state: Arc<ScanPrefetch>,
62    bytes: usize,
63}
64
65impl Drop for HeldReadAhead {
66    fn drop(&mut self) {
67        self.state
68            .held
69            .fetch_sub(self.bytes, core::sync::atomic::Ordering::Relaxed);
70    }
71}
72
73impl IterGuard for Guard {
74    fn into_inner_if(
75        self,
76        pred: impl Fn(&UserKey) -> bool,
77    ) -> crate::Result<(UserKey, Option<UserValue>)> {
78        let kv = self.kv?;
79
80        // Deliberately does NOT arm the scan's read-ahead, even on a match.
81        // This method exists so a caller can decide per key whether the value
82        // is worth reading, and a sparse predicate is the case it exists for.
83        // Arming on the first match would read ahead for every later item
84        // before its own predicate could reject it, which is exactly the I/O
85        // the caller asked to avoid: one match in a thousand would turn a
86        // selective scan into a full read of the value log. A caller that
87        // wants every value uses `into_inner`, and that is what arms it.
88        if pred(&kv.key.user_key) {
89            resolve_value_handle(
90                self.tree.id(),
91                &self.tree.index.config.cache,
92                self.fill,
93                &self.prefetched,
94                &self.version,
95                #[cfg(feature = "metrics")]
96                self.tree.metrics(),
97                kv,
98            )
99            .map(|(k, v)| (k, Some(v)))
100        } else {
101            Ok((kv.key.user_key, None))
102        }
103    }
104
105    fn key(self) -> crate::Result<UserKey> {
106        self.kv.map(|kv| kv.key.user_key)
107    }
108
109    fn size(self) -> crate::Result<u32> {
110        let kv = self.kv?;
111
112        if kv.key.value_type.is_indirection() {
113            let mut cursor = crate::io::Cursor::new(kv.value);
114            Ok(BlobIndirection::decode_from(&mut cursor)?.size)
115        } else if kv.key.value_type.is_cell_row() {
116            field_row::logical_len(&kv.value)
117        } else {
118            #[expect(clippy::cast_possible_truncation, reason = "values are u32 max length")]
119            Ok(kv.value.len() as u32)
120        }
121    }
122
123    fn into_inner(self) -> crate::Result<(UserKey, UserValue)> {
124        if let Some(p) = &self.prefetch {
125            p.note_resolved();
126        }
127        resolve_value_handle(
128            self.tree.id(),
129            &self.tree.index.config.cache,
130            self.fill,
131            &self.prefetched,
132            &self.version,
133            #[cfg(feature = "metrics")]
134            self.tree.metrics(),
135            self.kv?,
136        )
137    }
138}
139
140/// Writes the oldest value `ahead` holds to the blob file and its indirection
141/// to the table. `false` when it holds none.
142fn write_encoded_value(
143    ahead: &mut ValueEncoder,
144    blob_writer: &mut BlobFileWriter,
145    table_writer: &mut crate::table::multi_writer::MultiWriter,
146    encoded: &mut Vec<u8>,
147) -> crate::Result<bool> {
148    let Some(next) = ahead.take() else {
149        return Ok(false);
150    };
151    let (separated, value) = next?;
152    let Separated {
153        mut key,
154        size,
155        class,
156    } = separated;
157    blob_writer.select_lifetime_class(class)?;
158    let vhandle = blob_writer.write_encoded(&key.user_key, key.seqno, &value, size)?;
159    let indirection = BlobIndirection { vhandle, size };
160    encoded.clear();
161    indirection.encode_into(encoded)?;
162    key.value_type = crate::ValueType::Indirection;
163    table_writer.write(InternalValue::new(key, UserValue::new(encoded)))?;
164    table_writer.register_blob(indirection);
165    Ok(true)
166}
167
168/// Finishes a flush's blob and table outputs. Nothing orders the syncs of the
169/// two against each other, only both against the manifest edit that names them,
170/// so a synced flush runs them side by side.
171#[cfg_attr(
172    not(feature = "std"),
173    expect(unused_variables, reason = "without threads the two run in turn")
174)]
175fn finish_flush(
176    blob_writer: BlobFileWriter,
177    table_writer: crate::table::multi_writer::MultiWriter,
178    durability: crate::config::FlushDurability,
179) -> (
180    crate::Result<Vec<BlobFile>>,
181    crate::Result<crate::table::multi_writer::Written>,
182) {
183    #[cfg(feature = "std")]
184    if durability == crate::config::FlushDurability::Synced {
185        return std::thread::scope(|scope| {
186            let blob = scope.spawn(|| blob_writer.finish());
187            let table = table_writer.finish();
188            let blob = blob
189                .join()
190                .unwrap_or_else(|panic| std::panic::resume_unwind(panic));
191            (blob, table)
192        });
193    }
194    (blob_writer.finish(), table_writer.finish())
195}
196
197/// The value `prefetched` holds for `vhandle`: read ahead for this very item,
198/// under its key, so its place and declared size are what identify it.
199fn prefetched_value(
200    prefetched: &[(crate::vlog::ValueHandle, UserValue)],
201    vhandle: &crate::vlog::ValueHandle,
202) -> Option<UserValue> {
203    prefetched
204        .iter()
205        .find(|(held, _)| {
206            held.blob_file_id == vhandle.blob_file_id
207                && held.offset == vhandle.offset
208                && held.on_disk_size == vhandle.on_disk_size
209        })
210        .map(|(_, value)| value.clone())
211}
212
213pub(crate) fn resolve_value_handle(
214    tree_id: TreeId,
215    cache: &crate::Cache,
216    fill: crate::config::BlobCacheFill,
217    prefetched: &[(crate::vlog::ValueHandle, UserValue)],
218    version: &Version,
219    #[cfg(feature = "metrics")] metrics: &crate::metrics::Metrics,
220    item: InternalValue,
221) -> RangeItem {
222    if item.key.value_type.is_cell_row() {
223        let accessor = Accessor::new(
224            &version.blob_files,
225            fill,
226            #[cfg(feature = "metrics")]
227            Some(metrics),
228        );
229        let value = field_row::resolve_row(&item.value, |vptr| {
230            if let Some(value) = prefetched_value(prefetched, &vptr.vhandle) {
231                return Ok(value);
232            }
233            // A reference to an object the version no longer holds is damage,
234            // reported as such rather than read as an empty field.
235            accessor
236                .get(tree_id, &item.key.user_key, &vptr.vhandle, cache)?
237                .ok_or(crate::Error::InvalidHeader(
238                    "field row: a reference names a blob file the version does not hold",
239                ))
240        })?;
241        return Ok((item.key.user_key, UserValue::from(value)));
242    }
243    if item.key.value_type.is_indirection() {
244        let mut cursor = crate::io::Cursor::new(item.value);
245        let vptr = BlobIndirection::decode_from(&mut cursor)?;
246        if let Some(value) = prefetched_value(prefetched, &vptr.vhandle) {
247            return Ok((item.key.user_key, value));
248        }
249
250        // Resolve indirection using value log. No dictionary passed: each blob
251        // file carries the one its own descriptor names, pinned for as long as
252        // this version keeps the file alive.
253        let accessor = Accessor::new(
254            &version.blob_files,
255            fill,
256            #[cfg(feature = "metrics")]
257            Some(metrics),
258        );
259
260        // An indirection naming a blob file the version does not hold is
261        // damage, reported as such, the way a cell row's dangling reference is.
262        let value = accessor
263            .get(tree_id, &item.key.user_key, &vptr.vhandle, cache)?
264            .ok_or(crate::Error::InvalidHeader(
265                "blob tree: an indirection names a blob file the version does not hold",
266            ))?;
267        Ok((item.key.user_key, value))
268    } else {
269        let k = item.key.user_key;
270        let v = item.value;
271        Ok((k, v))
272    }
273}
274
275/// How a batched read gets one separated value: see [`BlobSource::plan_value`].
276pub(crate) enum ValuePlan<'v> {
277    /// The value, from the cache.
278    Ready(UserValue),
279    /// One record to read.
280    Read(ValueRead<'v>),
281    /// Read on the value's own path.
282    Alone,
283}
284
285/// One blob record a batched read reads: `len` bytes of `blob_file` at the
286/// handle's offset.
287pub(crate) struct ValueRead<'v> {
288    pub(crate) blob_file: &'v BlobFile,
289    pub(crate) vhandle: crate::vlog::ValueHandle,
290    pub(crate) len: usize,
291}
292
293/// Where a blob tree's values kept in the value log are read from: the tree
294/// they belong to and the cache their blocks go through.
295#[derive(Clone)]
296pub(crate) struct BlobSource {
297    pub(crate) tree_id: TreeId,
298    pub(crate) cache: Arc<crate::Cache>,
299    /// Whether the values read through it go into the cache, as the tree's
300    /// runtime config said when the read began.
301    pub(crate) fill: crate::config::BlobCacheFill,
302    #[cfg(feature = "metrics")]
303    pub(crate) metrics: Arc<crate::metrics::Metrics>,
304}
305
306impl BlobSource {
307    /// The value `item` stands for in `version`: read from the value log when
308    /// it is an indirection, its own bytes otherwise.
309    pub(crate) fn value(&self, version: &Version, item: InternalValue) -> crate::Result<UserValue> {
310        resolve_value_handle(
311            self.tree_id,
312            &self.cache,
313            self.fill,
314            &[],
315            version,
316            #[cfg(feature = "metrics")]
317            &self.metrics,
318            item,
319        )
320        .map(|(_, value)| value)
321    }
322
323    /// How a batched read gets the value the indirection `item` stands for in
324    /// `version`: from the cache, by reading one record of a blob file, or on
325    /// its own path through [`Self::value`] (a cell row, a reference the
326    /// version does not hold, a malformed one), which also reports what is
327    /// wrong with it. Nothing here blocks on I/O.
328    pub(crate) fn plan_value<'v>(
329        &self,
330        version: &'v Version,
331        item: &InternalValue,
332    ) -> ValuePlan<'v> {
333        if !item.key.value_type.is_indirection() {
334            return ValuePlan::Alone;
335        }
336        let mut cursor = crate::io::Cursor::new(&item.value[..]);
337        let Ok(indirection) = BlobIndirection::decode_from(&mut cursor) else {
338            return ValuePlan::Alone;
339        };
340        let vhandle = indirection.vhandle;
341        if let Some(value) = self
342            .cache
343            .get_blob(self.tree_id, &vhandle, &item.key.user_key)
344        {
345            return ValuePlan::Ready(value);
346        }
347        let (Some(blob_file), Ok(len)) = (
348            version.blob_files.get(vhandle.blob_file_id),
349            crate::vlog::blob_file::reader::record_len(item.key.user_key.len(), &vhandle),
350        ) else {
351            return ValuePlan::Alone;
352        };
353        ValuePlan::Read(ValueRead {
354            blob_file,
355            vhandle,
356            len,
357        })
358    }
359
360    /// `blob_file` open, from the descriptor cache or opened now: it may
361    /// block, so a batched read calls it from a job.
362    pub(crate) fn open_blob_file(
363        &self,
364        blob_file: &BlobFile,
365    ) -> crate::Result<Arc<dyn crate::fs::FsFile>> {
366        let id = crate::GlobalTableId::from((self.tree_id, blob_file.id()));
367        Ok(blob_file
368            .file_accessor()
369            .get_or_open_blob_file(&id, &blob_file.0.path)?
370            .0)
371    }
372
373    /// The value `read` planned, from the record's `bytes` as read: checked
374    /// against `key` and the handle as a single read checks it, counted, and
375    /// cached when the read caches what it reads.
376    ///
377    /// # Errors
378    ///
379    /// The record fails its checks; the caller then reads the value on its
380    /// own path, which reports the failure as a single read would.
381    pub(crate) fn finish_value(
382        &self,
383        version: &Version,
384        key: &[u8],
385        read: &ValueRead<'_>,
386        file: &dyn crate::fs::FsFile,
387        bytes: &crate::Slice,
388    ) -> crate::Result<UserValue> {
389        let accessor = Accessor::new(
390            &version.blob_files,
391            self.fill,
392            #[cfg(feature = "metrics")]
393            Some(&self.metrics),
394        );
395        let reader = crate::vlog::blob_file::reader::Reader::new(read.blob_file, file);
396        let mut decoded = 0;
397        let parsed = reader.parse_record(key, &read.vhandle, bytes, &mut decoded);
398        accessor.count_decoded(decoded);
399        let value = parsed?;
400        if self.fill.fills() {
401            let detached = self
402                .cache
403                .insert_blob(self.tree_id, &read.vhandle, key, value.clone());
404            accessor.count_gather(detached);
405        }
406        Ok(value)
407    }
408
409    /// Counts the read of `read`'s record as issued.
410    pub(crate) fn count_issued(&self, version: &Version, read: &ValueRead<'_>) {
411        Accessor::new(
412            &version.blob_files,
413            self.fill,
414            #[cfg(feature = "metrics")]
415            Some(&self.metrics),
416        )
417        .count_request(read.len);
418    }
419
420    /// The object `indirection` names under `key`, read from `version`'s blob
421    /// files.
422    ///
423    /// # Errors
424    ///
425    /// Returns an error if the object cannot be read, or `version` holds no
426    /// blob file of that id: a reference that names nothing is damage, never
427    /// an empty value.
428    pub(crate) fn object(
429        &self,
430        version: &Version,
431        key: &[u8],
432        indirection: &BlobIndirection,
433    ) -> crate::Result<UserValue> {
434        Accessor::new(
435            &version.blob_files,
436            self.fill,
437            #[cfg(feature = "metrics")]
438            Some(&self.metrics),
439        )
440        .get(self.tree_id, key, &indirection.vhandle, &self.cache)?
441        .ok_or(crate::Error::InvalidHeader(
442            "field row: a reference names a blob file the version does not hold",
443        ))
444    }
445
446    /// [`Self::object`], taken from `local` when [`Self::prefetch`] read it
447    /// ahead there.
448    #[cfg(feature = "columnar")]
449    pub(crate) fn object_prefetched(
450        &self,
451        version: &Version,
452        key: &[u8],
453        indirection: &BlobIndirection,
454        local: &mut crate::vlog::Prefetched,
455    ) -> crate::Result<UserValue> {
456        match local.take(key, &indirection.vhandle) {
457            Some(object) => Ok(object),
458            None => self.object(version, key, indirection),
459        }
460    }
461
462    /// Reads the objects `items` name (each its key, handle and a slot the
463    /// read fills) in as few requests as their places in the blob files
464    /// allow, for [`Self::object_prefetched`] to find them: into the cache
465    /// when the read caches what it reads, else into `local`. A run of objects
466    /// close together in one file is one read. Best effort: an object it does
467    /// not read is read on its own when asked for.
468    #[cfg(feature = "columnar")]
469    pub(crate) fn prefetch(
470        &self,
471        version: &Version,
472        items: &mut Vec<(&[u8], crate::vlog::ValueHandle, usize)>,
473        local: &mut crate::vlog::Prefetched,
474    ) {
475        let into = if self.fill.fills() {
476            crate::vlog::PrefetchInto::Cache
477        } else {
478            crate::vlog::PrefetchInto::Local(local)
479        };
480        Accessor::new(
481            &version.blob_files,
482            self.fill,
483            #[cfg(feature = "metrics")]
484            Some(&self.metrics),
485        )
486        .prefetch(
487            self.tree_id,
488            items,
489            &self.cache,
490            PREFETCH_MAX_GAP,
491            PREFETCH_MAX_READ,
492            into,
493        );
494    }
495}
496
497/// A key-value-separated log-structured merge tree
498///
499/// This tree is a composite structure, consisting of an
500/// index tree (LSM-tree) and a log-structured value log
501/// to reduce write amplification.
502///
503/// Closed like a [`Tree`](crate::Tree): once the [`TreeClose`](crate::TreeClose)
504/// of [`close`](Self::close) fires, the tree writes nothing into its directory,
505/// its blob folder included.
506#[derive(Clone)]
507pub struct BlobTree {
508    /// Index tree that holds value handles or small inline values
509    #[doc(hidden)]
510    pub index: crate::Tree,
511
512    pub(crate) blobs_folder: Arc<PathBuf>,
513}
514
515impl BlobTree {
516    /// A handle that does not keep the tree open; see [`WeakTree`](crate::WeakTree).
517    #[must_use]
518    pub fn downgrade(&self) -> crate::WeakTree {
519        crate::WeakTree::of(&self.index, Some(Arc::clone(&self.blobs_folder)))
520    }
521
522    /// Gives up this handle and returns the signal of the tree being closed;
523    /// see [`Tree::close`](crate::Tree::close). A blob file an iterator or a
524    /// snapshot still holds keeps the tree open, as a table does.
525    pub fn close(self) -> crate::TreeClose {
526        crate::close::close(self.index)
527    }
528
529    /// Physical footprint of the stale blob files a full compaction would
530    /// relocate: the linked, stale, non-dead subset across every live table (the
531    /// SAME set the merge gate budgets via `pick_blob_files_to_rewrite`), summed
532    /// by on-disk size. Zero when KV separation is unconfigured.
533    fn full_compaction_blob_need(&self, version: &crate::version::Version) -> crate::Result<u64> {
534        let Some(blob_opts) = &self.index.config.kv_separation_opts else {
535            return Ok(0);
536        };
537        let all_tables: crate::HashSet<TableId> = version.iter_tables().map(Table::id).collect();
538        crate::compaction::worker::pick_blob_files_to_rewrite(&all_tables, version, blob_opts)?
539            .iter()
540            .try_fold(0u64, |acc, bf| bf.physical_size().map(|size| acc + size))
541    }
542
543    /// Opens the tree, coming by the directory lock as `lock` says; see
544    /// [`Tree::open`](crate::Tree::open), and when this open made it, what
545    /// takes it back.
546    pub(crate) fn open(
547        config: Config,
548        #[cfg(feature = "std")] lock: crate::config::DirectoryLock,
549        names: crate::tree::NewFolderNames,
550    ) -> crate::Result<(Self, Option<crate::tree::Creation>)> {
551        use crate::file::BLOBS_FOLDER;
552
553        let (index, creation) = crate::Tree::open(
554            config,
555            #[cfg(feature = "std")]
556            lock,
557            names,
558        )?;
559
560        // The folder's name is an entry of the tree folder, and a blob file in
561        // it must not be named by the manifest before that entry is durable.
562        // A new tree made the folder before its first snapshot, whose
563        // directory sync covered the name, and an opened one syncs the tree
564        // folder at its first edit append (the log found at open is not
565        // trusted to have a durable name), which is the only way a blob file
566        // comes to be named. A name found here may not be durable yet (a
567        // process that died before its sync leaves one the page cache still
568        // shows), and that first append covers it. A folder this open makes,
569        // though, may come after the first append: the open itself installs
570        // an edit when it registers a dictionary. Its name is synced here.
571        let blobs_folder = index.config.path.join(BLOBS_FOLDER);
572        let folder = (|| -> crate::Result<()> {
573            let made = !(*index.config.fs).exists(&blobs_folder)?;
574            (*index.config.fs).create_dir_all(&blobs_folder)?;
575            if made {
576                crate::file::fsync_directory(
577                    &index.config.path,
578                    &*index.config.fs,
579                    index.config.sync_mode,
580                )?;
581            }
582            Ok(())
583        })();
584        // A tree this open made, whose open then fails, is taken back: the
585        // caller is told it was not opened, and a retry makes it anew.
586        if let Err(e) = folder {
587            if let Some(creation) = creation {
588                creation.take_back(index);
589            }
590            return Err(e);
591        }
592
593        let blob_file_id_to_continue_with = index
594            .current_version()
595            .blob_files
596            .list_ids()
597            .max()
598            .map(|x| x + 1)
599            .unwrap_or_default();
600
601        index
602            .0
603            .blob_file_id_counter
604            .set(blob_file_id_to_continue_with);
605
606        Ok((
607            Self {
608                index,
609                blobs_folder: Arc::new(blobs_folder),
610            },
611            creation,
612        ))
613    }
614
615    /// Resolves a single key against a pre-acquired [`SuperVersion`](crate::version::SuperVersion),
616    /// reading its value through `source`, taken as the read began, so every
617    /// key of one read caches as the tree said then.
618    fn resolve_key(
619        &self,
620        super_version: &crate::version::SuperVersion,
621        source: &BlobSource,
622        key: &[u8],
623        seqno: SeqNo,
624    ) -> crate::Result<Option<UserValue>> {
625        let Some(item) = crate::Tree::get_internal_entry_from_version(
626            super_version,
627            key,
628            seqno,
629            self.index.config.comparator.as_ref(),
630        )?
631        else {
632            return Ok(None);
633        };
634        // The newest version is an operand: the key's value is its operands
635        // merged onto its base, as a batch of keys resolves it. Without a
636        // merge operator the operand is the value.
637        if item.key.value_type.is_merge_operand()
638            && let Some(merge_operator) = &self.index.config.merge_operator
639        {
640            return crate::Tree::resolve_point_merge(
641                super_version,
642                key,
643                seqno,
644                merge_operator,
645                Some(source),
646            );
647        }
648
649        source.value(&super_version.version, item).map(Some)
650    }
651
652    /// Iterate change events with `seqno >= target_seqno`, resolving
653    /// KV-separated values.
654    ///
655    /// Same change-data-capture contract as [`Tree::scan_since_seqno`](crate::Tree::scan_since_seqno),
656    /// but a blob-indirected value is resolved from its blob file into a
657    /// [`ScanSinceEvent::Insert`] carrying the real value, so a downstream
658    /// consumer can replicate without access to the source's blob files.
659    /// Block-skip, seqno ordering, and tombstone handling are identical to the
660    /// standard-tree path (the same shared aggregation backs both).
661    ///
662    /// # Panics
663    ///
664    /// Panics if the internal version-history lock is poisoned.
665    ///
666    /// # Errors
667    ///
668    /// Returns `Err` if reading the index, a data block, or a referenced blob
669    /// fails.
670    pub fn scan_since_seqno(
671        &self,
672        target_seqno: SeqNo,
673    ) -> crate::Result<impl Iterator<Item = ScanSinceEvent> + use<>> {
674        let fill = self.blob_cache_fill();
675        self.index
676            .scan_since_seqno_with(target_seqno, true, |version, entry| {
677                let seqno = entry.key.seqno;
678                let (key, value) = resolve_value_handle(
679                    self.id(),
680                    &self.index.config.cache,
681                    fill,
682                    &[],
683                    version,
684                    #[cfg(feature = "metrics")]
685                    self.metrics(),
686                    entry,
687                )?;
688                Ok(ScanSinceEvent::Insert { key, value, seqno })
689            })
690    }
691
692    /// Starts a multi-get of `keys` at `seqno` that its caller drives, as
693    /// [`Tree::start_multi_get`](crate::Tree::start_multi_get) does; a value
694    /// kept in the value log is read by a job the read hands out.
695    ///
696    /// # Errors
697    ///
698    /// Returns [`crate::Error::SnapshotBelowRetention`] when `seqno` is below
699    /// what the tree retains.
700    pub fn start_multi_get<K: Into<UserKey>>(
701        &self,
702        keys: impl IntoIterator<Item = K>,
703        seqno: SeqNo,
704    ) -> crate::Result<crate::resumable::Step> {
705        let snapshot = self.index.snapshot_for_read(seqno)?;
706        let seqno = crate::Tree::read_ceiling(&snapshot, seqno);
707        let keys: Vec<UserKey> = keys.into_iter().map(Into::into).collect();
708        // Each key a point lookup, counted once as the read starts, not on
709        // every resume.
710        snapshot
711            .version
712            .read_mix()
713            .note_points(u64::try_from(keys.len()).unwrap_or(u64::MAX));
714        // One source for the whole read: every key caches as the tree said
715        // when the read began.
716        let source = self.blob_source();
717        Ok(crate::resumable::ResumableMultiGet::start(
718            crate::tree::read_job::ReadCtx {
719                super_version: crate::version::SnapshotRef::Owned((*snapshot).clone()),
720                keys,
721                seqno,
722                comparator: Arc::clone(&self.index.config.comparator),
723                merge_operator: self.index.config.merge_operator.clone(),
724                merge_base: Some(source.clone()),
725                values: crate::tree::read_job::Values::Blob(source),
726                metadata_budget: self.index.config.multi_get_metadata_budget,
727            },
728        ))
729    }
730
731    /// Whether a read beginning now caches the values it reads from blob
732    /// files.
733    fn blob_cache_fill(&self) -> crate::config::BlobCacheFill {
734        self.index.runtime_config.load().blob_cache_fill
735    }
736
737    /// Where this tree's values kept in the value log are read from.
738    fn blob_source(&self) -> BlobSource {
739        BlobSource {
740            tree_id: self.id(),
741            cache: Arc::clone(&self.index.config.cache),
742            fill: self.blob_cache_fill(),
743            #[cfg(feature = "metrics")]
744            metrics: Arc::clone(self.metrics()),
745        }
746    }
747
748    /// Range-scoped variant of [`Self::scan_since_seqno`], with the same
749    /// contract as
750    /// [`Tree::scan_since_seqno_in_range`](crate::Tree::scan_since_seqno_in_range):
751    /// point events only for keys inside `range`, range deletions when their
752    /// span overlaps it, and index SSTs outside the bounds skipped unread.
753    ///
754    /// # Panics
755    ///
756    /// Panics if the internal version-history lock is poisoned.
757    ///
758    /// # Errors
759    ///
760    /// Same as [`Self::scan_since_seqno`].
761    pub fn scan_since_seqno_in_range<K: AsRef<[u8]>, R: core::ops::RangeBounds<K>>(
762        &self,
763        target_seqno: SeqNo,
764        range: R,
765    ) -> crate::Result<impl Iterator<Item = ScanSinceEvent> + use<K, R>> {
766        let bounds = crate::tree::range_to_user_bounds(&range);
767        let fill = self.blob_cache_fill();
768        self.index.scan_since_seqno_scoped(
769            target_seqno,
770            true,
771            |version, entry| {
772                let seqno = entry.key.seqno;
773                let (key, value) = resolve_value_handle(
774                    self.id(),
775                    &self.index.config.cache,
776                    fill,
777                    &[],
778                    version,
779                    #[cfg(feature = "metrics")]
780                    self.metrics(),
781                    entry,
782                )?;
783                Ok(ScanSinceEvent::Insert { key, value, seqno })
784            },
785            Some(&bounds),
786        )
787    }
788
789    /// Writes `key` as a row of `fields` at `seqno`.
790    ///
791    /// Each [`Field`](field_row::Field) names its column and physical type,
792    /// and both are stored with the row, so a stored row always reads with
793    /// the layout it was written with. A column id is a field id of the
794    /// columnar format (from [`FIRST_FIELD_COLUMN`](field_row::FIRST_FIELD_COLUMN)
795    /// up to, not including, [`RESERVED_COLUMNS`](field_row::RESERVED_COLUMNS)),
796    /// so a columnar table can store the
797    /// field as that column. The row keeps its fields in ascending column
798    /// order, whatever order they are given in. Plain reads of the key return
799    /// the fields in that order, framed as the columnar format frames a row's value
800    /// sub-columns (a variable-width field as a little-endian `u32` length and
801    /// its bytes, a fixed-width one as its bytes), with every
802    /// [`Cell::Ref`](field_row::Cell::Ref) replaced by its object. A
803    /// [`Cell::Value`](field_row::Cell::Value) at or above its column's
804    /// threshold
805    /// ([`KvSeparationOptions::cell_separation_threshold`](crate::KvSeparationOptions::cell_separation_threshold),
806    /// else the tree's separation threshold) is stored in a blob file when the
807    /// row is flushed.
808    ///
809    /// A [`Cell::Ref`](field_row::Cell::Ref) keeps an object the key already
810    /// holds without rewriting it, which is how a metadata-only update leaves
811    /// a large field in place. Take the references from the key's latest
812    /// version and write this row above it.
813    ///
814    /// Returns the added item's size and the new memtable size, as
815    /// [`AbstractTree::insert`] does.
816    ///
817    /// # Errors
818    ///
819    /// Returns [`crate::Error::BlobRef`] if a reference was read from another
820    /// key or tree, two fields name one object, or a reference is stale (its
821    /// object was moved or let go of since the read: read the key again);
822    /// [`crate::Error::CellRow`] if a column is not a field id, two fields
823    /// share a column or a fixed-width field is not its type's width; and an
824    /// error if the row has
825    /// more than `u16::MAX` fields or a cell exceeds `u32::MAX` bytes. Nothing
826    /// is written then.
827    ///
828    /// # Examples
829    ///
830    /// ```
831    /// # use lsm_tree::{AbstractTree, Config, KvSeparationOptions, SequenceNumberCounter};
832    /// # use lsm_tree::blob_tree::field_row::Field;
833    /// # let folder = lsm_tree::get_tmp_folder();
834    /// let tree = Config::new(folder.path(), SequenceNumberCounter::default(), SequenceNumberCounter::default())
835    ///     .with_kv_separation(Some(KvSeparationOptions::default()))
836    ///     .open()?;
837    /// # let lsm_tree::AnyTree::Blob(tree) = tree else { unreachable!() };
838    /// // Given out of order, stored and read in column order.
839    /// tree.insert_cells("doc", &[Field::bytes(4, b"body"), Field::bytes(3, b"draft")], 0)?;
840    /// let value = tree.get("doc", 1)?.expect("written");
841    /// assert_eq!(&*value, b"\x05\0\0\0draft\x04\0\0\0body");
842    /// # Ok::<(), lsm_tree::Error>(())
843    /// ```
844    pub fn insert_cells<K: Into<UserKey>>(
845        &self,
846        key: K,
847        fields: &[field_row::Field<'_>],
848        seqno: SeqNo,
849    ) -> crate::Result<(u64, u64)> {
850        let key = key.into();
851        let mut row = Vec::with_capacity(fields.len());
852        for field in fields {
853            let cell = match &field.cell {
854                field_row::Cell::Value(bytes) => field_row::RowCell::Value(bytes),
855                field_row::Cell::Ref(reference) => {
856                    if reference.tree != self.id() {
857                        return Err(crate::Error::BlobRef("reference read from another tree"));
858                    }
859                    if !crate::comparator::same_user_key(reference.key, &key) {
860                        return Err(crate::Error::BlobRef("reference read from another key"));
861                    }
862                    // Written back by a caller, a reference never owns its
863                    // object: the version that wrote the object, or the one a
864                    // compaction moved ownership to, still does.
865                    field_row::RowCell::Ref {
866                        indirection: reference.indirection,
867                        owner: false,
868                    }
869                }
870            };
871            row.push(field_row::RowField {
872                column: field.column,
873                tag: field.tag,
874                cell,
875            });
876        }
877        field_row::order_fields(&mut row)?;
878        let refs = fields.iter().filter_map(|field| match &field.cell {
879            field_row::Cell::Ref(reference) => Some(reference),
880            field_row::Cell::Value(_) => None,
881        });
882        for (i, a) in refs.clone().enumerate() {
883            if refs.clone().skip(i + 1).any(|b| a == b) {
884                return Err(crate::Error::BlobRef("one object in two fields"));
885            }
886        }
887        let value = field_row::encode_row(&row)?;
888        let sourced: Vec<(BlobIndirection, u64)> = refs
889            .map(|reference| (reference.indirection, reference.source))
890            .collect();
891        // The caller's bytes, as an ingested row counts them: the key and the
892        // value fields, not the row's encoding; a reference brings none.
893        #[cfg(feature = "metrics")]
894        let payload = key.len()
895            + fields
896                .iter()
897                .map(|field| match &field.cell {
898                    field_row::Cell::Value(bytes) => bytes.len(),
899                    field_row::Cell::Ref(_) => 0,
900                })
901                .sum::<usize>();
902        let written = self.index.append_cell_row(
903            InternalValue::from_components(key, value, seqno, crate::ValueType::CellRow),
904            &sourced,
905        )?;
906        // Charged once the row is in: a row refused for a stale reference is
907        // written again, and counted, by the retry.
908        #[cfg(feature = "metrics")]
909        self.index.0.metrics.accepted_value(payload);
910        Ok(written)
911    }
912
913    /// The fields of `key` as of `seqno`, references included as references:
914    /// the read a metadata-only update starts from. No blob object is read.
915    ///
916    /// Returns `None` when the key is absent at `seqno`. References to write
917    /// back must come from the key's latest version; a reference read at an
918    /// older `seqno` may name an object a newer version already let go of.
919    ///
920    /// # Errors
921    ///
922    /// Returns [`crate::Error::SnapshotBelowRetention`] when `seqno` is below
923    /// what the tree retains, and [`crate::Error::BlobRef`] when the key's
924    /// version at `seqno` was not written as cells.
925    ///
926    /// # Examples
927    ///
928    /// ```
929    /// # use lsm_tree::{AbstractTree, Config, KvSeparationOptions, SequenceNumberCounter};
930    /// # use lsm_tree::blob_tree::field_row::{Cell, Field};
931    /// # let folder = lsm_tree::get_tmp_folder();
932    /// # let tree = Config::new(folder.path(), SequenceNumberCounter::default(), SequenceNumberCounter::default())
933    /// #     .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(16)))
934    /// #     .open()?;
935    /// # let lsm_tree::AnyTree::Blob(tree) = tree else { unreachable!() };
936    /// let body = vec![b'x'; 1_000];
937    /// tree.insert_cells("doc", &[Field::bytes(3, b"draft"), Field::bytes(4, &body)], 0)?;
938    /// tree.flush_active_memtable(0)?;
939    ///
940    /// // Change the status, keep the body where it is.
941    /// let row = tree.get_cells("doc", 1)?.expect("written");
942    /// let mut fields = row.fields()?;
943    /// assert!(matches!(fields[1].cell, Cell::Ref(_)));
944    /// fields[0].cell = Cell::Value(b"final");
945    /// tree.insert_cells("doc", &fields, 1)?;
946    /// # Ok::<(), lsm_tree::Error>(())
947    /// ```
948    pub fn get_cells<K: AsRef<[u8]>>(
949        &self,
950        key: K,
951        seqno: SeqNo,
952    ) -> crate::Result<Option<field_row::RowCells>> {
953        let read = self.cell_read(seqno)?;
954        read.version.version.read_mix().note_points(1);
955        let Some(item) = crate::Tree::get_internal_entry_from_version(
956            &read.version,
957            key.as_ref(),
958            seqno,
959            self.index.config.comparator.as_ref(),
960        )?
961        else {
962            return Ok(None);
963        };
964        if !item.key.value_type.is_cell_row() {
965            return Err(crate::Error::BlobRef("the key's version is not a cell row"));
966        }
967        Ok(Some(field_row::RowCells {
968            key: item.key.user_key,
969            row: item.value,
970            read: Arc::new(read),
971        }))
972    }
973
974    /// The rows written as cells in `range` as of `seqno`, in key order, each
975    /// with its references as references, as [`Self::get_cells`] reads one.
976    ///
977    /// A key whose version at `seqno` is stored whole rather than as cells is
978    /// passed over: read it with a plain range read. Every row comes from one
979    /// read, which keeps its objects readable and its references writable as
980    /// [`RowCells`](field_row::RowCells) describes, for as long as any of its
981    /// rows is held.
982    ///
983    /// # Errors
984    ///
985    /// Returns [`crate::Error::SnapshotBelowRetention`] when `seqno` is below
986    /// what the tree retains; the iterator yields an error for a row it
987    /// cannot read.
988    ///
989    /// # Examples
990    ///
991    /// ```
992    /// # use lsm_tree::{AbstractTree, Config, KvSeparationOptions, SeqNo, SequenceNumberCounter};
993    /// # use lsm_tree::blob_tree::field_row::{Cell, Field};
994    /// # let folder = lsm_tree::get_tmp_folder();
995    /// # let tree = Config::new(folder.path(), SequenceNumberCounter::default(), SequenceNumberCounter::default())
996    /// #     .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(16)))
997    /// #     .open()?;
998    /// # let lsm_tree::AnyTree::Blob(tree) = tree else { unreachable!() };
999    /// let body = vec![b'x'; 1_000];
1000    /// for key in ["a", "b"] {
1001    ///     tree.insert_cells(key, &[Field::bytes(3, b"draft"), Field::bytes(4, &body)], 0)?;
1002    /// }
1003    /// tree.insert("plain", "value", 0);
1004    /// tree.flush_active_memtable(0)?;
1005    ///
1006    /// // Publish every draft, keeping each body where it is.
1007    /// let rows: Vec<_> = tree.range_cells::<&str, _>(.., 1)?.collect::<lsm_tree::Result<_>>()?;
1008    /// assert_eq!(rows.len(), 2);
1009    /// for row in &rows {
1010    ///     let mut fields = row.fields()?;
1011    ///     fields[0].cell = Cell::Value(b"final");
1012    ///     tree.insert_cells(row.key().clone(), &fields, 1)?;
1013    /// }
1014    /// # Ok::<(), lsm_tree::Error>(())
1015    /// ```
1016    pub fn range_cells<K: AsRef<[u8]>, R: RangeBounds<K>>(
1017        &self,
1018        range: R,
1019        seqno: SeqNo,
1020    ) -> crate::Result<
1021        Box<dyn DoubleEndedIterator<Item = crate::Result<field_row::RowCells>> + Send + 'static>,
1022    > {
1023        let read = Arc::new(self.cell_read(seqno)?);
1024        let rows = crate::Tree::create_internal_range_with_prefix_hash(
1025            read.version.clone(),
1026            &range,
1027            seqno,
1028            None,
1029            self.index.config.merge_operator.clone(),
1030            self.index.config.comparator.clone(),
1031            None,
1032            Some(self.blob_source()),
1033        );
1034        Ok(Box::new(rows.filter_map(move |item| match item {
1035            Ok(item) if item.key.value_type.is_cell_row() => Some(Ok(field_row::RowCells {
1036                key: item.key.user_key,
1037                row: item.value,
1038                read: Arc::clone(&read),
1039            })),
1040            Ok(_) => None,
1041            Err(e) => Some(Err(e)),
1042        })))
1043    }
1044
1045    /// A reference-aware read as of `seqno`: the latest version and the
1046    /// read's registration, taken together under the history's read lock. An
1047    /// install records its releases under the write lock, so none newer than
1048    /// this version can go unrecorded.
1049    fn cell_read(&self, seqno: SeqNo) -> crate::Result<field_row::CellRead> {
1050        let history = self.index.version_history.read();
1051        let version = history.latest_version_ref().clone();
1052        version.check_serves(seqno)?;
1053        let token = history.released().register(version.version.id());
1054        drop(history);
1055        Ok(field_row::CellRead {
1056            tree: self.id(),
1057            version,
1058            source: self.blob_source(),
1059            token,
1060        })
1061    }
1062
1063    /// Runs a projected columnar scan across the tree, as
1064    /// [`Tree::columnar_scan`](crate::Tree::columnar_scan) does, reading each
1065    /// row's declared fields as late as it can.
1066    ///
1067    /// The rows are decided first on their keys, seqnos and value types; only
1068    /// the rows the scan returns have their fields read. A field of a row
1069    /// written as cells is read out of the row (or, in a columnar table, out
1070    /// of the field's own column), and its object is read from its blob file
1071    /// only when the field is projected: a projection of compact fields reads
1072    /// no blob at all. A row stored whole has its fields read through the
1073    /// projection's [`ValueProjector`](crate::ValueProjector), its value read
1074    /// from the value log first when it is kept there. When the predicate
1075    /// runs on a projected field, that field is read first and the objects of
1076    /// the other fields are read only for the rows it keeps.
1077    ///
1078    /// A field is projected as a declared field, with its type and what its
1079    /// absence means; the intrinsic key, seqno and value-type columns may be
1080    /// projected by id. A field holds its value, not how the tree stores it,
1081    /// so a row's value type reads as a value.
1082    ///
1083    /// # Errors
1084    ///
1085    /// Returns [`crate::Error::Projection`] when a value column is projected
1086    /// by id alone or the predicate runs on one no field declares, and
1087    /// otherwise as [`Tree::columnar_scan`](crate::Tree::columnar_scan), and,
1088    /// lazily, when a field is stored under another type than declared or an
1089    /// object cannot be read.
1090    ///
1091    /// # Examples
1092    ///
1093    /// ```
1094    /// # use lsm_tree::{AbstractTree, Config, KvSeparationOptions, SeqNo, SequenceNumberCounter};
1095    /// use lsm_tree::blob_tree::field_row::{Field, TypeTag};
1096    /// use lsm_tree::{Absent, ProjectedField, Projection};
1097    /// # let folder = lsm_tree::get_tmp_folder();
1098    /// # let tree = Config::new(folder.path(), SequenceNumberCounter::default(), SequenceNumberCounter::default())
1099    /// #     .with_kv_separation(Some(KvSeparationOptions::default().separation_threshold(16)))
1100    /// #     .open()?;
1101    /// # let lsm_tree::AnyTree::Blob(tree) = tree else { unreachable!() };
1102    /// let body = vec![b'x'; 1_000];
1103    /// tree.insert_cells("doc", &[Field::bytes(3, b"draft"), Field::bytes(4, &body)], 0)?;
1104    /// tree.flush_active_memtable(0)?;
1105    ///
1106    /// // The status alone: the body stays in its blob file, unread.
1107    /// let status = Projection::new().field(ProjectedField::new(3, TypeTag::Bytes, Absent::Null)?);
1108    /// let mut batches = tree.columnar_scan(status, None, SeqNo::MAX, ..)?;
1109    /// let batch = batches.next().expect("one batch")?;
1110    /// assert_eq!(batch.row_count, 1);
1111    /// # Ok::<(), lsm_tree::Error>(())
1112    /// ```
1113    #[cfg(feature = "columnar")]
1114    pub fn columnar_scan<R: core::ops::RangeBounds<UserKey>>(
1115        &self,
1116        projection: impl Into<crate::Projection>,
1117        predicate: Option<&crate::table::columnar_predicate::ColumnRangePredicate>,
1118        seqno: SeqNo,
1119        range: R,
1120    ) -> crate::Result<crate::tree::columnar_scan::ColumnarScan> {
1121        self.index
1122            .columnar_scan(projection, predicate, seqno, range)
1123    }
1124
1125    /// Updates the live [`RuntimeConfig`](crate::runtime_config::RuntimeConfig),
1126    /// as [`Tree::update_runtime_config`](crate::Tree::update_runtime_config)
1127    /// does for the index tree.
1128    ///
1129    /// The blob files share the snapshot: a change of
1130    /// [`blob_compression`](crate::runtime_config::RuntimeConfig::blob_compression)
1131    /// applies to the next blob file written, and every earlier one keeps the
1132    /// codec it recorded.
1133    ///
1134    /// # Errors
1135    ///
1136    /// Same as [`Tree::update_runtime_config`](crate::Tree::update_runtime_config),
1137    /// including a blob compression that names a dictionary the tree does not
1138    /// hold.
1139    pub fn update_runtime_config<F>(&self, mutator: F) -> crate::Result<()>
1140    where
1141        F: FnOnce(&mut crate::runtime_config::RuntimeConfig),
1142    {
1143        self.index.update_runtime_config(mutator)
1144    }
1145
1146    /// Snapshot of the current runtime config, shared by the index and the
1147    /// blob files.
1148    #[must_use]
1149    pub fn runtime_config(&self) -> Arc<crate::runtime_config::RuntimeConfig> {
1150        self.index.runtime_config()
1151    }
1152
1153    /// Stores `dict` in the tree, as
1154    /// [`Tree::register_zstd_dictionary`](crate::Tree::register_zstd_dictionary)
1155    /// does. The step before a blob compression that names it: an update
1156    /// naming a dictionary the tree does not hold is refused.
1157    ///
1158    /// # Errors
1159    ///
1160    /// Same as [`Tree::register_zstd_dictionary`](crate::Tree::register_zstd_dictionary).
1161    #[cfg(zstd_any)]
1162    pub fn register_zstd_dictionary(
1163        &self,
1164        dict: Arc<crate::compression::ZstdDictionary>,
1165    ) -> crate::Result<()> {
1166        self.index.register_zstd_dictionary(dict)
1167    }
1168}
1169
1170impl crate::abstract_tree::sealed::Sealed for BlobTree {}
1171
1172/// Wasted bytes between two blob records worth reading through to merge their
1173/// reads. One record's header plus a short key is under a hundred bytes, so a
1174/// gap this size means the records are effectively adjacent: the skipped bytes
1175/// cost less than the second read's setup would.
1176const PREFETCH_MAX_GAP: u64 = 4 * 1_024;
1177
1178/// Cap on a single coalesced prefetch read. Bounds the transient buffer a scan
1179/// holds regardless of how large the configured window is or how big the values
1180/// in it turn out to be.
1181const PREFETCH_MAX_READ: usize = 4 * 1_024 * 1_024;
1182
1183impl BlobTree {
1184    /// Read-ahead state for one scan, seeded from the tree's configuration,
1185    /// caching under `fill` as the scan's source does.
1186    fn scan_prefetch_state(&self, fill: crate::config::BlobCacheFill) -> Arc<ScanPrefetch> {
1187        let window = self
1188            .index
1189            .config
1190            .kv_separation_opts
1191            .as_ref()
1192            .map_or(0, |o| o.scan_prefetch as usize);
1193
1194        Arc::new(ScanPrefetch {
1195            resolving: core::sync::atomic::AtomicBool::new(false),
1196            held: core::sync::atomic::AtomicUsize::new(0),
1197            fill,
1198            window,
1199            max_gap: PREFETCH_MAX_GAP,
1200            max_read: PREFETCH_MAX_READ,
1201        })
1202    }
1203}
1204
1205/// The values `kv` reads from blob files that a scan read ahead into `local`,
1206/// taken out of it for `kv`'s guard: its own value, or its cells' objects.
1207fn take_prefetched(
1208    local: &mut crate::vlog::Prefetched,
1209    kv: &InternalValue,
1210) -> Vec<(crate::vlog::ValueHandle, UserValue)> {
1211    let key = &kv.key.user_key;
1212    if kv.key.value_type.is_cell_row() {
1213        let Ok(refs) = field_row::row_refs(&kv.value) else {
1214            return Vec::new();
1215        };
1216        return refs
1217            .into_iter()
1218            .filter_map(|(vptr, _)| {
1219                local
1220                    .take(key, &vptr.vhandle)
1221                    .map(|value| (vptr.vhandle, value))
1222            })
1223            .collect();
1224    }
1225    if !kv.key.value_type.is_indirection() {
1226        return Vec::new();
1227    }
1228    let mut cursor = crate::io::Cursor::new(&kv.value[..]);
1229    let Ok(vptr) = BlobIndirection::decode_from(&mut cursor) else {
1230        return Vec::new();
1231    };
1232    local
1233        .take(key, &vptr.vhandle)
1234        .map(|value| alloc::vec![(vptr.vhandle, value)])
1235        .unwrap_or_default()
1236}
1237
1238/// Maps a raw merge-pipeline item into a KV-separated iterator guard that
1239/// resolves the blob handle lazily against `version`.
1240fn blob_guard(
1241    tree: &crate::BlobTree,
1242    version: &Version,
1243    fill: crate::config::BlobCacheFill,
1244    item: crate::Result<InternalValue>,
1245) -> IterGuardImpl {
1246    IterGuardImpl::Blob(Guard {
1247        tree: tree.clone(),
1248        version: version.clone(),
1249        fill,
1250        prefetched: Vec::new(),
1251        kv: item,
1252        prefetch: None,
1253        _held: None,
1254    })
1255}
1256
1257/// Whether a scan's caller is actually resolving separated values, shared
1258/// between the scan iterator and the guards it has handed out.
1259///
1260/// A guard resolves its value only if the caller asks: `key()` never touches
1261/// the blob file, and `into_inner_if` can reject an item on its key first.
1262/// Reading ahead unconditionally would therefore charge a key-only scan for
1263/// values nobody wants. So the guards report the first resolution, and only
1264/// then does the iterator start reading ahead: a scan that never asks for a
1265/// value never pays, and one that asks for every value pays a solo read just
1266/// for the first.
1267pub(crate) struct ScanPrefetch {
1268    resolving: core::sync::atomic::AtomicBool,
1269    /// Bytes of values read ahead into the scan's own store that guards it
1270    /// handed out still hold, unresolved. Within two reads, so a `usize`
1271    /// holds it on every target, 32-bit ones without 64-bit atomics too.
1272    held: core::sync::atomic::AtomicUsize,
1273    /// Whether the scan caches the values it reads, as the tree said when the
1274    /// scan began; when it does not, it keeps what it reads ahead itself.
1275    fill: crate::config::BlobCacheFill,
1276    /// How many upcoming items to gather per read-ahead.
1277    window: usize,
1278    /// Wasted bytes between two records worth swallowing to merge their reads.
1279    max_gap: u64,
1280    /// Cap on one coalesced read.
1281    max_read: usize,
1282}
1283
1284impl ScanPrefetch {
1285    /// Records that the caller resolved a value, arming the read-ahead.
1286    pub(crate) fn note_resolved(&self) {
1287        self.resolving
1288            .store(true, core::sync::atomic::Ordering::Relaxed);
1289    }
1290
1291    /// Whether the next item should come from a read-ahead window. A zero
1292    /// window is checked here so a disabled knob costs one load per item
1293    /// instead of an empty refill.
1294    fn armed(&self) -> bool {
1295        self.window > 0 && self.resolving.load(core::sync::atomic::Ordering::Relaxed)
1296    }
1297}
1298
1299/// A KV-separated scan that reads its upcoming values ahead in coalesced runs.
1300///
1301/// Sits between the merge pipeline and the guards: it buffers a window of
1302/// upcoming items, warms their blobs through
1303/// [`Accessor::prefetch`](crate::vlog::Accessor::prefetch), and hands them out
1304/// one at a time. Guards resolve exactly as they always did, and hit the cache
1305/// instead of the disk.
1306struct PrefetchScan<I> {
1307    inner: I,
1308    /// Items pulled from the FRONT of `inner` and not yet handed out.
1309    buf: alloc::collections::VecDeque<crate::Result<InternalValue>>,
1310    /// Set once `inner` has yielded its last item from the back, which is when
1311    /// reverse iteration has to start draining `buf` instead.
1312    inner_back_done: bool,
1313    /// Latched copy of [`ScanPrefetch::armed`]. The shared flag only ever goes
1314    /// false to true, so once this iterator has seen it there is no reason to
1315    /// keep loading the atomic for every item it yields.
1316    armed: bool,
1317    state: Arc<ScanPrefetch>,
1318    tree: crate::BlobTree,
1319    version: Version,
1320    /// The values of the window read ahead when the scan does not cache
1321    /// them, until the guards of their items take them.
1322    local: crate::vlog::Prefetched,
1323}
1324
1325impl<I: Iterator<Item = crate::Result<InternalValue>>> PrefetchScan<I> {
1326    /// Pulls the next window into `buf` and warms the values it points at.
1327    fn fill(&mut self) {
1328        // What the window before left untaken (an item whose guard was
1329        // dropped, a value it held twice) is no use to this one.
1330        self.local.clear();
1331        // Sized here rather than at construction: a scan that never resolves a
1332        // value never refills, and should not pay for a window it will not use.
1333        // After the first refill the capacity persists, so this is a no-op.
1334        self.buf.reserve(self.state.window);
1335        for _ in 0..self.state.window {
1336            match self.inner.next() {
1337                Some(item) => self.buf.push_back(item),
1338                None => break,
1339            }
1340        }
1341
1342        // Decode the handles of the items that actually carry one. A window is
1343        // usually all-indirection or all-inline (separation is by value size),
1344        // so this is either the whole window or nothing. Sized up front: the
1345        // window is the only allocation a refill makes, and `prefetch` filters
1346        // and sorts it in place rather than building its own copy.
1347        //
1348        // The indirection is decoded from a BORROWED view of the value. Cloning
1349        // it would bump a refcount per item on the scan's hottest loop, for a
1350        // cursor that is dropped three lines later.
1351        let mut handles: Vec<(&[u8], crate::vlog::ValueHandle, usize)> =
1352            Vec::with_capacity(self.buf.len());
1353        for item in &self.buf {
1354            let Ok(kv) = item else { continue };
1355            if kv.key.value_type.is_cell_row() {
1356                // A cell row's references are read when the row resolves, as
1357                // an indirection is; warming them is the same read-ahead.
1358                if let Ok(refs) = field_row::row_refs(&kv.value) {
1359                    for (vptr, _) in refs {
1360                        handles.push((&kv.key.user_key, vptr.vhandle, 0));
1361                    }
1362                }
1363                continue;
1364            }
1365            if !kv.key.value_type.is_indirection() {
1366                continue;
1367            }
1368            let mut cursor = crate::io::Cursor::new(&kv.value[..]);
1369            let Ok(vptr) = BlobIndirection::decode_from(&mut cursor) else {
1370                continue;
1371            };
1372            handles.push((&kv.key.user_key, vptr.vhandle, 0));
1373        }
1374
1375        if handles.len() < 2 {
1376            return;
1377        }
1378
1379        let accessor = Accessor::new(
1380            &self.version.blob_files,
1381            self.state.fill,
1382            #[cfg(feature = "metrics")]
1383            Some(self.tree.metrics()),
1384        );
1385
1386        // Into the cache when the scan caches what it reads, else into its own
1387        // store, where the guards of the window's items take their values.
1388        // While the guards handed out still hold a read's worth unresolved (a
1389        // caller keeping them, say), the scan reads nothing more ahead: its
1390        // memory stays bounded by two reads, not by the scan, and the guards
1391        // that follow read their values when resolved.
1392        let into = if self.state.fill.fills() {
1393            crate::vlog::PrefetchInto::Cache
1394        } else if self.state.held.load(core::sync::atomic::Ordering::Relaxed) >= self.state.max_read
1395        {
1396            return;
1397        } else {
1398            crate::vlog::PrefetchInto::Local(&mut self.local)
1399        };
1400        accessor.prefetch(
1401            self.tree.id(),
1402            &mut handles,
1403            &self.tree.index.config.cache,
1404            self.state.max_gap,
1405            self.state.max_read,
1406            into,
1407        );
1408    }
1409
1410    fn guard(&mut self, item: crate::Result<InternalValue>) -> IterGuardImpl {
1411        let prefetched = match &item {
1412            Ok(kv) if !self.local.is_empty() => take_prefetched(&mut self.local, kv),
1413            _ => Vec::new(),
1414        };
1415        let held = (!prefetched.is_empty()).then(|| {
1416            let bytes = prefetched.iter().map(|(_, value)| value.len()).sum();
1417            self.state
1418                .held
1419                .fetch_add(bytes, core::sync::atomic::Ordering::Relaxed);
1420            HeldReadAhead {
1421                state: Arc::clone(&self.state),
1422                bytes,
1423            }
1424        });
1425        IterGuardImpl::Blob(Guard {
1426            tree: self.tree.clone(),
1427            version: self.version.clone(),
1428            fill: self.state.fill,
1429            prefetched,
1430            _held: held,
1431            kv: item,
1432            // Only until the read-ahead is armed, and never with the window
1433            // disabled. The guards exist to report the FIRST resolution; once
1434            // that has happened (or can change nothing, because a zero window
1435            // ignores it), handing every guard a counted reference would be an
1436            // atomic bump per item of the scan for no effect, plus an atomic
1437            // store when the guard resolves.
1438            prefetch: if self.armed || self.state.window == 0 {
1439                None
1440            } else {
1441                Some(self.state.clone())
1442            },
1443        })
1444    }
1445}
1446
1447impl<I: Iterator<Item = crate::Result<InternalValue>>> Iterator for PrefetchScan<I> {
1448    type Item = IterGuardImpl;
1449
1450    fn next(&mut self) -> Option<Self::Item> {
1451        if self.buf.is_empty() {
1452            if !self.armed {
1453                self.armed = self.state.armed();
1454            }
1455            if self.armed {
1456                self.fill();
1457            }
1458        }
1459
1460        let item = match self.buf.pop_front() {
1461            Some(item) => item,
1462            None => self.inner.next()?,
1463        };
1464        Some(self.guard(item))
1465    }
1466}
1467
1468impl<I: DoubleEndedIterator<Item = crate::Result<InternalValue>>> DoubleEndedIterator
1469    for PrefetchScan<I>
1470{
1471    fn next_back(&mut self) -> Option<Self::Item> {
1472        // Latch the shared armed state here too: read-ahead stays forward-only
1473        // (no fill from this end), but once a reverse guard has resolved there
1474        // is nothing left for later guards to report, and without the latch
1475        // every one of them would carry a counted reference (an atomic bump on
1476        // emit, an atomic store on resolve) for no effect.
1477        if !self.armed {
1478            self.armed = self.state.armed();
1479        }
1480        // `buf` holds the EARLIEST remaining items (they were pulled off the
1481        // front), so the back of the scan is still inside `inner` until `inner`
1482        // runs out; only then does the buffer's own back become the last item.
1483        // Read-ahead stays a forward-only optimization: a reverse scan walks
1484        // `inner` directly and reads its values one at a time, as before.
1485        if !self.inner_back_done {
1486            if let Some(item) = self.inner.next_back() {
1487                return Some(self.guard(item));
1488            }
1489            self.inner_back_done = true;
1490        }
1491
1492        let item = self.buf.pop_back()?;
1493        Some(self.guard(item))
1494    }
1495}
1496
1497/// Wraps a [`SeekableTreeIter`](crate::range::SeekableTreeIter) over the index
1498/// tree so a KV-separated tree can expose it as a [`SeekableGuardIter`](crate::iter_guard::SeekableGuardIter).
1499struct BlobSeekable {
1500    inner: crate::range::SeekableTreeIter,
1501    tree: crate::BlobTree,
1502    version: Version,
1503    /// Whether its values are cached, as the tree said when it was opened.
1504    fill: crate::config::BlobCacheFill,
1505}
1506
1507impl Iterator for BlobSeekable {
1508    type Item = IterGuardImpl;
1509
1510    fn next(&mut self) -> Option<Self::Item> {
1511        self.inner
1512            .next()
1513            .map(|item| blob_guard(&self.tree, &self.version, self.fill, item))
1514    }
1515}
1516
1517impl DoubleEndedIterator for BlobSeekable {
1518    fn next_back(&mut self) -> Option<Self::Item> {
1519        self.inner
1520            .next_back()
1521            .map(|item| blob_guard(&self.tree, &self.version, self.fill, item))
1522    }
1523}
1524
1525impl crate::iter_guard::SeekableGuardIter for BlobSeekable {
1526    fn seek_to(&mut self, key: &[u8]) {
1527        self.inner.seek_to(key);
1528    }
1529
1530    fn seek_to_for_prev(&mut self, key: &[u8]) {
1531        self.inner.seek_to_for_prev(key);
1532    }
1533
1534    fn peek_key(&mut self) -> Option<crate::Result<crate::UserKey>> {
1535        // The key lives in the index tree (blob separation only moves the value),
1536        // so the inner seekable's peek already yields the right user key.
1537        self.inner.peek_key()
1538    }
1539}
1540
1541impl AbstractTree for BlobTree {
1542    #[cfg(feature = "std")]
1543    fn create_checkpoint(
1544        &self,
1545        target_path: &crate::path::Path,
1546    ) -> crate::Result<crate::CheckpointInfo> {
1547        crate::checkpoint::run_checkpoint(
1548            self,
1549            &crate::checkpoint::CheckpointParams {
1550                target_root: target_path,
1551                target_fs: &self.index.config.fs,
1552                src_root: &self.index.config.path,
1553                src_fs: &self.index.config.fs,
1554                deletion_pause: &self.index.deletion_pause,
1555                visible_seqno: &self.index.config.visible_seqno,
1556                include_blobs: true,
1557                runtime_config: self.index.0.runtime_config.load_full(),
1558                encryption: self.index.0.config.encryption.clone(),
1559                source: &self.index.0,
1560            },
1561        )
1562    }
1563
1564    #[cfg(feature = "std")]
1565    fn pin(&self) -> crate::PinnedVersion {
1566        crate::PinnedVersion::take(&self.index)
1567    }
1568
1569    #[cfg(feature = "std")]
1570    fn acknowledge_repair_report(&self, id: u128) -> crate::Result<bool> {
1571        self.index.acknowledge_repair_report(id)
1572    }
1573
1574    fn print_trace(&self, key: &[u8]) -> crate::Result<()> {
1575        self.index.print_trace(key)
1576    }
1577
1578    fn table_file_cache_size(&self) -> usize {
1579        self.index.table_file_cache_size()
1580    }
1581
1582    fn get_version_history_lock(&self) -> VersionsWriteGuard<'_> {
1583        self.index.get_version_history_lock()
1584    }
1585
1586    fn persist_flushes(&self) -> crate::Result<()> {
1587        self.index.persist_flushes()
1588    }
1589
1590    fn next_table_id(&self) -> TableId {
1591        self.index.next_table_id()
1592    }
1593
1594    fn id(&self) -> crate::TreeId {
1595        self.index.id()
1596    }
1597
1598    fn get_internal_entry(&self, key: &[u8], seqno: SeqNo) -> crate::Result<Option<InternalValue>> {
1599        self.index.get_internal_entry(key, seqno)
1600    }
1601
1602    fn current_version(&self) -> Version {
1603        self.index.current_version()
1604    }
1605
1606    #[cfg(feature = "std")]
1607    fn refresh_table_checksum(
1608        &self,
1609        table_id: crate::TableId,
1610        checksum: crate::checksum::Checksum,
1611        expected_restriction: Option<&crate::UserKey>,
1612    ) -> crate::Result<crate::abstract_tree::ChecksumRefreshOutcome> {
1613        // Tables live in the index tree's version; blob files carry no
1614        // manifest digest.
1615        self.index
1616            .refresh_table_checksum(table_id, checksum, expected_restriction)
1617    }
1618
1619    fn sync_mode(&self) -> crate::fs::SyncMode {
1620        // SSTs live in the index tree; its durability mode governs them.
1621        self.index.sync_mode()
1622    }
1623
1624    fn wait_for_table_file_refills(&self) {
1625        self.index.wait_for_table_file_refills();
1626    }
1627
1628    fn prefix_extractor(&self) -> Option<alloc::sync::Arc<dyn crate::prefix::PrefixExtractor>> {
1629        // The prefix filter is built over the index tree's SST keys.
1630        self.index.prefix_extractor()
1631    }
1632
1633    fn storage_stats(&self) -> crate::Result<crate::StorageStats> {
1634        // Forward the index tree's compaction state (the default impl would
1635        // always report idle), and mark value bytes as NOT user values: large
1636        // values are KV-separated into blob files, so the SST records only
1637        // indirection pointers. One version snapshot is reused for both the
1638        // footprint and the blob-headroom sum below: a second `current_version()`
1639        // could race a concurrent flush / compaction and mix two snapshots.
1640        let version = self.current_version();
1641        let mut stats = crate::storage_stats::compute_storage_stats(
1642            &version,
1643            self.index.is_compacting(),
1644            false,
1645        )?;
1646        // Capacity is disk-aware and driven by the index tree's runtime config;
1647        // its footprint basis (`compute_used_bytes`) already stats blob files, so
1648        // `used_bytes` here is the blob-inclusive figure capacity is measured
1649        // against.
1650        let (capacity, available, compaction_possible) =
1651            self.index.admission_capacity(stats.used_bytes);
1652        stats.capacity_bytes = capacity;
1653        stats.available_bytes = available;
1654        stats.compaction_possible = compaction_possible;
1655        // A full compaction of a blob tree also relocates STALE blob files, which
1656        // the merge gate budgets via `pick_blob_files_to_rewrite` (linked, stale,
1657        // non-dead). Estimate the same subset across every table — NOT the whole
1658        // live blob footprint, which would overstate the need (large non-stale
1659        // blobs are not rewritten) and report tight while the gate would admit
1660        // the merge. Fold it into the gauge figure.
1661        let blob_need = self.full_compaction_blob_need(&version)?;
1662        stats.full_compaction_bytes += blob_need;
1663        // Surface full-vs-tight compaction availability through the SAME two-layer
1664        // check the compaction space gate enforces (logical quota + physical free
1665        // per destination volume), so the status matches what the gate admits.
1666        if self.index.storage_admission_enabled()
1667            && capacity.is_some()
1668            && stats.status == crate::StorageStatus::Healthy
1669        {
1670            // SST output lands in the LAST configured level's volume
1671            // (`level_count - 1`), stale blob relocation in the primary blobs
1672            // volume; the demand is bounded by the largest level's size. The
1673            // per-volume gate (not `available >= full_compaction_bytes` against
1674            // the min-volume free) keeps the status from reporting tight when the
1675            // SST and blob outputs each fit their own volume — see the gate's
1676            // two-layer model.
1677            let sst_need = crate::storage_stats::full_compaction_demand_bytes(&version)?;
1678            // `saturating_sub`: `level_count >= 1` always (the clamp only guards a
1679            // degenerate zero-level config) → the last level index.
1680            let sst_dest_level = self.index.config.level_count.saturating_sub(1);
1681            let quota_headroom = self.index.quota_headroom(stats.used_bytes);
1682            let full_fits = crate::compaction::worker::merge_would_fit(
1683                &self.index.compaction_capacity,
1684                self.index.id,
1685                &self.index.config,
1686                quota_headroom,
1687                (sst_need, blob_need),
1688                sst_dest_level,
1689            );
1690            stats.status = if full_fits {
1691                crate::StorageStatus::FullCompactionAvailable
1692            } else {
1693                crate::StorageStatus::TightCompactionAvailable
1694            };
1695        }
1696        // Admission is driven by the index tree's runtime config / footprint;
1697        // a closed gate is the operator-actionable state (see the standard
1698        // tree's override for the precedence rationale).
1699        if self.index.is_read_only() {
1700            stats.status = crate::StorageStatus::ReadOnlyOutOfSpace;
1701        }
1702        Ok(stats)
1703    }
1704
1705    fn write_admission(&self) -> crate::Result<()> {
1706        // Admission state lives on the index tree (which holds the runtime
1707        // config). The forward is blob-aware: the index tree's version IS this
1708        // blob tree's version (current_version() delegates here), and the gate's
1709        // footprint basis (`storage_stats::compute_used_bytes`) stats live
1710        // tables AND blob files, so blob bytes count toward the budget.
1711        self.index.write_admission()
1712    }
1713
1714    fn write_backpressure(
1715        &self,
1716        strategy: &dyn crate::compaction::CompactionStrategy,
1717    ) -> crate::Backpressure {
1718        // Same delegation as write_admission: the index tree holds the runtime
1719        // config and its version is this blob tree's version, so the L0 run
1720        // count and pending-compaction-bytes signals are computed on the right
1721        // state.
1722        self.index.write_backpressure(strategy)
1723    }
1724
1725    #[cfg(feature = "metrics")]
1726    fn cache_stats(&self) -> crate::CacheStats {
1727        self.index.cache_stats()
1728    }
1729
1730    #[cfg(feature = "metrics")]
1731    fn metrics(&self) -> &Arc<crate::Metrics> {
1732        self.index.metrics()
1733    }
1734
1735    fn prefix<K: AsRef<[u8]>>(
1736        &self,
1737        prefix: K,
1738        seqno: SeqNo,
1739        index: Option<(Arc<Memtable>, SeqNo)>,
1740    ) -> Box<dyn DoubleEndedIterator<Item = IterGuardImpl> + Send + 'static> {
1741        use crate::prefix::compute_prefix_hash;
1742        use crate::range::prefix_to_range;
1743
1744        let prefix_bytes = prefix.as_ref();
1745
1746        let prefix_hash =
1747            compute_prefix_hash(self.index.config.prefix_extractor.as_ref(), prefix_bytes);
1748
1749        // Resolved once, and the scan keeps the version alive in the guard it
1750        // returns: an indirection this iterator yields is resolved against the
1751        // blob files of the same version that held the pointer, so a blob GC
1752        // running concurrently cannot reclaim the file the pointer names.
1753        let super_version = match self.index.get_version_for_snapshot(seqno) {
1754            Ok(super_version) => super_version,
1755            Err(e) => return Box::new(core::iter::once(crate::tree::error_guard(e))),
1756        };
1757        let tree = self.clone();
1758        // One source for the scan: its read-ahead and its merges cache as the
1759        // tree said when it began.
1760        let source = self.blob_source();
1761        let state = self.scan_prefetch_state(source.fill);
1762
1763        let range = prefix_to_range(prefix_bytes);
1764
1765        Box::new(PrefetchScan {
1766            inner: crate::Tree::create_internal_range_with_prefix_hash(
1767                super_version.clone(),
1768                &range,
1769                seqno,
1770                index,
1771                self.index.config.merge_operator.clone(),
1772                self.index.config.comparator.clone(),
1773                prefix_hash,
1774                Some(source),
1775            ),
1776            buf: alloc::collections::VecDeque::new(),
1777            inner_back_done: false,
1778            armed: false,
1779            state,
1780            version: super_version.version,
1781            tree,
1782            local: crate::vlog::Prefetched::default(),
1783        })
1784    }
1785
1786    fn range<K: AsRef<[u8]>, R: RangeBounds<K>>(
1787        &self,
1788        range: R,
1789        seqno: SeqNo,
1790        index: Option<(Arc<Memtable>, SeqNo)>,
1791    ) -> Box<dyn DoubleEndedIterator<Item = IterGuardImpl> + Send + 'static> {
1792        // One version for the traversal and for resolving the pointers it
1793        // yields, as in `prefix` above; the range bounds only narrow which of
1794        // that version's tables are opened.
1795        let super_version = match self.index.get_version_for_snapshot(seqno) {
1796            Ok(super_version) => super_version,
1797            Err(e) => return Box::new(core::iter::once(crate::tree::error_guard(e))),
1798        };
1799        let tree = self.clone();
1800        // One source for the scan, as in `prefix` above.
1801        let source = self.blob_source();
1802        let state = self.scan_prefetch_state(source.fill);
1803
1804        Box::new(PrefetchScan {
1805            inner: crate::Tree::create_internal_range_with_prefix_hash(
1806                super_version.clone(),
1807                &range,
1808                seqno,
1809                index,
1810                self.index.config.merge_operator.clone(),
1811                self.index.config.comparator.clone(),
1812                None,
1813                Some(source),
1814            ),
1815            buf: alloc::collections::VecDeque::new(),
1816            inner_back_done: false,
1817            armed: false,
1818            state,
1819            version: super_version.version,
1820            tree,
1821            local: crate::vlog::Prefetched::default(),
1822        })
1823    }
1824
1825    fn range_seekable<K: AsRef<[u8]>, R: RangeBounds<K>>(
1826        &self,
1827        range: R,
1828        seqno: SeqNo,
1829        index: Option<(Arc<Memtable>, SeqNo)>,
1830    ) -> Box<dyn crate::iter_guard::SeekableGuardIter + 'static> {
1831        let (lo, hi) = crate::tree::range_to_user_bounds(&range);
1832        // One source for the iterator, its merges and its values alike.
1833        let source = self.blob_source();
1834        let fill = source.fill;
1835        let inner =
1836            match self
1837                .index
1838                .create_seekable_range_bounds_with(lo, hi, seqno, index, Some(source))
1839            {
1840                Ok(inner) => inner,
1841                Err(e) => return Box::new(crate::iter_guard::FailedSeekable::new(e)),
1842            };
1843        let version = inner.version();
1844        Box::new(BlobSeekable {
1845            inner,
1846            tree: self.clone(),
1847            version,
1848            fill,
1849        })
1850    }
1851
1852    fn batch_range_scan<K: AsRef<[u8]>, R: RangeBounds<K> + 'static, I: IntoIterator<Item = R>>(
1853        &self,
1854        intervals: I,
1855        seqno: SeqNo,
1856        index: Option<(Arc<Memtable>, SeqNo)>,
1857    ) -> Box<dyn Iterator<Item = IterGuardImpl> + Send + 'static>
1858    where
1859        I::IntoIter: Send + 'static,
1860    {
1861        // One source for the scan, as in `prefix` above.
1862        let source = self.blob_source();
1863        let state = self.scan_prefetch_state(source.fill);
1864        let inner = match self.index.create_seekable_range_bounds_with(
1865            core::ops::Bound::Unbounded,
1866            core::ops::Bound::Unbounded,
1867            seqno,
1868            index,
1869            Some(source),
1870        ) {
1871            Ok(inner) => inner,
1872            Err(e) => return Box::new(core::iter::once(crate::tree::error_guard(e))),
1873        };
1874        let version = inner.version();
1875        let tree = self.clone();
1876        let intervals = intervals
1877            .into_iter()
1878            .map(|r| crate::tree::range_to_user_bounds(&r));
1879        Box::new(PrefetchScan {
1880            inner: crate::range::BatchRangeScan::new(inner, intervals),
1881            buf: alloc::collections::VecDeque::new(),
1882            inner_back_done: false,
1883            armed: false,
1884            state,
1885            version,
1886            tree,
1887            local: crate::vlog::Prefetched::default(),
1888        })
1889    }
1890
1891    fn tombstone_count(&self) -> u64 {
1892        self.index.tombstone_count()
1893    }
1894
1895    fn weak_tombstone_count(&self) -> u64 {
1896        self.index.weak_tombstone_count()
1897    }
1898
1899    fn weak_tombstone_reclaimable_count(&self) -> u64 {
1900        self.index.weak_tombstone_reclaimable_count()
1901    }
1902
1903    fn clear(&self) -> crate::Result<()> {
1904        let config = self.tree_config();
1905        let mut versions = self.get_version_history_lock();
1906
1907        // Pre-clear snapshot: its tables AND blob files all become garbage once
1908        // the new empty version is installed.
1909        let prior = versions.latest_version();
1910
1911        // The blob dictionary counts here as much as the table one: the next
1912        // blob file is compressed against it.
1913        #[cfg(zstd_any)]
1914        let still_written = self.index.write_referenced_dict_ids();
1915        #[cfg(not(zstd_any))]
1916        let still_written: Vec<crate::file::DictId> = Vec::new();
1917
1918        versions.upgrade_version(
1919            &config.path,
1920            |v| {
1921                let mut copy = v.clone();
1922                copy.active_memtable = Arc::new(Memtable::new(
1923                    self.index.memtable_id_counter.next(),
1924                    config.comparator.clone(),
1925                ));
1926                copy.sealed_memtables = Arc::default();
1927                copy.version = v.version.cleared(&still_written);
1928                Ok(copy)
1929            },
1930            &config.seqno,
1931            &config.visible_seqno,
1932            &*config.fs,
1933            self.index.0.runtime_config.load_full(),
1934            self.index.0.config.encryption.clone(),
1935            // Every table and blob file goes: no snapshot up to this install
1936            // is servable.
1937            crate::version::RetentionEffect::DropsData,
1938        )?;
1939
1940        // Same MVCC-safe reclaim as the standard tree, plus the blob files:
1941        // mark every obsolete table / blob file deleted and let Inner::Drop
1942        // reclaim each once its last reference is released (a live reader's
1943        // snapshot clone defers deletion).
1944        drop(versions);
1945
1946        for table in prior.version.iter_tables() {
1947            table.mark_as_deleted();
1948        }
1949        for blob_file in prior.version.blob_files.iter() {
1950            blob_file.mark_as_deleted();
1951        }
1952
1953        Ok(())
1954    }
1955
1956    fn major_compact(
1957        &self,
1958        target_size: u64,
1959        gc_watermark: SeqNo,
1960    ) -> crate::Result<crate::compaction::CompactionResult> {
1961        self.index.major_compact(target_size, gc_watermark)
1962    }
1963
1964    fn clear_active_memtable(&self) {
1965        self.index.clear_active_memtable();
1966    }
1967
1968    fn l0_run_count(&self) -> usize {
1969        self.index.l0_run_count()
1970    }
1971
1972    fn blob_file_count(&self) -> usize {
1973        self.current_version().blob_file_count()
1974    }
1975
1976    // NOTE: We skip reading from the value log
1977    // because the vHandles already store the value size
1978    fn size_of<K: AsRef<[u8]>>(&self, key: K, seqno: SeqNo) -> crate::Result<Option<u32>> {
1979        let key = key.as_ref();
1980        let super_version = self.index.snapshot_for_read(seqno)?;
1981        super_version.version.read_mix().note_points(1);
1982        let Some(item) = crate::Tree::get_internal_entry_from_version(
1983            &super_version,
1984            key,
1985            seqno,
1986            self.index.config.comparator.as_ref(),
1987        )?
1988        else {
1989            return Ok(None);
1990        };
1991
1992        // An operand's size is not the key's: the value is the operands
1993        // merged onto the base, which only the merge itself knows. Merged from
1994        // the same read, which counted this lookup already.
1995        if item.key.value_type.is_merge_operand()
1996            && let Some(merge_operator) = &self.index.config.merge_operator
1997        {
1998            #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")]
1999            return Ok(crate::Tree::resolve_point_merge(
2000                &super_version,
2001                key,
2002                seqno,
2003                merge_operator,
2004                Some(&self.blob_source()),
2005            )?
2006            .map(|value| value.len() as u32));
2007        }
2008
2009        Ok(Some(if item.key.value_type.is_indirection() {
2010            let mut cursor = crate::io::Cursor::new(item.value);
2011            let vptr = BlobIndirection::decode_from(&mut cursor)?;
2012            vptr.size
2013        } else if item.key.value_type.is_cell_row() {
2014            field_row::logical_len(&item.value)?
2015        } else {
2016            #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")]
2017            {
2018                item.value.len() as u32
2019            }
2020        }))
2021    }
2022
2023    fn stale_blob_bytes(&self) -> u64 {
2024        self.current_version().gc_stats().stale_bytes()
2025    }
2026
2027    fn filter_size(&self) -> u64 {
2028        self.index.filter_size()
2029    }
2030
2031    fn pinned_filter_size(&self) -> usize {
2032        self.index.pinned_filter_size()
2033    }
2034
2035    fn filter_memory(&self) -> crate::FilterMemory {
2036        self.index.filter_memory()
2037    }
2038
2039    fn pinned_block_index_size(&self) -> usize {
2040        self.index.pinned_block_index_size()
2041    }
2042
2043    fn sealed_memtable_count(&self) -> usize {
2044        self.index.sealed_memtable_count()
2045    }
2046
2047    fn get_flush_lock(&self) -> FlushGuard<'_> {
2048        self.index.get_flush_lock()
2049    }
2050
2051    #[expect(clippy::too_many_lines, reason = "flush logic is inherently complex")]
2052    fn flush_to_tables_with_rt(
2053        &self,
2054        stream: impl Iterator<Item = crate::Result<InternalValue>>,
2055        range_tombstones: Vec<crate::range_tombstone::RangeTombstone>,
2056        keys: u64,
2057        hashes: u64,
2058        overwritten: Option<&overwritten::OverwrittenKeys>,
2059    ) -> crate::Result<
2060        Option<(
2061            Vec<Table>,
2062            Option<Vec<BlobFile>>,
2063            crate::runtime_config::WritePin,
2064        )>,
2065    > {
2066        use crate::{coding::Encode, file::BLOBS_FOLDER, table::multi_writer::MultiWriter};
2067
2068        let start = crate::time::Instant::now();
2069
2070        let (table_folder, level_fs) = self.index.config.tables_folder_for_level(0);
2071
2072        let data_block_size = self.index.config.data_block_size_policy.get(0);
2073
2074        let data_block_restart_interval =
2075            self.index.config.data_block_restart_interval_policy.get(0);
2076        let index_block_restart_interval =
2077            self.index.config.index_block_restart_interval_policy.get(0);
2078
2079        // Live runtime snapshot, one for the whole flush: the compression
2080        // policies (tables and blob files alike) and the per-flush SST options
2081        // below all come from it, so a concurrent `update_runtime_config`
2082        // cannot give one flush two configurations.
2083        let rc = self.index.0.runtime_config.load_full();
2084
2085        let data_block_compression = rc.data_block_compression_policy.get(0);
2086        let index_block_compression = rc.index_block_compression_policy.get(0);
2087
2088        let data_block_hash_ratio = self.index.config.data_block_hash_ratio_policy.get(0);
2089
2090        let index_partitioning = self.index.config.index_block_partitioning_policy.get(0);
2091        let filter_partitioning = self.index.config.filter_block_partitioning_policy.get(0);
2092
2093        log::debug!(
2094            "Flushing memtable(s) and performing key-value separation, 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:?}"
2095        );
2096        log::debug!("=> to table(s) in {}", table_folder.display());
2097        log::debug!("=> to blob file(s) at {}", self.blobs_folder.display());
2098
2099        let table_writer = MultiWriter::from_pool(
2100            table_folder.clone(),
2101            Arc::clone(&self.index.table_files),
2102            None,
2103            self.index.config.table_target_size,
2104            0,
2105            level_fs.clone(),
2106            crate::fs::WriteOrigin::FlushTable,
2107        )?;
2108        // The flush's L0 recency, taken as the standard tree's flush takes
2109        // it: a pooled file's id says nothing about when it was written, a
2110        // file made now carries an id taken now.
2111        let recency = table_writer
2112            .first_made_now()
2113            .unwrap_or_else(|| self.index.table_id_counter.next());
2114        let mut table_writer = table_writer
2115            .set_comparator(self.index.config.comparator.clone())
2116            .use_recency(Some(recency))
2117            .use_data_block_restart_interval(data_block_restart_interval)
2118            .use_index_block_restart_interval(index_block_restart_interval)
2119            .use_data_block_compression(data_block_compression)
2120            .use_index_block_compression(index_block_compression)
2121            .use_data_block_size(data_block_size)
2122            .use_index_partition_size(self.index.config.index_block_partition_size_policy.get(0))
2123            .use_filter_partition_size(self.index.config.filter_block_partition_size_policy.get(0))
2124            .use_row_group_size(self.index.config.columnar_row_group_size_policy.get(0))
2125            .use_columnar_page_size(self.index.config.columnar_page_size_policy.get(0))
2126            .use_column_encoding(self.index.config.column_encoding_policy.get(0))
2127            .use_data_block_hash_ratio(data_block_hash_ratio);
2128
2129        let bloom_policy = {
2130            use crate::config::FilterPolicyEntry::{Bloom, None};
2131            use crate::table::filter::BloomConstructionPolicy;
2132
2133            match self.index.config.filter_policy.get(0) {
2134                Bloom(policy) => policy,
2135                None => BloomConstructionPolicy::BitsPerKey(0.0),
2136            }
2137        };
2138        table_writer = table_writer.use_bloom_policy(bloom_policy);
2139        let filter_sizing = self.index.new_data_filter_sizing(
2140            0,
2141            bloom_policy,
2142            crate::filter_budget::FilterCount { keys, hashes },
2143            rc.ecc_scheme,
2144        );
2145        table_writer = table_writer.use_filter_sizing(filter_sizing.clone());
2146
2147        if index_partitioning {
2148            // Size-adaptive index: single-level for small SSTs, spill to
2149            // partitioned only past the threshold (see flush path).
2150            table_writer = table_writer.use_adaptive_index(rc.index_partition_spill_threshold);
2151        }
2152        if filter_partitioning {
2153            table_writer = table_writer.use_partitioned_filter();
2154        }
2155
2156        table_writer =
2157            table_writer.use_prefix_extractor(self.index.config.prefix_extractor.clone());
2158        table_writer = table_writer.use_key_domains(self.index.config.key_domains.clone());
2159        table_writer = table_writer.use_encryption(self.index.config.encryption.clone());
2160        table_writer = table_writer.use_clock(self.index.config.clock.clone());
2161        // Runtime-driven SST options, mirroring the standard tree's flush so a
2162        // blob tree's index honors columnar, zone maps, ECC, seqno-in-index,
2163        // per-KV checksums, locator policy, sync mode, and CoW disable.
2164        table_writer = table_writer.use_page_ecc(self.index.config.page_ecc, rc.ecc_scheme);
2165        table_writer = table_writer
2166            .use_sync_mode(self.index.config.sync_mode)
2167            .use_durability(self.index.config.flush_durability)
2168            .use_writeback_bytes(self.index.config.writeback_bytes);
2169        table_writer = table_writer.use_seqno_in_index(rc.seqno_in_index);
2170        table_writer = table_writer.use_zone_map(rc.zone_map);
2171        table_writer = table_writer
2172            .use_columnar(rc.columnar)
2173            .use_cell_rows(rc.columnar);
2174        table_writer = table_writer.use_disable_cow_on_sst(rc.disable_cow_on_sst_files);
2175        table_writer = table_writer.use_kv_checksums(rc.kv_checksums, rc.kv_checksum_algo);
2176        table_writer = table_writer.use_locator(self.index.config.locator_policy.get(0));
2177
2178        // Resolved from `rc`, which the flush holds until the install, as in
2179        // the standard flush; one snapshot covers the tables and the blob files.
2180        #[cfg(zstd_any)]
2181        let dicts = self.index.config.current_zstd_dictionaries();
2182        #[cfg(zstd_any)]
2183        {
2184            table_writer = table_writer
2185                .use_zstd_dictionary(dicts.for_compression(data_block_compression)?)
2186                .use_zstd_two_pass_seed(rc.zstd_two_pass_seed);
2187        }
2188        let write_pin = crate::runtime_config::WritePin::new(&rc).with_filter_sizing(filter_sizing);
2189
2190        // Parallel block compression for the flush writer, mirroring the
2191        // standard tree's flush: engaged only when the per-block transform does
2192        // real CPU work (codec / encryption / page ECC). See the standard
2193        // flush for why this is safe wherever the host runs the flush (the
2194        // pipeline's help-first drain, see `parallel_compressor`).
2195        #[cfg(feature = "std")]
2196        {
2197            let transform_does_work = data_block_compression != crate::CompressionType::None
2198                || self.index.config.encryption.is_some()
2199                || self.index.config.page_ecc;
2200            if transform_does_work {
2201                table_writer =
2202                    table_writer.use_parallel_compression(self.index.config.parallel_compression());
2203            }
2204        }
2205
2206        #[expect(
2207            clippy::expect_used,
2208            reason = "cannot create blob tree without defining kv separation options"
2209        )]
2210        let kv_opts = self
2211            .index
2212            .config
2213            .kv_separation_opts
2214            .as_ref()
2215            .expect("kv separation options should exist");
2216
2217        let mut blob_writer = {
2218            let w = BlobFileWriter::new_for(
2219                self.index.0.blob_file_id_counter.clone(),
2220                self.index.config.path.join(BLOBS_FOLDER),
2221                self.id(),
2222                self.index.config.descriptor_table.clone(),
2223                self.index.config.fs.clone(),
2224                crate::fs::WriteOrigin::FlushBlob,
2225            )?
2226            .use_target_size(kv_opts.file_target_size)
2227            .use_compression(rc.blob_compression)
2228            .use_sync_mode(self.index.config.sync_mode)
2229            .use_durability(self.index.config.flush_durability)
2230            .use_writeback_bytes(self.index.config.writeback_bytes)
2231            .use_lifetime_groups(kv_opts.lifetime_groups)
2232            .use_clock(self.index.config.clock.clone())
2233            .use_key_domains(
2234                self.index.config.key_domains.clone(),
2235                self.index.config.comparator.clone(),
2236            );
2237            #[cfg(zstd_any)]
2238            let w = w
2239                .use_zstd_dictionary(dicts.for_compression(rc.blob_compression)?)
2240                .use_zstd_two_pass_seed(rc.zstd_two_pass_seed)
2241                .use_zstd_dictionaries(dicts);
2242            w
2243        };
2244
2245        let separation_threshold = kv_opts.separation_threshold;
2246        let lifetime = kv_opts.lifetime_classifier();
2247        let comparator = &self.index.config.comparator;
2248
2249        // Set range tombstones BEFORE writing KV items so that if MultiWriter
2250        // rotates to a new table during the write loop, earlier tables already
2251        // carry the RT metadata.
2252        table_writer.set_range_tombstones(range_tombstones);
2253
2254        // One buffer for every indirection of the flush.
2255        let mut encoded = Vec::new();
2256
2257        // A key domain picks the dictionary of the values under it, as the
2258        // writer enters it, so those stay on the writer's thread.
2259        #[cfg(all(feature = "std", zstd_any))]
2260        let mut ahead = if self.index.config.key_domains.is_none() {
2261            ValueEncoder::for_flush(
2262                self.index.config.parallel_compression(),
2263                rc.blob_compression,
2264                rc.zstd_two_pass_seed,
2265            )
2266        } else {
2267            None
2268        };
2269        #[cfg(not(all(feature = "std", zstd_any)))]
2270        let mut ahead: Option<ValueEncoder> = None;
2271
2272        for item in stream {
2273            let item = item?;
2274
2275            // Asked for every key, separated or not, so the queue forgets each
2276            // key once the flush has passed it.
2277            let overwritten = overwritten
2278                .is_some_and(|keys| keys.take_before_and_check(&item.key.user_key, comparator));
2279
2280            if let Some(ahead) = &mut ahead {
2281                #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")]
2282                let size = item.value.len() as u32;
2283                if item.key.value_type == crate::ValueType::Value && size >= separation_threshold {
2284                    if ahead.is_full() {
2285                        write_encoded_value(
2286                            ahead,
2287                            &mut blob_writer,
2288                            &mut table_writer,
2289                            &mut encoded,
2290                        )?;
2291                    }
2292                    let class = lifetime.class_of(&item.key.user_key, overwritten);
2293                    let separated = Separated {
2294                        key: item.key,
2295                        size,
2296                        class,
2297                    };
2298                    ahead.submit(separated, item.value);
2299                    continue;
2300                }
2301                // Anything else follows the values before it into the table.
2302                while write_encoded_value(ahead, &mut blob_writer, &mut table_writer, &mut encoded)?
2303                {
2304                }
2305            }
2306
2307            if item.is_tombstone() {
2308                // NOTE: Still need to add tombstone to index tree
2309                // But no blob to blob writer
2310                table_writer.write(InternalValue::new(item.key, UserValue::empty()))?;
2311                continue;
2312            }
2313
2314            let value = item.value;
2315
2316            if item.key.value_type.is_cell_row() {
2317                let class = lifetime.class_of(&item.key.user_key, overwritten);
2318                // Each cell is weighed on its own: the large ones go to the
2319                // blob file and the row keeps owning references to them.
2320                let row = field_row::separate_row(
2321                    &value,
2322                    |column| kv_opts.cell_threshold(column),
2323                    |bytes| {
2324                        blob_writer.select_lifetime_class(class)?;
2325                        blob_writer.write(&item.key.user_key, item.key.seqno, bytes)
2326                    },
2327                )?
2328                .map_or(value, UserValue::from);
2329                table_writer.write(InternalValue::new(item.key, row.clone()))?;
2330                table_writer.register_cell_row(&row)?;
2331                continue;
2332            }
2333
2334            #[expect(clippy::cast_possible_truncation, reason = "values are u32 length max")]
2335            let value_size = value.len() as u32;
2336
2337            // Only a value is separated: an indirection reads as a put, so an
2338            // operand stored as one would stop merging onto its base.
2339            if item.key.value_type == crate::ValueType::Value && value_size >= separation_threshold
2340            {
2341                blob_writer
2342                    .select_lifetime_class(lifetime.class_of(&item.key.user_key, overwritten))?;
2343                let vhandle = blob_writer.write(&item.key.user_key, item.key.seqno, &value)?;
2344
2345                let indirection = BlobIndirection {
2346                    vhandle,
2347                    size: value_size,
2348                };
2349
2350                encoded.clear();
2351                indirection.encode_into(&mut encoded)?;
2352                table_writer.write({
2353                    let mut vptr = InternalValue::new(item.key, UserValue::new(&encoded));
2354                    vptr.key.value_type = crate::ValueType::Indirection;
2355                    vptr
2356                })?;
2357
2358                table_writer.register_blob(indirection);
2359            } else {
2360                table_writer.write(InternalValue::new(item.key, value))?;
2361            }
2362        }
2363
2364        if let Some(ahead) = &mut ahead {
2365            while write_encoded_value(ahead, &mut blob_writer, &mut table_writer, &mut encoded)? {}
2366        }
2367
2368        let (blob_files, written) = finish_flush(
2369            blob_writer,
2370            table_writer,
2371            self.index.config.flush_durability,
2372        );
2373        let blob_files = blob_files?;
2374        let written = written?;
2375        let entries_durable = written.entries_durable;
2376
2377        log::debug!("Flushed memtable(s) in {:?}", start.elapsed());
2378
2379        let pin_filter = self.index.config.filter_block_pinning_policy.get(0);
2380        let pin_index = self.index.config.index_block_pinning_policy.get(0);
2381
2382        // Load tables
2383        let tables = written
2384            .tables
2385            .into_iter()
2386            .map(|(table_id, checksum)| -> crate::Result<Table> {
2387                let mut params = crate::table::RecoverParams::new(
2388                    table_folder.join(table_id.to_string()),
2389                    checksum,
2390                    table_id,
2391                    level_fs.clone(),
2392                    self.index.config.comparator.clone(),
2393                    self.index.config.cache.clone(),
2394                );
2395                params.tree_id = self.index.id;
2396                params
2397                    .descriptor_table
2398                    .clone_from(&self.index.config.descriptor_table);
2399                params.pin_filter = pin_filter;
2400                params.pin_index = pin_index;
2401                params.encryption.clone_from(&self.index.config.encryption);
2402                params
2403                    .prefix_extractor
2404                    .clone_from(&self.index.config.prefix_extractor);
2405                #[cfg(zstd_any)]
2406                {
2407                    params.zstd_dictionaries = self.index.config.current_zstd_dictionaries();
2408                }
2409                #[cfg(feature = "metrics")]
2410                {
2411                    params.metrics = self.index.metrics.clone();
2412                }
2413                let table = Table::recover(params)?;
2414                if entries_durable {
2415                    table.mark_entry_durable();
2416                }
2417                Ok(table)
2418            })
2419            .collect::<crate::Result<Vec<_>>>()?;
2420
2421        // Return Some even when tables is empty (RT-only flush): the caller
2422        // (AbstractTree::flush) handles empty tables by re-inserting RTs into
2423        // the active memtable and still needs to delete sealed memtables.
2424        Ok(Some((tables, Some(blob_files), write_pin)))
2425    }
2426
2427    fn register_tables(
2428        &self,
2429        tables: &[Table],
2430        blob_files: Option<&[BlobFile]>,
2431        frag_map: Option<FragmentationMap>,
2432        sealed_memtables_to_delete: &[MemtableId],
2433        gc_watermark: SeqNo,
2434        collected_below_watermark: bool,
2435    ) -> crate::Result<()> {
2436        self.index.register_tables(
2437            tables,
2438            blob_files,
2439            frag_map,
2440            sealed_memtables_to_delete,
2441            gc_watermark,
2442            collected_below_watermark,
2443        )
2444    }
2445
2446    fn compact(
2447        &self,
2448        strategy: Arc<dyn crate::compaction::CompactionStrategy>,
2449        gc_watermark: SeqNo,
2450    ) -> crate::Result<crate::compaction::CompactionResult> {
2451        self.index.compact(strategy, gc_watermark)
2452    }
2453
2454    fn get_next_table_id(&self) -> TableId {
2455        self.index.get_next_table_id()
2456    }
2457
2458    fn tree_config(&self) -> &Config {
2459        &self.index.config
2460    }
2461
2462    fn get_highest_seqno(&self) -> Option<SeqNo> {
2463        self.index.get_highest_seqno()
2464    }
2465
2466    fn active_memtable(&self) -> Arc<Memtable> {
2467        self.index.active_memtable()
2468    }
2469
2470    fn rotate_memtable(&self) -> Option<Arc<Memtable>> {
2471        self.index.rotate_memtable()
2472    }
2473
2474    fn table_count(&self) -> usize {
2475        self.index.table_count()
2476    }
2477
2478    fn level_table_count(&self, idx: usize) -> Option<usize> {
2479        self.index.level_table_count(idx)
2480    }
2481
2482    fn approximate_len(&self) -> usize {
2483        self.index.approximate_len()
2484    }
2485
2486    // NOTE: Override the default implementation to not fetch
2487    // data from the value log, so we get much faster key reads
2488    fn is_empty(&self, seqno: SeqNo, index: Option<(Arc<Memtable>, SeqNo)>) -> crate::Result<bool> {
2489        self.index.is_empty(seqno, index)
2490    }
2491
2492    // NOTE: Override the default implementation to not fetch
2493    // data from the value log, so we get much faster key reads
2494    fn contains_key<K: AsRef<[u8]>>(&self, key: K, seqno: SeqNo) -> crate::Result<bool> {
2495        self.index.contains_key(key, seqno)
2496    }
2497
2498    // NOTE: Override the default implementation to delegate directly
2499    // to the index tree, avoiding extra iterator/guard overhead for
2500    // prefix checks
2501    fn contains_prefix<K: AsRef<[u8]>>(
2502        &self,
2503        prefix: K,
2504        seqno: SeqNo,
2505        index: Option<(Arc<Memtable>, SeqNo)>,
2506    ) -> crate::Result<bool> {
2507        self.index.contains_prefix(prefix, seqno, index)
2508    }
2509
2510    // NOTE: Override the default implementation to not fetch
2511    // data from the value log, so we get much faster scans
2512    fn len(&self, seqno: SeqNo, index: Option<(Arc<Memtable>, SeqNo)>) -> crate::Result<usize> {
2513        self.index.len(seqno, index)
2514    }
2515
2516    fn disk_space(&self) -> u64 {
2517        let version = self.current_version();
2518        self.index.disk_space() + version.blob_files.on_disk_size()
2519    }
2520
2521    fn approximate_range_stats<K: AsRef<[u8]>, R: core::ops::RangeBounds<K>>(
2522        &self,
2523        range: R,
2524        seqno: SeqNo,
2525    ) -> crate::Result<crate::ApproximateRangeStats> {
2526        // The index tree's SSTs record their referenced blob bytes per-SST (at
2527        // both flush and compaction), so its estimate already apportions the
2528        // blob bytes by the in-range fraction. Delegating keeps the
2529        // KV-separated estimate unified with the standard path.
2530        //
2531        // The figure tracks the value's CURRENT physical location: an unflushed
2532        // value still lives inline in the index memtable, so the memtable share
2533        // counts its full size there; once flushed it is separated into a blob
2534        // file and counted via the SST's referenced-blob bytes. Both reflect the
2535        // real footprint at the requested snapshot.
2536        self.index.approximate_range_stats(range, seqno)
2537    }
2538
2539    fn approximate_range_cardinality<K: AsRef<[u8]>, R: core::ops::RangeBounds<K>>(
2540        &self,
2541        range: R,
2542        seqno: SeqNo,
2543    ) -> crate::Result<crate::RangeCardinality> {
2544        // Cardinality counts rows (index entries) and their key ranges, both of
2545        // which live entirely in the index tree; blob files hold only the
2546        // separated values, so the index tree's zone map and counts are authoritative.
2547        self.index.approximate_range_cardinality(range, seqno)
2548    }
2549
2550    fn get_highest_memtable_seqno(&self) -> Option<SeqNo> {
2551        self.index.get_highest_memtable_seqno()
2552    }
2553
2554    fn get_highest_persisted_seqno(&self) -> Option<SeqNo> {
2555        self.index.get_highest_persisted_seqno()
2556    }
2557
2558    fn oldest_retained_seqno(&self) -> SeqNo {
2559        // Snapshots are resolved through the index tree's history.
2560        self.index.oldest_retained_seqno()
2561    }
2562
2563    fn retention_floor(&self) -> SeqNo {
2564        self.index.retention_floor()
2565    }
2566
2567    fn lower_retention_floor(&self, floor: SeqNo) -> crate::Result<()> {
2568        // The blob files share the index tree's version, and its floor.
2569        self.index.lower_retention_floor(floor)
2570    }
2571
2572    fn compaction_rate_limiter(&self) -> alloc::sync::Arc<crate::rate_limiter::RateLimiter> {
2573        // Compactions run through the index tree, so its limiter is the one.
2574        self.index.compaction_rate_limiter()
2575    }
2576
2577    fn write_rate_limiter(&self) -> alloc::sync::Arc<crate::rate_limiter::RateLimiter> {
2578        // Writes land in the index tree's memtables, so its limiter is the one.
2579        self.index.write_rate_limiter()
2580    }
2581
2582    fn apply_batch(&self, batch: crate::WriteBatch, seqno: SeqNo) -> crate::Result<(u64, u64)> {
2583        self.index.apply_batch(batch, seqno)
2584    }
2585
2586    fn insert<K: Into<UserKey>, V: Into<UserValue>>(
2587        &self,
2588        key: K,
2589        value: V,
2590        seqno: SeqNo,
2591    ) -> (u64, u64) {
2592        self.index.insert(key, value.into(), seqno)
2593    }
2594
2595    fn get<K: AsRef<[u8]>>(&self, key: K, seqno: SeqNo) -> crate::Result<Option<crate::UserValue>> {
2596        // The index entry and the blob it points at are read from the same
2597        // version: `resolve_key` follows the indirection through this
2598        // `super_version`, never through the current one, so the pointer and the
2599        // blob file it names cannot come from different points in time.
2600        let super_version = self.index.snapshot_for_read(seqno)?;
2601        super_version.version.read_mix().note_points(1);
2602        self.resolve_key(&super_version, &self.blob_source(), key.as_ref(), seqno)
2603    }
2604
2605    fn multi_get<K: AsRef<[u8]>>(
2606        &self,
2607        keys: impl IntoIterator<Item = K>,
2608        seqno: SeqNo,
2609    ) -> crate::Result<Vec<Option<crate::UserValue>>> {
2610        // The snapshot is validated BEFORE the empty-batch return: the
2611        // retention contract is per read, not per key, and the standard tree
2612        // refuses an empty batch below retention the same way.
2613        let super_version = self.index.snapshot_for_read(seqno)?;
2614
2615        let keys: Vec<_> = keys.into_iter().collect();
2616        let n = keys.len();
2617        if n == 0 {
2618            return Ok(Vec::new());
2619        }
2620
2621        // For small batches, use the simple per-key path
2622        if n <= 2 {
2623            super_version
2624                .version
2625                .read_mix()
2626                .note_points(u64::try_from(n).unwrap_or(u64::MAX));
2627            let source = self.blob_source();
2628            return keys
2629                .iter()
2630                .map(|key| self.resolve_key(&super_version, &source, key.as_ref(), seqno))
2631                .collect();
2632        }
2633
2634        // The read shared with `Tree::multi_get`, which follows a blob
2635        // indirection into this version's value log. One source for the whole
2636        // read, so every key caches as the tree said when it began.
2637        let source = self.blob_source();
2638        crate::Tree::read_many(&crate::tree::read_job::ReadCtx {
2639            super_version,
2640            keys,
2641            seqno,
2642            comparator: Arc::clone(&self.index.config.comparator),
2643            merge_operator: self.index.config.merge_operator.clone(),
2644            merge_base: Some(source.clone()),
2645            values: crate::tree::read_job::Values::Blob(source),
2646            metadata_budget: self.index.config.multi_get_metadata_budget,
2647        })
2648    }
2649
2650    fn merge<K: Into<UserKey>, V: Into<UserValue>>(
2651        &self,
2652        key: K,
2653        operand: V,
2654        seqno: SeqNo,
2655    ) -> (u64, u64) {
2656        self.index.merge(key, operand, seqno)
2657    }
2658
2659    fn remove<K: Into<UserKey>>(&self, key: K, seqno: SeqNo) -> (u64, u64) {
2660        self.index.remove(key, seqno)
2661    }
2662
2663    fn remove_weak<K: Into<UserKey>>(&self, key: K, seqno: SeqNo) -> (u64, u64) {
2664        self.index.remove_weak(key, seqno)
2665    }
2666
2667    fn remove_range<K: Into<UserKey>>(&self, start: K, end: K, seqno: SeqNo) -> u64 {
2668        self.index.remove_range(start, end, seqno)
2669    }
2670}
2671
2672#[cfg(test)]
2673mod tests;