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