Skip to main content

lsm_tree/config/
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(crate) mod blob_lifetime;
6mod block_size;
7mod column_encoding;
8mod compression;
9mod delete_strategy;
10mod filter;
11mod flush_durability;
12mod hash_ratio;
13mod locator;
14mod pinning;
15mod restart_interval;
16
17pub use blob_lifetime::{LifetimeGroups, LifetimeHint, LifetimeHintFn};
18pub use block_size::{BlockSizePolicy, MAX_BLOCK_SIZE};
19pub use column_encoding::{ColumnEncoding, ColumnEncodingPolicy};
20pub use compression::CompressionPolicy;
21pub use delete_strategy::{DeleteStrategy, DeleteStrategyPolicy};
22pub use filter::{BloomConstructionPolicy, FilterAdvisor, FilterPolicy, FilterPolicyEntry};
23pub use flush_durability::FlushDurability;
24pub use hash_ratio::HashRatioPolicy;
25pub use locator::{LocatorPolicy, LocatorPolicyEntry, LocatorPrecision};
26pub use pinning::PinningPolicy;
27pub use restart_interval::RestartIntervalPolicy;
28
29/// Partitioning policy for indexes and filters
30pub type PartitioningPolicy = PinningPolicy;
31
32#[cfg(feature = "std")]
33use crate::fs::StdFs;
34use crate::path::PathBuf;
35use crate::{
36    AnyTree, BlobTree, Cache, CompressionType, DescriptorTable, SharedSequenceNumberGenerator,
37    Tree,
38    compaction::filter::Factory,
39    comparator::SharedComparator,
40    encryption::EncryptionProvider,
41    file::TABLES_FOLDER,
42    fs::{Fs, SyncMode},
43    merge_operator::MergeOperator,
44    path::absolute_path,
45    prefix::PrefixExtractor,
46};
47// std-only: used solely by the std-gated `Config::default` / `Config::new`
48// constructors (the no_std path builds `Config` field-by-field).
49#[cfg(feature = "std")]
50use crate::{SequenceNumberCounter, comparator, path::Path, version::DEFAULT_LEVEL_COUNT};
51use alloc::sync::Arc;
52#[cfg(not(feature = "std"))]
53use alloc::vec::Vec;
54use core::ops::Range;
55
56/// The size a columnar row group is cut at unless
57/// [`Config::columnar_row_group_size_policy`] says otherwise.
58///
59/// Four row pages of [`DEFAULT_COLUMNAR_PAGE_SIZE`]: the smallest group that
60/// reads fewer bytes than one-page groups on a near-full scan and a point
61/// read without reading more on a sparse scan. The measured grid it was chosen
62/// from is in `docs/columnar-page-format.md`.
63pub const DEFAULT_COLUMNAR_ROW_GROUP_SIZE: u32 = 16 * 1_024;
64
65/// The size a columnar row group's rows are cut into row pages at unless
66/// [`Config::columnar_page_size_policy`] says otherwise.
67pub const DEFAULT_COLUMNAR_PAGE_SIZE: u32 = 4_096;
68
69/// How a columnar read fetches the pages it wants: how much one request may
70/// ask for, and how many requests it keeps in flight at once.
71///
72/// A property of the reader, not of the file: the same table reads under any
73/// budget and returns the same rows, and a larger budget takes the pages in
74/// fewer requests, because a column's pages lie next to each other and a run
75/// of them is one request up to the I/O buffer. Set per tree with
76/// [`Config::columnar_read_budget`].
77///
78/// # Examples
79///
80/// ```
81/// use lsm_tree::config::ReadBudget;
82///
83/// // 256 KiB per request, eight requests in flight at once.
84/// let budget = ReadBudget::new(256 * 1_024, 8);
85/// assert_eq!(budget.io_buffer(), 256 * 1_024);
86/// assert_eq!(budget.in_flight(), 8);
87/// ```
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
89pub struct ReadBudget {
90    io_buffer: u32,
91    in_flight: u16,
92}
93
94impl ReadBudget {
95    /// The least a request may ask for: a block header's worth, below which
96    /// no request holds even the header of the block it reads.
97    pub const MIN_IO_BUFFER: u32 = 64;
98
99    /// A budget of `io_buffer` bytes per request and `in_flight` requests at
100    /// once. A budget below [`Self::MIN_IO_BUFFER`] is raised to it, and a
101    /// zero `in_flight` to one: a read needs at least one request of at least
102    /// a header to make progress.
103    ///
104    /// A request never splits a block: a directory, zone block or page larger
105    /// than the I/O buffer is still one request, and blocks are asked for
106    /// together only while they fit it.
107    #[must_use]
108    pub const fn new(io_buffer: u32, in_flight: u16) -> Self {
109        Self {
110            io_buffer: if io_buffer < Self::MIN_IO_BUFFER {
111                Self::MIN_IO_BUFFER
112            } else {
113                io_buffer
114            },
115            in_flight: if in_flight == 0 { 1 } else { in_flight },
116        }
117    }
118
119    /// Bytes one request may ask for.
120    #[must_use]
121    pub const fn io_buffer(&self) -> u32 {
122        self.io_buffer
123    }
124
125    /// Requests submitted together, which a backend with batched I/O keeps
126    /// in flight at once.
127    #[must_use]
128    pub const fn in_flight(&self) -> u16 {
129        self.in_flight
130    }
131}
132
133impl Default for ReadBudget {
134    /// 1 MiB per request, the I/O buffer Vortex reads a column with, and 16
135    /// requests in flight.
136    fn default() -> Self {
137        Self::new(1 << 20, 16)
138    }
139}
140
141/// The page bytes a projected columnar scan holds at once unless
142/// [`Config::columnar_scan_budget`] says otherwise: what one read request of
143/// the default [`ReadBudget`] fetches.
144pub const DEFAULT_COLUMNAR_SCAN_BUDGET: u64 = 1 << 20;
145
146/// The default of [`Config::multi_get_metadata_budget`].
147///
148/// Room for the filters of a few large tables in flight together, which is
149/// all the overlap the reads of a level gain from.
150pub const DEFAULT_MULTI_GET_METADATA_BUDGET: u64 = 8 << 20;
151
152/// The default of [`Config::table_target_size`]: 64 MiB, the size the leveled
153/// and tiered strategies cut compaction output at.
154pub const DEFAULT_TABLE_TARGET_SIZE: u64 = 64 * 1_024 * 1_024;
155
156/// Per-level filesystem routing entry for tiered storage.
157///
158/// Maps a range of LSM levels to a base directory and filesystem backend.
159/// Tables at these levels are stored under `path/tables/`.
160///
161/// # Example
162///
163/// ```
164/// use lsm_tree::config::LevelRoute;
165/// use lsm_tree::fs::StdFs;
166/// use std::sync::Arc;
167///
168/// // Hot tier: L0-L1 on NVMe
169/// let hot = LevelRoute {
170///     levels: 0..2,
171///     path: "/mnt/nvme/db".into(),
172///     fs: Arc::new(StdFs),
173/// };
174///
175/// // Cold tier: L4-L6 on HDD
176/// let cold = LevelRoute {
177///     levels: 4..7,
178///     path: "/mnt/hdd/db".into(),
179///     fs: Arc::new(StdFs),
180/// };
181/// ```
182#[derive(Clone)]
183pub struct LevelRoute {
184    /// LSM levels this route covers (e.g., `0..2` for L0–L1).
185    pub levels: Range<u8>,
186
187    /// Base data directory for tables at these levels.
188    pub path: PathBuf,
189
190    /// Filesystem backend for I/O at these levels.
191    pub fs: Arc<dyn Fs>,
192}
193
194impl core::fmt::Debug for LevelRoute {
195    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
196        f.debug_struct("LevelRoute")
197            .field("levels", &self.levels)
198            .field("path", &self.path)
199            .finish_non_exhaustive()
200    }
201}
202
203/// Policy governing what `Tree::open` does when the manifest's edit log ends
204/// inside a record.
205///
206/// The manifest is a snapshot plus an append-only edit log. The snapshot's
207/// sections are checksummed blocks its checksummed footer indexes, so they
208/// are read whole or not at all and no mode relaxes them. The only damage an
209/// append can leave that is not already covered is a torn last record: power
210/// was lost mid-append, so the operation it records was never acknowledged.
211/// This is the one thing a mode chooses.
212///
213/// Corruption of anything already committed fails the open in every mode.
214/// Rolling a committed edit back would roll back every committed edit after
215/// it, and the open then deletes the tables those edits added as orphans,
216/// losing acknowledged data for good. `RocksDB` and Pebble refuse the same
217/// way; the remedy is [`Config::repair`], which rebuilds the manifest from
218/// the table files themselves.
219#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
220pub enum ManifestRecoveryMode {
221    /// Any defect fails the open, a torn last edit included, so an operator
222    /// decides how to resolve it ([`Config::repair`] recovers every table on
223    /// disk). The default.
224    #[default]
225    AbsoluteConsistency,
226
227    /// A torn last edit is rolled back and the tree opens at the durable
228    /// prefix, with a `warn!`. The torn edit was never acknowledged, so
229    /// nothing a caller was told is durable is lost. Any other defect still
230    /// fails the open.
231    TolerateCorruptedTailRecords,
232}
233
234/// LSM-tree type
235#[derive(Copy, Clone, Debug, PartialEq, Eq)]
236pub enum TreeType {
237    /// Standard LSM-tree, see [`Tree`]
238    Standard,
239
240    /// Key-value separated LSM-tree, see [`BlobTree`]
241    Blob,
242}
243
244impl From<TreeType> for u8 {
245    fn from(val: TreeType) -> Self {
246        match val {
247            TreeType::Standard => 0,
248            TreeType::Blob => 1,
249        }
250    }
251}
252
253impl TryFrom<u8> for TreeType {
254    type Error = ();
255
256    fn try_from(value: u8) -> Result<Self, Self::Error> {
257        match value {
258            0 => Ok(Self::Standard),
259            1 => Ok(Self::Blob),
260            _ => Err(()),
261        }
262    }
263}
264
265#[cfg_attr(
266    not(feature = "std"),
267    allow(
268        dead_code,
269        reason = "default data-folder path used only on the std-gated default-config path"
270    )
271)]
272const DEFAULT_FILE_FOLDER: &str = ".lsm.data";
273
274/// Options for key-value separation
275///
276/// Blob compression is not among them: it changes on a live tree, so it lives in
277/// [`RuntimeConfig::blob_compression`](crate::runtime_config::RuntimeConfig::blob_compression),
278/// seeded by [`Config::blob_compression`].
279#[derive(Clone, Debug, PartialEq)]
280pub struct KvSeparationOptions {
281    /// Blob file target size in bytes
282    #[doc(hidden)]
283    pub file_target_size: u64,
284
285    /// Key-value separation threshold in bytes
286    #[doc(hidden)]
287    pub separation_threshold: u32,
288
289    /// Separation thresholds of the fields of rows written as cells, as
290    /// `(column, bytes)` pairs; a column without one uses
291    /// `separation_threshold`. See [`Self::cell_separation_threshold`].
292    #[doc(hidden)]
293    pub cell_separation_thresholds: alloc::vec::Vec<(u16, u32)>,
294
295    #[doc(hidden)]
296    pub staleness_threshold: f32,
297
298    #[doc(hidden)]
299    pub age_cutoff: f32,
300
301    /// How many upcoming values a scan reads ahead in one coalesced batch.
302    ///
303    /// `0` disables read-ahead. See
304    /// [`scan_prefetch`](KvSeparationOptions::scan_prefetch).
305    #[doc(hidden)]
306    pub scan_prefetch: u16,
307
308    /// Relocation of blob files for scan locality, off when `None`. See
309    /// [`Self::relocate_for_locality`].
310    #[doc(hidden)]
311    pub locality_relocation: Option<BlobLocalityRelocation>,
312
313    /// How many lifetime groups values are split into. See
314    /// [`Self::lifetime_groups`].
315    #[doc(hidden)]
316    pub lifetime_groups: LifetimeGroups,
317
318    /// The caller's lifetime class per key, over the observed one. See
319    /// [`Self::lifetime_hint`].
320    #[doc(hidden)]
321    pub lifetime_hint: Option<LifetimeHint>,
322
323    /// A zstd dictionary to register with the tree at open, for the blob
324    /// compression to name. See [`Self::dict`].
325    #[cfg(zstd_any)]
326    #[doc(hidden)]
327    pub zstd_dictionary: Option<alloc::sync::Arc<crate::compression::ZstdDictionary>>,
328}
329
330/// Whether a value read from a blob file goes into the shared cache.
331///
332/// Lookups are the same either way: a value already cached is served from
333/// it. Set on a live tree through
334/// [`RuntimeConfig::blob_cache_fill`](crate::runtime_config::RuntimeConfig::blob_cache_fill),
335/// seeded by [`Config::blob_cache_fill`].
336#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
337pub enum BlobCacheFill {
338    /// Every value read from a blob file is cached, and a scan reads its
339    /// values ahead into the cache.
340    Always,
341    /// No value read from a blob file is cached: values large enough to be
342    /// separated are mostly read once, and cached they would evict the index,
343    /// filter and data blocks of every tree sharing the cache. A scan still
344    /// reads its values ahead, into a store of its own holding at most one
345    /// coalesced read, from which it hands them out.
346    #[default]
347    Never,
348}
349
350impl BlobCacheFill {
351    /// Whether a value read now goes into the cache.
352    #[must_use]
353    pub const fn fills(self) -> bool {
354        matches!(self, Self::Always)
355    }
356}
357
358/// When and how much a compaction relocates blob files to restore scan
359/// locality. Set through [`KvSeparationOptions::relocate_for_locality`].
360#[derive(Copy, Clone, Debug, PartialEq)]
361pub struct BlobLocalityRelocation {
362    /// The blob reference depth a compaction's inputs may reach before it
363    /// relocates for locality. Relocation fires only above it.
364    pub max_depth: core::num::NonZeroU64,
365
366    /// The extra I/O relocation may add, as a fraction of the bytes the
367    /// compaction writes anyway.
368    pub budget: f32,
369}
370
371/// The threshold `thresholds` gives `column`, else `default`.
372pub(crate) fn cell_threshold(thresholds: &[(u16, u32)], default: u32, column: u16) -> u32 {
373    thresholds
374        .iter()
375        .find(|(set, _)| *set == column)
376        .map_or(default, |(_, bytes)| *bytes)
377}
378
379impl Default for KvSeparationOptions {
380    fn default() -> Self {
381        Self {
382            file_target_size: /* 64 MiB */ 64 * 1_024 * 1_024,
383            separation_threshold: /* 1 KiB */ 1_024,
384            cell_separation_thresholds: alloc::vec::Vec::new(),
385
386            staleness_threshold: 0.25,
387            age_cutoff: 0.25,
388
389            scan_prefetch: 64,
390
391            locality_relocation: None,
392
393            lifetime_groups: LifetimeGroups::MAX,
394            lifetime_hint: None,
395
396            #[cfg(zstd_any)]
397            zstd_dictionary: None,
398        }
399    }
400}
401
402impl KvSeparationOptions {
403    /// Sets the target size of blob files.
404    ///
405    /// Smaller blob files allow more granular garbage collection
406    /// which allows lower space amp for lower write I/O cost.
407    ///
408    /// Larger blob files decrease the number of files on disk and maintenance
409    /// overhead.
410    ///
411    /// Defaults to 64 MiB.
412    #[must_use]
413    pub fn file_target_size(mut self, bytes: u64) -> Self {
414        self.file_target_size = bytes;
415        self
416    }
417
418    /// Sets the key-value separation threshold in bytes.
419    ///
420    /// Smaller value will reduce compaction overhead and thus write amplification,
421    /// at the cost of lower read performance.
422    ///
423    /// Defaults to 1 KiB.
424    #[must_use]
425    pub fn separation_threshold(mut self, bytes: u32) -> Self {
426        self.separation_threshold = bytes;
427        self
428    }
429
430    /// Sets the separation threshold of `column` in rows written as cells
431    /// (see [`BlobTree::insert_cells`](crate::BlobTree::insert_cells)): a
432    /// value field of that column goes to a blob file when it is at least
433    /// `bytes` long. Setting a column again replaces its threshold; a column
434    /// never set uses [`Self::separation_threshold`].
435    ///
436    /// A field that is always read with the row's compact attributes can stay
437    /// inline whatever its size (`u32::MAX`), and a field that is rarely read
438    /// can go to a blob file however small (`0`).
439    ///
440    /// # Examples
441    ///
442    /// ```
443    /// use lsm_tree::KvSeparationOptions;
444    ///
445    /// // Column 3 (status) never separates; column 4 (body) separates from 256 bytes.
446    /// let opts = KvSeparationOptions::default()
447    ///     .cell_separation_threshold(3, u32::MAX)
448    ///     .cell_separation_threshold(4, 256);
449    /// ```
450    #[must_use]
451    pub fn cell_separation_threshold(mut self, column: u16, bytes: u32) -> Self {
452        match self
453            .cell_separation_thresholds
454            .iter_mut()
455            .find(|(set, _)| *set == column)
456        {
457            Some(entry) => entry.1 = bytes,
458            None => self.cell_separation_thresholds.push((column, bytes)),
459        }
460        self
461    }
462
463    /// What picks the lifetime class of a value written now.
464    #[must_use]
465    pub(crate) fn lifetime_classifier(&self) -> blob_lifetime::LifetimeClassifier<'_> {
466        blob_lifetime::LifetimeClassifier {
467            groups: self.lifetime_groups,
468            hint: self.lifetime_hint.as_ref(),
469        }
470    }
471
472    /// The separation threshold of the fields of `column`.
473    #[must_use]
474    pub(crate) fn cell_threshold(&self, column: u16) -> u32 {
475        cell_threshold(
476            &self.cell_separation_thresholds,
477            self.separation_threshold,
478            column,
479        )
480    }
481
482    /// Sets how many upcoming values a scan reads ahead in one batch.
483    ///
484    /// A scan resolves separated values one at a time, and each is its own read
485    /// of a few hundred bytes. Values sit in the blob file in the order the
486    /// flush wrote them, which is key order, so a scan's next values are
487    /// usually its immediate on-disk neighbours: reading a window of them at
488    /// once collapses that stream of small reads into a few large ones.
489    ///
490    /// Read-ahead starts only once the scan's caller resolves a value
491    /// unconditionally, so a scan that reads keys alone never pays for it, and
492    /// neither does one that decides per key whether the value is worth
493    /// reading. Larger windows amortize better on long scans; smaller ones
494    /// waste less on a scan that stops early. `0` disables it.
495    ///
496    /// Applies to the sequential scans: `iter`, `range`, `prefix` and
497    /// `batch_range_scan`. NOT to the seekable iterator, where a seek would
498    /// throw away whatever was read ahead, and which exists for jumping around
499    /// rather than walking a run.
500    ///
501    /// Defaults to 64.
502    #[must_use]
503    pub fn scan_prefetch(mut self, items: u16) -> Self {
504        self.scan_prefetch = items;
505        self
506    }
507
508    /// Lets compactions relocate blob files to restore scan locality.
509    ///
510    /// Values keep the order they were flushed in, so keys written in
511    /// separate batches end up interleaved across many blob files, and a range
512    /// scan alternates between all of them. A compaction whose input tables
513    /// reference files interleaved deeper than `max_depth` (see
514    /// [`BlobReferenceStats::depth`](crate::BlobReferenceStats::depth)) rewrites
515    /// the deepest ones in key order, so each output file holds one run of
516    /// adjacent keys. A table a strategy would only move to another level is
517    /// merged instead when this applies to it, so the interleaving is not
518    /// carried down unchanged. Files written in consecutive key runs have a
519    /// depth of one and are never relocated for this, however many there are.
520    ///
521    /// Relocation copies values as they are stored, so files written under
522    /// different blob codecs are rewritten into one file per codec and do not
523    /// merge further; a file is relocated only together with another of its
524    /// codec that it overlaps.
525    ///
526    /// Relocation is bounded three ways:
527    /// - only whole files are rewritten, and only those no table outside the
528    ///   compaction references;
529    /// - reading and writing them together may cost at most `budget` times the
530    ///   bytes the compaction writes anyway (its tables, plus the stale blob
531    ///   files it already relocates, which are always served first);
532    /// - nothing is relocated for locality when the output would not leave the
533    ///   reserved free space intact.
534    ///
535    /// A `budget` of zero, negative or NaN relocates nothing. Off by default:
536    /// compactions then behave and write exactly as without this option.
537    ///
538    /// # Examples
539    ///
540    /// ```
541    /// use core::num::NonZeroU64;
542    /// use lsm_tree::KvSeparationOptions;
543    ///
544    /// let depth = NonZeroU64::new(4).expect("non-zero");
545    /// let opts = KvSeparationOptions::default().relocate_for_locality(depth, 0.5);
546    /// assert!(opts.locality_relocation.is_some());
547    /// ```
548    #[must_use]
549    pub fn relocate_for_locality(mut self, max_depth: core::num::NonZeroU64, budget: f32) -> Self {
550        self.locality_relocation = Some(BlobLocalityRelocation { max_depth, budget });
551        self
552    }
553
554    /// Splits blob values into `groups` by how long they are expected to
555    /// live, each group written to its own blob files.
556    ///
557    /// A blob file is reclaimed as a whole, so a value that lives long and
558    /// shares a file with values that are overwritten quickly is copied
559    /// forward every time that file is collected. Grouping keeps the two apart:
560    ///
561    /// - group `0` takes values a flush finds overwritten within the memtable
562    ///   it writes, which are likely to be overwritten again soon;
563    /// - group `1` takes every other value written for the first time;
564    /// - a value relocated out of a file moves one group up, having outlived
565    ///   it, up to the last group.
566    ///
567    /// [`Self::lifetime_hint`] overrides the observed group per key. On Linux
568    /// the device is also told each file's expected lifetime
569    /// (`F_SET_RW_HINT`), which a flash device uses to keep data that dies
570    /// together in the same erase blocks.
571    ///
572    /// The group only decides where a value is written. Which values are
573    /// visible, and when a blob file may be removed, follows references and
574    /// retention exactly as without grouping. Separating values by lifetime
575    /// spreads keys that a range scan reads together over more files, so it
576    /// trades scan locality for less relocation work.
577    ///
578    /// Defaults to [`LifetimeGroups::MAX`], four groups. [`LifetimeGroups::ONE`]
579    /// turns grouping off: every value then shares the same files and no
580    /// device hint is given. Any count from one to [`LifetimeGroups::MAX`] is
581    /// accepted ([`LifetimeGroups::new`] refuses the rest), and a flush or a
582    /// relocation keeps at most that many files open per blob codec.
583    ///
584    /// The setting applies to the files written from now on and may change
585    /// between opens: a file keeps the class it was written with, and a class
586    /// above the configured groups counts as the last group.
587    ///
588    /// # Examples
589    ///
590    /// ```
591    /// use lsm_tree::{KvSeparationOptions, config::LifetimeGroups};
592    ///
593    /// // Grouped by default.
594    /// assert_eq!(
595    ///     KvSeparationOptions::default().lifetime_groups,
596    ///     LifetimeGroups::MAX,
597    /// );
598    ///
599    /// // Fewer groups, or none at all.
600    /// let groups = LifetimeGroups::new(2).expect("within the bound");
601    /// let two = KvSeparationOptions::default().lifetime_groups(groups);
602    /// assert_eq!(two.lifetime_groups.get(), 2);
603    /// let off = KvSeparationOptions::default().lifetime_groups(LifetimeGroups::ONE);
604    /// assert_eq!(off.lifetime_groups.get(), 1);
605    /// ```
606    #[must_use]
607    pub fn lifetime_groups(mut self, groups: LifetimeGroups) -> Self {
608        self.lifetime_groups = groups;
609        self
610    }
611
612    /// Sets the caller's estimate of each key's lifetime class, used over
613    /// the observed one wherever it answers (see [`Self::lifetime_groups`]).
614    ///
615    /// `hint` returns the class for a key, `0` for the shortest-lived; a class
616    /// past the last group goes to the last group, and `None` leaves the key
617    /// to the observed estimate. The hint is advisory: a wrong one costs
618    /// relocation work and never changes what a read returns. It has no effect
619    /// with a single group.
620    ///
621    /// # Examples
622    ///
623    /// ```
624    /// use lsm_tree::{
625    ///     KvSeparationOptions,
626    ///     config::{LifetimeGroups, LifetimeHint},
627    /// };
628    ///
629    /// // Session payloads churn, archived objects stay.
630    /// let hint = LifetimeHint::new(|key: &[u8]| {
631    ///     if key.starts_with(b"session/") {
632    ///         Some(0)
633    ///     } else if key.starts_with(b"archive/") {
634    ///         Some(3)
635    ///     } else {
636    ///         None
637    ///     }
638    /// });
639    /// let opts = KvSeparationOptions::default()
640    ///     .lifetime_groups(LifetimeGroups::MAX)
641    ///     .lifetime_hint(hint);
642    /// assert!(opts.lifetime_hint.is_some());
643    /// ```
644    #[must_use]
645    pub fn lifetime_hint(mut self, hint: LifetimeHint) -> Self {
646        self.lifetime_hint = Some(hint);
647        self
648    }
649
650    /// Sets the staleness threshold percentage.
651    ///
652    /// The staleness percentage determines how much a blob file needs to be fragmented to be
653    /// picked up by the garbage collection.
654    ///
655    /// Defaults to 33%.
656    #[must_use]
657    pub fn staleness_threshold(mut self, ratio: f32) -> Self {
658        self.staleness_threshold = ratio;
659        self
660    }
661
662    /// Sets the age cutoff threshold.
663    ///
664    /// Defaults to 20%.
665    #[must_use]
666    pub fn age_cutoff(mut self, ratio: f32) -> Self {
667        self.age_cutoff = ratio;
668        self
669    }
670
671    /// Supplies a zstd dictionary for the blob compression to name.
672    ///
673    /// The open registers it with the tree, which stores it, so a later open
674    /// needs nothing supplied; this is also how the bytes reach a tree the
675    /// first time. Which dictionary blob files are written against is decided
676    /// by [`Config::blob_compression`]: a [`CompressionType::ZstdDict`] there
677    /// names one by id, and [`Config::open`] refuses one the tree does not hold
678    /// with [`Error::ZstdDictMismatch`](crate::Error::ZstdDictMismatch).
679    #[cfg(zstd_any)]
680    #[must_use]
681    pub fn dict(
682        mut self,
683        dictionary: alloc::sync::Arc<crate::compression::ZstdDictionary>,
684    ) -> Self {
685        self.zstd_dictionary = Some(dictionary);
686        self
687    }
688}
689
690/// Where a compaction ends its outputs.
691#[derive(Clone, Copy, Debug, PartialEq, Eq)]
692pub(crate) enum OutputCuts {
693    /// On the boundaries between the tables of the level the outputs are
694    /// merged into next, within half the target size and twice it.
695    Aligned,
696    /// At the target size alone.
697    TargetSize,
698}
699
700/// Tree configuration builder
701// Clone: every shared handle is `Arc`-backed, so a clone is a cheap second
702// reference to the same backends — which is what lets `open_or_repair` retry
703// an `open(self)` after a repair.
704#[derive(Clone)]
705pub struct Config {
706    /// Folder path
707    #[doc(hidden)]
708    pub path: PathBuf,
709
710    /// Default filesystem backend for levels without an explicit route.
711    ///
712    /// Defaults to [`StdFs`]. Use [`Config::with_fs`] to plug in an
713    /// alternative backend such as [`MemFs`](crate::fs::MemFs).
714    ///
715    /// Both fresh tree creation and reopening (recovery) are supported
716    /// for any backend that implements [`Fs`].
717    #[doc(hidden)]
718    pub fs: Arc<dyn Fs>,
719
720    /// Per-level filesystem routing for tiered storage.
721    ///
722    /// When set, tables at different LSM levels can be stored on different
723    /// storage devices (e.g., NVMe for L0–L1, SSD for L2–L4, HDD for L5–L6).
724    /// Each entry maps a range of levels to a base directory and filesystem
725    /// backend. Uncovered levels fall back to the primary `path` and `fs`.
726    ///
727    /// Zero additional overhead when `None` — only a single branch check;
728    /// path construction allocations are unchanged.
729    #[doc(hidden)]
730    pub level_routes: Option<Vec<LevelRoute>>,
731
732    /// Block cache to use
733    #[doc(hidden)]
734    pub cache: Arc<Cache>,
735
736    /// Descriptor table to use
737    #[doc(hidden)]
738    pub descriptor_table: Option<Arc<DescriptorTable>>,
739
740    /// Number of levels of the LSM tree (depth of tree)
741    ///
742    /// Once set, the level count is fixed (in the manifest snapshot `v<id>`)
743    pub level_count: u8,
744
745    /// Restart interval inside data blocks
746    pub data_block_restart_interval_policy: RestartIntervalPolicy,
747
748    /// Restart interval inside index blocks
749    pub index_block_restart_interval_policy: RestartIntervalPolicy,
750
751    /// Block size of data blocks
752    pub data_block_size_policy: BlockSizePolicy,
753
754    /// Uncompressed size a columnar table's row groups are cut at, per level:
755    /// the columnar counterpart of [`Self::data_block_size_policy`], which a
756    /// columnar table does not use for its data. A row group is split into
757    /// independently readable column pages, so it is sized for what a read of
758    /// one page is worth rather than for what a read of the whole group costs.
759    pub columnar_row_group_size_policy: BlockSizePolicy,
760
761    /// Uncompressed bytes of row data a columnar row page holds, per level.
762    /// A row group's rows are cut into row pages shared by every column, so a
763    /// read of a few rows decodes only the pages that hold them.
764    pub columnar_page_size_policy: BlockSizePolicy,
765
766    /// How a columnar table's column pages store their values, per level;
767    /// see [`ColumnEncoding`].
768    pub column_encoding_policy: ColumnEncodingPolicy,
769
770    /// How columnar reads of this tree fetch their pages; see [`ReadBudget`].
771    pub columnar_read_budget: ReadBudget,
772
773    /// The page bytes one projected columnar scan holds at once, shared by
774    /// the segments it merges; see [`Self::columnar_scan_budget`].
775    pub columnar_scan_budget: u64,
776
777    /// The filter and index bytes a multi-get holds at once across the tables
778    /// of a level it reads stage by stage; see
779    /// [`Self::multi_get_metadata_budget`].
780    pub multi_get_metadata_budget: u64,
781
782    /// Whether to pin index blocks
783    pub index_block_pinning_policy: PinningPolicy,
784
785    /// Whether to pin filter blocks
786    pub filter_block_pinning_policy: PinningPolicy,
787
788    /// Whether to pin top level index of partitioned index
789    pub top_level_index_block_pinning_policy: PinningPolicy,
790
791    /// Whether to pin top level index of partitioned filter
792    pub top_level_filter_block_pinning_policy: PinningPolicy,
793
794    /// Data block hash ratio
795    pub data_block_hash_ratio_policy: HashRatioPolicy,
796
797    /// Whether to partition index blocks
798    pub index_block_partitioning_policy: PartitioningPolicy,
799
800    /// Whether to partition filter blocks
801    pub filter_block_partitioning_policy: PartitioningPolicy,
802
803    /// Partition size when using partitioned indexes, per level; see
804    /// [`Self::index_block_partition_size_policy`].
805    pub index_block_partition_size_policy: BlockSizePolicy,
806
807    /// Partition size when using partitioned filters, per level; see
808    /// [`Self::filter_block_partition_size_policy`].
809    pub filter_block_partition_size_policy: BlockSizePolicy,
810
811    /// If `true`, the last level will not build filters, reducing the filter size of a database
812    /// by ~90% typically
813    pub(crate) expect_point_read_hits: bool,
814
815    /// Where a compaction ends its outputs; see
816    /// [`Self::compaction_output_alignment`].
817    pub(crate) compaction_output_cuts: OutputCuts,
818
819    /// Per-block Page ECC. When `true`, every block on disk carries a parity
820    /// trailer; on read, if the block's XXH3 disagrees with the on-disk bytes,
821    /// the reader attempts recovery from the trailer before surfacing the
822    /// corruption. The correction scheme is selected at runtime
823    /// (`update_runtime_config`): per-word SEC-DED (the default), single XOR
824    /// parity, or Reed-Solomon. Requires the `page_ecc` cargo feature — opening a
825    /// tree with `page_ecc = true` on a build without the feature returns
826    /// [`crate::Error::PageEccUnsupported`].
827    ///
828    /// Off by default. `RocksDB` ships per-block ECC as an operator-
829    /// chosen knob (typically off on RAID-protected media, on on
830    /// single-drive) and the cost is non-trivial on the write path,
831    /// so the default keeps the existing behaviour.
832    pub(crate) page_ecc: bool,
833
834    /// Initial [`crate::runtime_config::RuntimeConfig`] snapshot
835    /// the tree starts with. Seeds both the first
836    /// `persist_version` call and the Tree's
837    /// `RuntimeConfigHandle`, so a non-default value supplied via
838    /// [`Config::with_runtime_config`] is honoured from byte zero
839    /// of the manifest. Defaults to `RuntimeConfig::default()` —
840    /// matches the pre-existing implicit behaviour.
841    #[expect(
842        clippy::struct_field_names,
843        reason = "name mirrors the type for grep-ability across the persist + Tree handle init wiring"
844    )]
845    pub(crate) initial_runtime_config: crate::runtime_config::RuntimeConfig,
846
847    /// Filter construction policy
848    pub filter_policy: FilterPolicy,
849
850    /// Allocates filter memory by measured negative-probe load; `None` (the
851    /// default) leaves every filter to [`Self::filter_policy`].
852    pub filter_advisor: Option<FilterAdvisor>,
853
854    /// Retrieval-ribbon locator policy (per level). Defaults to
855    /// [`LocatorPolicy::block_level`]: written SSTs carry an optional `locator`
856    /// section mapping each key to its data block for O(1) point reads (skipping
857    /// the index-block binary search). Set [`LocatorPolicy::disabled`] to opt
858    /// out — disabled levels produce byte-identical SSTs (no section).
859    pub locator_policy: LocatorPolicy,
860
861    /// Compaction filter factory
862    pub compaction_filter_factory: Option<Arc<dyn Factory>>,
863
864    /// Prefix extractor for prefix bloom filters.
865    ///
866    /// When set, the bloom filter indexes extracted prefixes in addition to
867    /// full keys, allowing prefix scans to skip segments that contain no
868    /// matching prefixes.
869    pub prefix_extractor: Option<Arc<dyn PrefixExtractor>>,
870
871    /// Key domains no table spans (see [`Config::key_domains`]).
872    pub key_domains: Option<Arc<dyn crate::KeyDomains>>,
873
874    /// Merge operator for commutative operations
875    ///
876    /// When set, enables `merge()` operations that store partial updates
877    /// which are lazily combined during reads and compaction.
878    pub merge_operator: Option<Arc<dyn MergeOperator>>,
879
880    #[doc(hidden)]
881    pub kv_separation_opts: Option<KvSeparationOptions>,
882
883    /// Custom user key comparator.
884    ///
885    /// When set, all key comparisons use this comparator instead of the
886    /// default lexicographic byte ordering. Once a tree is opened with a
887    /// comparator, it must always be re-opened with the same comparator.
888    // Not `pub` — use `Config::comparator()` builder method as the public API.
889    #[doc(hidden)]
890    pub(crate) comparator: SharedComparator,
891
892    /// Block-level encryption provider for encryption at rest.
893    ///
894    /// When set, all blocks (data, index, filter, meta) are encrypted
895    /// using this provider after compression and before checksumming.
896    pub(crate) encryption: Option<Arc<dyn EncryptionProvider>>,
897
898    /// The wall clock of this tree; `None` reads the process clock (see
899    /// [`Config::with_clock`]).
900    pub(crate) clock: Option<Arc<dyn crate::Clock>>,
901
902    /// Policy governing what `Tree::open` does when the on-disk
903    /// MANIFEST contains corrupt records. Defaults to
904    /// [`ManifestRecoveryMode::AbsoluteConsistency`], the only
905    /// production-safe choice — any corruption aborts the open. The only
906    /// other mode, [`ManifestRecoveryMode::TolerateCorruptedTailRecords`],
907    /// additionally drops a torn edit-log tail (an append the writer never
908    /// finished); damage to anything committed fails the open in both.
909    pub(crate) manifest_recovery_mode: ManifestRecoveryMode,
910
911    /// Durability level for every fsync the tree issues (SST writes,
912    /// manifest, version persist, directory syncs).
913    ///
914    /// Defaults to [`SyncMode::Normal`] (plain `fsync`), matching the
915    /// out-of-the-box durability of `RocksDB` and `SQLite`. Only observable on
916    /// macOS, where [`SyncMode::Full`] opts into the much slower
917    /// `F_FULLFSYNC` barrier and [`SyncMode::Barrier`] into ordered syncs;
918    /// on other platforms all modes are plain `fsync`. Set via
919    /// [`Config::sync_mode`].
920    pub(crate) sync_mode: SyncMode,
921
922    /// When a flush's tables become durable. Set via
923    /// [`Config::flush_durability`].
924    pub(crate) flush_durability: FlushDurability,
925
926    /// Bytes a table or blob file gathers between the writebacks its writer
927    /// starts. Set via [`Config::writeback_bytes`].
928    pub(crate) writeback_bytes: u64,
929
930    /// Size a flush or an ingestion cuts its output tables at. Set via
931    /// [`Config::table_target_size`].
932    pub(crate) table_target_size: u64,
933
934    /// When `true` (the default), [`Config::open`] and [`Config::repair`]
935    /// acquire an exclusive cross-process lock on a `LOCK` file in the tree
936    /// directory (an advisory OS file lock) and hold it for the lifetime of the
937    /// [`Tree`] (open) or the duration of the call (repair). A
938    /// second process attempting to open / repair the same directory fails fast
939    /// with [`Error::Locked`](crate::Error::Locked) instead of racing on the
940    /// manifest. Set `false` via [`Config::with_directory_lock`] only when the
941    /// embedder already enforces exclusive directory ownership at a higher layer
942    /// (e.g. a keyspace / journal manager). Best-effort per `Fs` backend: real
943    /// on-disk backends enforce it, in-memory backends are single-process and
944    /// satisfy it vacuously.
945    pub(crate) directory_lock: bool,
946
947    /// Shared live-progress counters the repair / salvage paths tick while
948    /// they run, or `None` (the default) to skip publishing. Set via
949    /// [`Config::with_recovery_progress`]; observed by polling
950    /// [`RecoveryProgress::snapshot`](crate::RecoveryProgress::snapshot) from
951    /// another thread.
952    #[cfg(feature = "std")]
953    pub(crate) recovery_progress: Option<Arc<crate::RecoveryProgress>>,
954
955    /// Edit-log size (bytes) past which the next manifest persist rotates: it
956    /// writes a fresh full snapshot and starts an empty log instead of appending
957    /// another [`VersionEdit`](crate::version::edit::VersionEdit). Bounds both
958    /// recovery replay time (edits to re-apply) and log disk use, while keeping
959    /// the common per-flush path a tiny `O(changed-levels)` append rather than an
960    /// `O(all-SSTs)` full manifest rewrite.
961    ///
962    /// Defaults to 1 MiB (≈ tens of thousands of edits). Set via
963    /// [`Config::manifest_log_rotate_bytes`]. A smaller value rotates more
964    /// often (shorter recovery, more frequent full-snapshot writes); `0` rotates
965    /// on every upgrade, degenerating to the full-rewrite-per-version behaviour.
966    pub(crate) manifest_log_rotate_bytes: u64,
967
968    /// Empty table files each tables folder creates at a time, ahead of the
969    /// tables written into them, with one folder sync for the batch. Set via
970    /// [`Config::table_file_pool`].
971    pub(crate) table_file_pool: usize,
972
973    /// Where a table file pool refills: off the claiming write's thread, or
974    /// on it, as it does without `std`. Set via
975    /// [`Config::table_file_refill_inline`].
976    pub(crate) table_file_refill: crate::table::file_pool::RefillOn,
977
978    /// Retention floor a manifest REPAIR seeds the rebuilt version with: the
979    /// highest snapshot seqno the repaired tree refuses to serve. The lost
980    /// manifest was the only record of the floor a past GC compaction or
981    /// `clear` established, and the tables cannot stand in for it, so the
982    /// deployment that ran those compactions (and knows the watermark it
983    /// passed) supplies it here. Defaults to `0`: a repaired tree serves
984    /// every snapshot, which is right only if history was never collected.
985    /// Set via [`Config::repair_retention_floor`].
986    pub(crate) repair_retention_floor: crate::SeqNo,
987
988    /// Compaction I/O rate limit in bytes per second.
989    ///
990    /// Caps the rate at which the compaction worker is allowed to issue
991    /// I/O, so background compaction cannot saturate the device and starve
992    /// user point reads / range scans (P99 stability). `0` (the default)
993    /// means unlimited — no throttling, no behaviour change. Flush and
994    /// user reads are never throttled, only compaction. Set via
995    /// [`Config::compaction_rate_limit`].
996    pub(crate) compaction_rate_limit: u64,
997
998    /// Optional shared compaction rate limiter. `None` (default) = the tree
999    /// builds its own from [`Self::compaction_rate_limit`]. `Some` = every
1000    /// tree holding this `Arc` is bounded by it together, at the rate it
1001    /// holds. Set via [`Config::compaction_rate_limiter`].
1002    pub(crate) compaction_rate_limiter: Option<Arc<crate::rate_limiter::RateLimiter>>,
1003
1004    /// The limiter the open tree throttles its compactions with, set at open
1005    /// for a strategy to read the live rate. Apart from
1006    /// [`Self::compaction_rate_limiter`], so a copy of the tree's
1007    /// configuration opened as another tree does not share it.
1008    pub(crate) live_compaction_rate_limiter: Option<Arc<crate::rate_limiter::RateLimiter>>,
1009
1010    /// Optional pool compactions reserve their room in, shared with other
1011    /// trees. `None` (default) = the tree keeps a pool of its own. Set via
1012    /// [`Config::compaction_capacity`].
1013    pub(crate) compaction_capacity: Option<Arc<crate::CapacityPool>>,
1014
1015    /// Bytes per second writes are paced to while the backpressure verdict is
1016    /// in its slowdown tier; `0` (the default) leaves the slowdown delay to
1017    /// the ramp. Set via [`Config::write_rate_limit`].
1018    pub(crate) write_rate_limit: u64,
1019
1020    /// Optional write rate limiter shared with other trees, used in place of
1021    /// the per-tree one built from [`Self::write_rate_limit`]. Set via
1022    /// [`Config::write_rate_limiter`].
1023    pub(crate) write_rate_limiter: Option<Arc<crate::rate_limiter::RateLimiter>>,
1024
1025    /// Optional write-buffer budget shared with other trees, which the
1026    /// memtable axis of the backpressure verdict compares in place of this
1027    /// tree's own unflushed bytes. Set via [`Config::write_buffer_budget`].
1028    // no-std: the budget reads members through their std-only arc-swap mirror
1029    #[cfg(feature = "std")]
1030    pub(crate) write_buffer_budget: Option<Arc<crate::backpressure::WriteBufferBudget>>,
1031
1032    /// Worker-thread count for compaction parallelism (`std` only), used two
1033    /// ways: it sizes the per-tree pool built at open when
1034    /// [`Self::compaction_pool`] is `None`, which prepares data blocks and
1035    /// builds partitioned-filter partitions, and it caps how many range-parallel
1036    /// sub-compactions a single compaction is split into. Default
1037    /// `max(1, available_parallelism / 2)` — leaves half the cores for
1038    /// application work. `1` forces the serial path for both. Without the
1039    /// `parallel` feature there is no built-in pool, so block compression and
1040    /// sub-compaction ranges run serially even for a value > 1. Set via
1041    /// [`Config::compaction_threads`].
1042    #[cfg(feature = "std")] // no-std: parallel compaction unavailable (no threads)
1043    pub(crate) compaction_threads: usize,
1044
1045    /// Optional shared compaction thread pool. `None` (default) = a per-tree
1046    /// pool is built at [`crate::Tree::open`] sized by [`Self::compaction_threads`]
1047    /// (predictable, matches the per-DB pattern). `Some` = caller-supplied
1048    /// executor shared across every tree holding this `Arc`, bounding total
1049    /// threads regardless of tree count. Set via [`Config::compaction_pool`].
1050    #[cfg(feature = "std")]
1051    pub(crate) compaction_pool: Option<Arc<dyn crate::table::writer::CompactionSpawner>>,
1052
1053    /// Payload size (bytes) below which a block of a parallel-compressed table
1054    /// is prepared on the writer thread instead of a worker: its transform
1055    /// costs less than handing it to a worker and back. `None` (default)
1056    /// derives it from the table's block codec. Set via
1057    /// [`Config::parallel_compression_inline_below`].
1058    #[cfg(feature = "std")]
1059    pub(crate) parallel_compression_inline_below: Option<u32>,
1060
1061    /// Minimum total input size (bytes) for a compaction to be split into
1062    /// parallel sub-compactions. Below it the compaction stays single-threaded
1063    /// (per-thread setup + extra output tables outweigh the parallelism on small
1064    /// compactions). Default
1065    /// [`SUBCOMPACTION_MIN_INPUT_BYTES`](crate::compaction::worker::SUBCOMPACTION_MIN_INPUT_BYTES)
1066    /// (8 MiB). Set via [`Config::subcompaction_min_bytes`].
1067    #[cfg(feature = "std")]
1068    pub(crate) subcompaction_min_bytes: u64,
1069
1070    /// Test-only failpoint: when armed, the first parallel sub-compaction range
1071    /// that observes it returns an error and disarms it, so the crash-safety
1072    /// rollback paths (sibling output rollback, input restore) can be exercised
1073    /// deterministically. Behind `cfg(test)`, never compiled into release builds.
1074    #[cfg(all(test, feature = "std"))]
1075    pub(crate) fail_one_subcompaction: Arc<core::sync::atomic::AtomicBool>,
1076
1077    /// Test-only failpoint: when armed, the serial merge raises the stop signal
1078    /// after writing its first item and disarms itself, so the interrupted-merge
1079    /// path is reachable without racing a tree drop against an in-flight
1080    /// compaction. Behind `cfg(test)`, never compiled into release builds.
1081    #[cfg(all(test, feature = "std"))]
1082    pub(crate) stop_serial_merge_after_first_item: Arc<core::sync::atomic::AtomicBool>,
1083
1084    /// Test-only failpoint: when armed, a tight-space compaction returns an error
1085    /// immediately after durably installing (and punching) its FIRST slice, so
1086    /// the crash-mid-loop recovery path (reopen a tree whose manifest carries a
1087    /// persisted input restriction) can be exercised deterministically. Behind
1088    /// `cfg(test)`, never compiled into release builds.
1089    #[cfg(all(test, feature = "std"))]
1090    pub(crate) fail_tight_after_first_slice: Arc<core::sync::atomic::AtomicBool>,
1091
1092    /// Test-only: bytes another tree sharing the capacity pool takes on the
1093    /// destination volume right before a tight-space pass reserves its first
1094    /// slice, after the pass measured the room. Behind `cfg(test)`.
1095    #[cfg(all(test, feature = "std"))]
1096    pub(crate) tight_slice_squeeze: Arc<core::sync::atomic::AtomicU64>,
1097
1098    /// Test-only failpoint: when armed, a tight-space relocation fails at the
1099    /// restricted-blob reopen step of its current slice — after the slice's
1100    /// outputs were finalized but before the install — so the pre-install
1101    /// rollback (retract the finalized-but-unreferenced outputs) can be
1102    /// exercised deterministically. Behind `cfg(test)`, never compiled into
1103    /// release builds.
1104    #[cfg(all(test, feature = "std"))]
1105    pub(crate) fail_tight_blob_reopen: Arc<core::sync::atomic::AtomicBool>,
1106
1107    /// Test-only failpoint: run once, then disarmed, between the moment a write
1108    /// has finished its output files and the moment it installs them (a flush,
1109    /// an ingestion, a parallel or tight-space compaction), so a test can put a
1110    /// concurrent operation into that window deterministically. Behind
1111    /// `cfg(test)`, never compiled into release builds.
1112    #[cfg(all(test, feature = "std"))]
1113    pub(crate) before_output_install: Arc<std::sync::Mutex<Option<OutputInstallHook>>>,
1114
1115    /// Test-only failpoint: run once, then disarmed, while a compaction
1116    /// finishes its outputs, after their last write and before their reopen.
1117    #[cfg(all(test, feature = "std"))]
1118    pub(crate) while_outputs_finish: Arc<std::sync::Mutex<Option<OutputInstallHook>>>,
1119
1120    /// A dictionary supplied for the data block policy to name. The open
1121    /// registers it with the tree, so it survives the reopen without being
1122    /// supplied again.
1123    ///
1124    /// Supply only: which dictionary new blocks are written against is the
1125    /// policy's to say, by id, and a writer resolves that id from
1126    /// [`Self::zstd_dictionaries`] when it starts. A slot filled at open could
1127    /// not follow a policy that changes on a live tree.
1128    #[cfg(zstd_any)]
1129    pub(crate) zstd_dictionary: Option<Arc<crate::compression::ZstdDictionary>>,
1130
1131    /// Every dictionary the tree can decompress against: the ones it has
1132    /// registered, plus [`Self::zstd_dictionary`] when one was supplied.
1133    ///
1134    /// Populated at open from the tree's own `dicts/` folder, so a reopen needs
1135    /// no dictionary from the caller at all. A table resolves the id it
1136    /// recorded against this set rather than being compared to a single
1137    /// configured dictionary, which is what lets one tree hold data written
1138    /// under more than one.
1139    ///
1140    /// Shared and swappable, because registration happens on a LIVE tree: a
1141    /// plain field would leave a freshly registered dictionary unresolvable
1142    /// until the next open, so the very tables the registration was made for
1143    /// would fail to read.
1144    #[cfg(zstd_any)]
1145    pub(crate) zstd_dictionaries: Arc<arc_swap::ArcSwap<crate::compression::ZstdDictionaries>>,
1146
1147    /// The global sequence number generator.
1148    ///
1149    /// Should be shared between multiple trees of a database.
1150    pub(crate) seqno: SharedSequenceNumberGenerator,
1151
1152    /// Sequence number watermark that is visible to readers.
1153    ///
1154    /// Used for MVCC snapshots and to control which updates are
1155    /// observable in a given view of the database.
1156    pub(crate) visible_seqno: SharedSequenceNumberGenerator,
1157}
1158
1159// TODO: remove default?
1160// std-only: the default backend is `StdFs` and the default path is resolved
1161// via std::path::absolute. no_std callers construct `Config` explicitly with a
1162// caller-provided `Fs`.
1163#[cfg(feature = "std")]
1164impl Default for Config {
1165    fn default() -> Self {
1166        Self {
1167            path: absolute_path(Path::new(DEFAULT_FILE_FOLDER)),
1168            fs: Arc::new(StdFs),
1169            level_routes: None,
1170            descriptor_table: Some(Arc::new(DescriptorTable::new(256))),
1171            seqno: SharedSequenceNumberGenerator::from(SequenceNumberCounter::default()),
1172            visible_seqno: SharedSequenceNumberGenerator::from(SequenceNumberCounter::default()),
1173
1174            cache: Arc::new(Cache::with_capacity_bytes(
1175                /* 16 MiB */ 16 * 1_024 * 1_024,
1176            )),
1177
1178            data_block_restart_interval_policy: RestartIntervalPolicy::all(16),
1179            index_block_restart_interval_policy: RestartIntervalPolicy::all(1),
1180
1181            level_count: DEFAULT_LEVEL_COUNT,
1182
1183            data_block_size_policy: BlockSizePolicy::all(4_096),
1184
1185            columnar_row_group_size_policy: BlockSizePolicy::all(DEFAULT_COLUMNAR_ROW_GROUP_SIZE),
1186
1187            columnar_page_size_policy: BlockSizePolicy::all(DEFAULT_COLUMNAR_PAGE_SIZE),
1188
1189            column_encoding_policy: ColumnEncodingPolicy::default(),
1190
1191            columnar_read_budget: ReadBudget::default(),
1192            columnar_scan_budget: DEFAULT_COLUMNAR_SCAN_BUDGET,
1193            multi_get_metadata_budget: DEFAULT_MULTI_GET_METADATA_BUDGET,
1194
1195            index_block_pinning_policy: PinningPolicy::new([true, true, false]),
1196            filter_block_pinning_policy: PinningPolicy::new([true, false]),
1197
1198            top_level_index_block_pinning_policy: PinningPolicy::all(true), // TODO: implement
1199            top_level_filter_block_pinning_policy: PinningPolicy::all(true), // TODO: implement
1200
1201            // Partitioned at every level so a bit-flip inside one
1202            // sub-index block only takes out the keys covered by that
1203            // partition, not the entire SST. A full-index SST has no
1204            // within-block redundancy: one corrupt byte in the single
1205            // index block makes every data block in the table
1206            // unreachable. See tests/partitioned_index_blast_radius.rs
1207            // for the isolation property this default relies on.
1208            index_block_partitioning_policy: PinningPolicy::all(true),
1209            // Filter-block default intentionally left at the pre-#329
1210            // shape (L3+ only). A corrupt filter block can produce a
1211            // false negative (filter says "not present" → read short-
1212            // circuits → caller misses an existing key), which is a
1213            // correctness hazard distinct from index corruption (where
1214            // the read fails loudly). Flipping this default is tracked
1215            // as a separate decision pending a filter blast-radius /
1216            // false-negative analysis; symmetry with index is not
1217            // sufficient justification on its own.
1218            filter_block_partitioning_policy: PinningPolicy::new([false, false, false, true]),
1219
1220            index_block_partition_size_policy: BlockSizePolicy::all(4_096),
1221            filter_block_partition_size_policy: BlockSizePolicy::all(4_096),
1222
1223            data_block_hash_ratio_policy: HashRatioPolicy::all(0.0),
1224
1225            locator_policy: LocatorPolicy::block_level(),
1226            filter_policy: FilterPolicy::all(FilterPolicyEntry::Bloom(
1227                BloomConstructionPolicy::BitsPerKey(10.0),
1228            )),
1229            filter_advisor: None,
1230
1231            compaction_filter_factory: None,
1232            merge_operator: None,
1233
1234            prefix_extractor: None,
1235            key_domains: None,
1236
1237            expect_point_read_hits: false,
1238
1239            compaction_output_cuts: OutputCuts::Aligned,
1240
1241            page_ecc: false,
1242
1243            initial_runtime_config: crate::runtime_config::RuntimeConfig::default(),
1244
1245            kv_separation_opts: None,
1246
1247            #[cfg(zstd_any)]
1248            zstd_dictionary: None,
1249            #[cfg(zstd_any)]
1250            zstd_dictionaries: Arc::new(arc_swap::ArcSwap::from_pointee(
1251                crate::compression::ZstdDictionaries::new(),
1252            )),
1253
1254            comparator: comparator::default_comparator(),
1255            encryption: None,
1256            clock: None,
1257            manifest_recovery_mode: ManifestRecoveryMode::AbsoluteConsistency,
1258            sync_mode: SyncMode::Normal,
1259            flush_durability: FlushDurability::Synced,
1260            writeback_bytes: 1_024 * 1_024,
1261            table_target_size: DEFAULT_TABLE_TARGET_SIZE,
1262            directory_lock: true,
1263            #[cfg(feature = "std")]
1264            recovery_progress: None,
1265            manifest_log_rotate_bytes: 1024 * 1024,
1266            table_file_pool: crate::table::file_pool::DEFAULT_TABLE_FILE_POOL,
1267            table_file_refill: crate::table::file_pool::RefillOn::Background,
1268            repair_retention_floor: 0,
1269            compaction_rate_limit: 0,
1270            compaction_rate_limiter: None,
1271            live_compaction_rate_limiter: None,
1272            compaction_capacity: None,
1273            write_rate_limit: 0,
1274            write_rate_limiter: None,
1275            #[cfg(feature = "std")]
1276            write_buffer_budget: None,
1277
1278            #[cfg(feature = "std")]
1279            compaction_threads: std::thread::available_parallelism()
1280                .map_or(1, |n| (n.get() / 2).max(1)),
1281            #[cfg(feature = "std")]
1282            compaction_pool: None,
1283            #[cfg(feature = "std")]
1284            parallel_compression_inline_below: None,
1285            #[cfg(feature = "std")]
1286            subcompaction_min_bytes: crate::compaction::worker::SUBCOMPACTION_MIN_INPUT_BYTES,
1287            #[cfg(all(test, feature = "std"))]
1288            fail_one_subcompaction: Arc::new(core::sync::atomic::AtomicBool::new(false)),
1289            #[cfg(all(test, feature = "std"))]
1290            stop_serial_merge_after_first_item: Arc::new(core::sync::atomic::AtomicBool::new(
1291                false,
1292            )),
1293            #[cfg(all(test, feature = "std"))]
1294            fail_tight_after_first_slice: Arc::new(core::sync::atomic::AtomicBool::new(false)),
1295            #[cfg(all(test, feature = "std"))]
1296            tight_slice_squeeze: Arc::new(core::sync::atomic::AtomicU64::new(0)),
1297            #[cfg(all(test, feature = "std"))]
1298            fail_tight_blob_reopen: Arc::new(core::sync::atomic::AtomicBool::new(false)),
1299            #[cfg(all(test, feature = "std"))]
1300            before_output_install: Arc::new(std::sync::Mutex::new(None)),
1301            #[cfg(all(test, feature = "std"))]
1302            while_outputs_finish: Arc::new(std::sync::Mutex::new(None)),
1303        }
1304    }
1305}
1306
1307/// The callback the [`Config::before_output_install`] failpoint runs.
1308#[cfg(all(test, feature = "std"))]
1309pub(crate) type OutputInstallHook = Box<dyn FnOnce() + Send>;
1310
1311#[cfg(all(test, feature = "std"))]
1312impl Config {
1313    /// Arms the [`Self::before_output_install`] failpoint with `hook`.
1314    pub(crate) fn arm_before_output_install(&self, hook: impl FnOnce() + Send + 'static) {
1315        #[expect(clippy::unwrap_used, reason = "test-only seam")]
1316        self.before_output_install
1317            .lock()
1318            .unwrap()
1319            .replace(Box::new(hook));
1320    }
1321
1322    /// Runs the [`Self::before_output_install`] failpoint once, if armed.
1323    pub(crate) fn fire_before_output_install(&self) {
1324        #[expect(clippy::unwrap_used, reason = "test-only seam")]
1325        let hook = self.before_output_install.lock().unwrap().take();
1326        if let Some(hook) = hook {
1327            hook();
1328        }
1329    }
1330
1331    /// Arms the [`Self::while_outputs_finish`] failpoint with `hook`.
1332    pub(crate) fn arm_while_outputs_finish(&self, hook: impl FnOnce() + Send + 'static) {
1333        #[expect(clippy::unwrap_used, reason = "test-only seam")]
1334        self.while_outputs_finish
1335            .lock()
1336            .unwrap()
1337            .replace(Box::new(hook));
1338    }
1339
1340    /// Runs the [`Self::while_outputs_finish`] failpoint once, if armed.
1341    pub(crate) fn fire_while_outputs_finish(&self) {
1342        #[expect(clippy::unwrap_used, reason = "test-only seam")]
1343        let hook = self.while_outputs_finish.lock().unwrap().take();
1344        if let Some(hook) = hook {
1345            hook();
1346        }
1347    }
1348}
1349
1350/// Name of the lock file created in a tree directory for the cross-process
1351/// exclusive directory lock.
1352#[cfg_attr(
1353    not(feature = "std"),
1354    allow(
1355        dead_code,
1356        reason = "directory-lock filename used only by the std-gated lock-acquisition path"
1357    )
1358)]
1359pub(crate) const DIRECTORY_LOCK_FILE: &str = "LOCK";
1360
1361/// How an open comes by the directory lock.
1362#[cfg(feature = "std")]
1363pub(crate) enum DirectoryLock {
1364    /// The open takes it.
1365    Acquire,
1366    /// The caller already holds it (`None` when the config disables the lock),
1367    /// having written the tree's files under it.
1368    Held(Option<Box<dyn crate::fs::FsFile>>),
1369}
1370
1371/// Acquires the cross-process exclusive directory lock when `enabled`.
1372///
1373/// Opens (creating if absent) a `LOCK` file under `dir` and takes a
1374/// non-blocking exclusive advisory lock on it through the `Fs` backend. Returns
1375/// the locked handle to hold for as long as exclusivity is required; dropping it
1376/// releases the lock (the OS frees an advisory lock when the fd / handle
1377/// closes). `Ok(None)` when `enabled` is false. Fails with
1378/// [`Error::Locked`](crate::Error::Locked) when another live instance holds the
1379/// lock. The directory must already exist (the caller creates it for a fresh
1380/// tree before acquiring).
1381#[cfg(feature = "std")]
1382pub(crate) fn acquire_directory_lock(
1383    fs: &dyn Fs,
1384    dir: &Path,
1385    enabled: bool,
1386) -> crate::Result<Option<Box<dyn crate::fs::FsFile>>> {
1387    if !enabled {
1388        return Ok(None);
1389    }
1390    let lock_path = dir.join(DIRECTORY_LOCK_FILE);
1391    let file = fs.open(
1392        &lock_path,
1393        &crate::fs::FsOpenOptions::new()
1394            .read(true)
1395            .write(true)
1396            .create(true)
1397            .write_origin(crate::fs::WriteOrigin::Metadata),
1398    )?;
1399    if file.try_lock_exclusive()? {
1400        Ok(Some(file))
1401    } else {
1402        Err(crate::Error::Locked(dir.display().to_string()))
1403    }
1404}
1405
1406impl Config {
1407    /// Initializes a new config
1408    // std-only: seeds the remaining fields from `Config::default`, whose
1409    // default `Fs` is `StdFs`. no_std callers build `Config` field-by-field
1410    // with a caller-provided `Fs`.
1411    #[cfg(feature = "std")]
1412    pub fn new<P: AsRef<Path>>(
1413        path: P,
1414        seqno: SequenceNumberCounter,
1415        visible_seqno: SequenceNumberCounter,
1416    ) -> Self {
1417        Self {
1418            path: absolute_path(path.as_ref()),
1419            seqno: Arc::new(seqno),
1420            visible_seqno: Arc::new(visible_seqno),
1421            ..Default::default()
1422        }
1423    }
1424
1425    /// Sets the default filesystem backend used for levels without an explicit route.
1426    ///
1427    /// Defaults to [`StdFs`]. Use [`MemFs`](crate::fs::MemFs) for
1428    /// in-memory trees (testing, ephemeral indexes).
1429    ///
1430    /// # Example
1431    ///
1432    /// ```
1433    /// # fn main() -> lsm_tree::Result<()> {
1434    /// use lsm_tree::{Config, SequenceNumberCounter};
1435    /// use lsm_tree::fs::MemFs;
1436    ///
1437    /// let tree = Config::new(
1438    ///     "/virtual/tree",
1439    ///     SequenceNumberCounter::default(),
1440    ///     SequenceNumberCounter::default(),
1441    /// )
1442    /// .with_fs(MemFs::new())
1443    /// .open()?;
1444    /// # Ok(())
1445    /// # }
1446    /// ```
1447    #[must_use]
1448    pub fn with_fs<F: Fs>(mut self, fs: F) -> Self {
1449        self.fs = Arc::new(fs);
1450        self
1451    }
1452
1453    /// Sets the default filesystem backend from an existing shared handle.
1454    ///
1455    /// Useful when multiple configs should reuse the same backend
1456    /// instance, including trait objects and backends that are not `Clone`.
1457    ///
1458    #[must_use]
1459    pub fn with_shared_fs(mut self, fs: Arc<dyn Fs>) -> Self {
1460        self.fs = fs;
1461        self
1462    }
1463
1464    /// Opens a tree using the config.
1465    ///
1466    /// # Errors
1467    ///
1468    /// Will return `Err` if an IO error occurs.
1469    /// Returns [`Error::ZstdDictMismatch`](crate::Error::ZstdDictMismatch) if
1470    /// the compression policy references a `dict_id` that doesn't match the
1471    /// configured dictionary.
1472    pub fn open(self) -> crate::Result<AnyTree> {
1473        self.open_with(
1474            #[cfg(feature = "std")]
1475            DirectoryLock::Acquire,
1476            crate::tree::NewFolderNames::Synced,
1477        )
1478        .map(|(tree, _)| tree)
1479    }
1480
1481    /// Opens a tree for each config, making each one that does not exist
1482    /// yet, and makes the new trees' folder names durable with one sync of
1483    /// each folder that holds them, after all of them are made, rather than
1484    /// one sync of it per tree. A consumer that makes many trees in one data
1485    /// directory at once pays one device flush for that directory.
1486    ///
1487    /// The trees come back in the order of `configs`. They are durable once
1488    /// this returns: before that a power loss may take back any new tree of
1489    /// the batch, with everything in it. A batch that fails takes back every
1490    /// tree it made and closes the ones it opened, so a retry finds what was
1491    /// there before it.
1492    ///
1493    /// # Errors
1494    ///
1495    /// As [`Self::open`], for the first config whose tree fails to open, or a
1496    /// failed sync of a folder holding a new tree's name.
1497    ///
1498    /// # Examples
1499    ///
1500    /// ```
1501    /// use lsm_tree::fs::MemFs;
1502    /// use lsm_tree::{AbstractTree, Config, SequenceNumberCounter};
1503    /// use std::sync::Arc;
1504    ///
1505    /// let fs = Arc::new(MemFs::new());
1506    /// let config = |name: &str| {
1507    ///     Config::new(
1508    ///         format!("/data/{name}"),
1509    ///         SequenceNumberCounter::default(),
1510    ///         SequenceNumberCounter::default(),
1511    ///     )
1512    ///     .with_shared_fs(fs.clone())
1513    /// };
1514    /// let trees = Config::open_many([config("nodes"), config("edges")])?;
1515    /// trees[0].insert("a", "node", 0);
1516    /// assert!(trees[1].is_empty(lsm_tree::SeqNo::MAX, None)?);
1517    /// # Ok::<(), lsm_tree::Error>(())
1518    /// ```
1519    pub fn open_many(configs: impl IntoIterator<Item = Self>) -> crate::Result<Vec<AnyTree>> {
1520        crate::tree::open_batch(configs)
1521    }
1522
1523    /// [`Self::open`], coming by the directory lock as `lock` says, and
1524    /// leaving the new tree folder's name to its caller to sync as `names`
1525    /// says; with the tree, when this open made it, what takes it back.
1526    pub(crate) fn open_with(
1527        self,
1528        #[cfg(feature = "std")] lock: DirectoryLock,
1529        names: crate::tree::NewFolderNames,
1530    ) -> crate::Result<(AnyTree, Option<crate::tree::Creation>)> {
1531        // The dictionary registry is loaded and the compression policy validated
1532        // inside `Tree::open`, under the directory lock — NOT here. Both read the
1533        // tree's `dicts/` folder, and the load SWEEPS unpublished temps; doing
1534        // that before the lock lets an opener that is about to fail with
1535        // `Locked` delete a live owner's in-flight registration.
1536
1537        // On a zstd build the live block path seals encrypted blocks through
1538        // the AAD-bound envelope, so the configured provider MUST implement it.
1539        // Reject an opaque-only provider here, at open time, instead of letting
1540        // it fail on the first encrypted read/write.
1541        #[cfg(zstd_any)]
1542        if self
1543            .encryption
1544            .as_ref()
1545            .is_some_and(|enc| !enc.supports_aad_block_path())
1546        {
1547            return Err(crate::Error::Encrypt(
1548                "encryption provider does not implement the AAD-bound block path \
1549                 (encrypt_block_aad / decrypt_block_aad) required for encrypted \
1550                 blocks on a zstd build",
1551            ));
1552        }
1553
1554        Ok(if self.kv_separation_opts.is_some() {
1555            let (tree, creation) = BlobTree::open(
1556                self,
1557                #[cfg(feature = "std")]
1558                lock,
1559                names,
1560            )?;
1561            (AnyTree::Blob(tree), creation)
1562        } else {
1563            let (tree, creation) = Tree::open(
1564                self,
1565                #[cfg(feature = "std")]
1566                lock,
1567                names,
1568            )?;
1569            (AnyTree::Standard(tree), creation)
1570        })
1571    }
1572
1573    /// Builds a tree at [`Self::path`] from a [`PinnedVersion`](crate::PinnedVersion)
1574    /// taken elsewhere, and opens it.
1575    ///
1576    /// Before the call, the pin's files lie in the tree's folders under their
1577    /// ids: each of [`tables`](crate::PinnedVersion::tables) as `tables/{id}`
1578    /// (with its [`heal_attestation`](crate::PinnedFile::heal_attestation),
1579    /// when it has one, as `tables/{id}.heal-attest`), each of
1580    /// [`blob_files`](crate::PinnedVersion::blob_files) as `blobs/{id}`, each of
1581    /// [`dictionaries`](crate::PinnedVersion::dictionaries) as `dicts/{id}`.
1582    /// `layout` is the pin's [`layout`](crate::PinnedVersion::layout), and
1583    /// `entries` and `range_tombstones` are its
1584    /// [`memtable_entries`](crate::PinnedVersion::memtable_entries) and
1585    /// [`memtable_range_tombstones`](crate::PinnedVersion::memtable_range_tombstones).
1586    ///
1587    /// The directory lock is taken first and held into the open, so no other
1588    /// opener sees the folder half restored. Until the restored memtable
1589    /// contents are durable a `restoring` marker lies in the folder: an
1590    /// ordinary [`Self::open`] refuses it, and calling this again with the
1591    /// same pin finishes an interrupted restore. The files are synced and the
1592    /// layout published as the tree's manifest, then the memtable contents are
1593    /// written with their sequence numbers and flushed. The returned tree reads
1594    /// as the source did at the pin, for every key and every snapshot the
1595    /// source served then. The config's sequence number counters are raised to
1596    /// the pin's seqno (and the seqno counter past every restored version), so
1597    /// later writes land above the restored data. The config must match the
1598    /// source's where it shapes the files: the tree type, comparator,
1599    /// encryption provider and dictionaries.
1600    ///
1601    /// # Errors
1602    ///
1603    /// [`Error::InvalidHeader`](crate::Error::InvalidHeader) for a layout too
1604    /// short to hold its header; [`Error::Locked`](crate::Error::Locked) when
1605    /// another instance holds the folder; an `AlreadyExists` I/O error, before
1606    /// anything is written, when the folder already holds a tree (a manifest
1607    /// snapshot or edit log) that no interrupted restore left; otherwise
1608    /// the failures of the syncs, the
1609    /// manifest write, [`Self::open`] and the flush.
1610    #[cfg(feature = "std")]
1611    pub fn restore_pinned(
1612        self,
1613        layout: &[u8],
1614        entries: impl IntoIterator<Item = crate::PinnedEntry>,
1615        range_tombstones: impl IntoIterator<Item = crate::PinnedRangeTombstone>,
1616    ) -> crate::Result<AnyTree> {
1617        crate::pin::restore(self, layout, entries, range_tombstones)
1618    }
1619
1620    /// A snapshot of the dictionaries the tree can currently decompress
1621    /// against.
1622    ///
1623    /// One load per table open, which is where the set is needed; the returned
1624    /// value is a handle (one `Arc`), not a copy of the dictionaries.
1625    #[cfg(zstd_any)]
1626    pub(crate) fn current_zstd_dictionaries(&self) -> crate::compression::ZstdDictionaries {
1627        self.zstd_dictionaries.load().as_ref().clone()
1628    }
1629
1630    /// Gives this config a registry of its OWN, holding every dictionary the
1631    /// tree at [`Self::path`] stores plus the ones supplied through
1632    /// [`Self::zstd_dictionary`] and the KV-separation options.
1633    ///
1634    /// Every path that opens a table has to call this first: a table resolves
1635    /// the dictionary id it recorded against this set, so an unloaded one fails
1636    /// every dictionary-compressed table in the tree.
1637    ///
1638    /// The registry is REPLACED rather than stored into, because `Config` is
1639    /// `Clone` and a keyspace derives one config per partition from a base. The
1640    /// clones would otherwise share a single set, and the second tree's load
1641    /// would take over the first tree's registry — after which the first tree
1642    /// stops resolving the tables it wrote itself.
1643    ///
1644    /// # Errors
1645    ///
1646    /// Propagates the folder scan and the store's integrity check: a dictionary
1647    /// whose bytes no longer hash to its name fails the open rather than
1648    /// dropping silently out of the set.
1649    #[cfg(zstd_any)]
1650    pub(crate) fn install_own_zstd_dictionaries(&mut self) -> crate::Result<()> {
1651        let folder = self.path.join(crate::file::DICTS_FOLDER);
1652        // A `.tmp` is an unpublished registration referenced by no version, so a
1653        // crashed one is swept rather than left to linger.
1654        crate::dicts::sweep_temps(&*self.fs, &folder)?;
1655
1656        let dicts = crate::dicts::read_all(&*self.fs, &folder, self.encryption.as_deref())?;
1657        self.install_zstd_dictionary_set(dicts);
1658        Ok(())
1659    }
1660
1661    /// [`Self::install_own_zstd_dictionaries`] for a repair: a dictionary whose
1662    /// bytes no longer hash to its name is left out of the set instead of
1663    /// failing the load, and its id is returned. Nothing is changed on disk.
1664    ///
1665    /// # Errors
1666    ///
1667    /// Propagates the folder scan and every read failure other than the
1668    /// integrity check.
1669    #[cfg(zstd_any)]
1670    pub(crate) fn install_own_zstd_dictionaries_skipping_damaged(
1671        &mut self,
1672    ) -> crate::Result<alloc::vec::Vec<crate::file::DictId>> {
1673        let folder = self.path.join(crate::file::DICTS_FOLDER);
1674        crate::dicts::sweep_temps(&*self.fs, &folder)?;
1675
1676        let (dicts, damaged) = crate::dicts::read_all_skipping_damaged(
1677            &*self.fs,
1678            &folder,
1679            self.encryption.as_deref(),
1680        )?;
1681        self.install_zstd_dictionary_set(dicts);
1682        Ok(damaged)
1683    }
1684
1685    /// Joins the supplied dictionaries to `dicts` and installs it as this
1686    /// config's own registry.
1687    ///
1688    /// The supplied ones join the SAME set the tree's stored ones are in, and
1689    /// every read and write resolves out of it by id: a table or blob file by
1690    /// the id it recorded, a writer by the id its policy names.
1691    #[cfg(zstd_any)]
1692    fn install_zstd_dictionary_set(&mut self, mut dicts: crate::compression::ZstdDictionaries) {
1693        if let Some(supplied) = self.zstd_dictionary.clone() {
1694            dicts = dicts.with(supplied);
1695        }
1696        if let Some(supplied) = self
1697            .kv_separation_opts
1698            .as_ref()
1699            .and_then(|o| o.zstd_dictionary.clone())
1700        {
1701            dicts = dicts.with(supplied);
1702        }
1703        self.zstd_dictionaries = Arc::new(arc_swap::ArcSwap::from_pointee(dicts));
1704    }
1705
1706    /// Like [`Config::new`], but accepts pre-built shared generators.
1707    ///
1708    /// This is useful when the caller already has
1709    /// [`SharedSequenceNumberGenerator`] instances (e.g., from a higher-level
1710    /// database that shares generators across multiple trees).
1711    // std-only: see [`Config::new`] — seeds via `Config::default` (`StdFs`).
1712    #[cfg(feature = "std")]
1713    pub fn new_with_generators<P: AsRef<Path>>(
1714        path: P,
1715        seqno: SharedSequenceNumberGenerator,
1716        visible_seqno: SharedSequenceNumberGenerator,
1717    ) -> Self {
1718        Self {
1719            path: absolute_path(path.as_ref()),
1720            seqno,
1721            visible_seqno,
1722            ..Default::default()
1723        }
1724    }
1725}
1726
1727#[cfg(all(test, zstd_any))]
1728mod tests;
1729
1730impl Config {
1731    /// The [`level_routes`](Self::level_routes) entry covering `level`, if any.
1732    ///
1733    /// The single place the level→route lookup lives, so the path a level's
1734    /// tables are written to and the backend its reads are submitted to cannot
1735    /// disagree about which route covers it.
1736    fn route_for_level(&self, level: u8) -> Option<&LevelRoute> {
1737        self.level_routes
1738            .as_ref()?
1739            .iter()
1740            .find(|route| route.levels.contains(&level))
1741    }
1742
1743    /// Returns the tables folder path and [`Fs`] backend for the given level.
1744    ///
1745    /// If [`level_routes`](Self::level_routes) has an entry covering this
1746    /// level, uses that entry's path and `Fs`. Otherwise falls back to the
1747    /// primary [`path`](Self::path) and [`fs`](Self::fs).
1748    #[must_use]
1749    pub fn tables_folder_for_level(&self, level: u8) -> (PathBuf, Arc<dyn Fs>) {
1750        match self.route_for_level(level) {
1751            Some(route) => (route.path.join(TABLES_FOLDER), route.fs.clone()),
1752            None => (self.path.join(TABLES_FOLDER), self.fs.clone()),
1753        }
1754    }
1755
1756    /// Best-effort minimum free space (bytes) across every filesystem this tree
1757    /// writes to: the primary [`path`](Self::path) plus each
1758    /// [`level_routes`](Self::level_routes) volume.
1759    ///
1760    /// The tightest volume bounds storage admission and compaction space gating,
1761    /// since a full routed (cold-tier) volume fails a flush / compaction
1762    /// targeting it even while the primary still has room. A backend that cannot
1763    /// report free space (or an I/O hiccup) contributes `u64::MAX`, so a probe
1764    /// failure never fabricates disk pressure.
1765    #[must_use]
1766    pub(crate) fn min_available_space(&self) -> u64 {
1767        let mut free = self.fs.available_space(&self.path).unwrap_or(u64::MAX);
1768        if let Some(routes) = &self.level_routes {
1769            for route in routes {
1770                free = free.min(route.fs.available_space(&route.path).unwrap_or(u64::MAX));
1771            }
1772        }
1773        free
1774    }
1775
1776    /// Returns all unique tables folders that need to be scanned during
1777    /// recovery: the primary folder plus every [`LevelRoute`] folder.
1778    #[must_use]
1779    pub fn all_tables_folders(&self) -> Vec<(PathBuf, Arc<dyn Fs>)> {
1780        let primary_fs: Arc<dyn Fs> = self.fs.clone();
1781        let mut folders: Vec<(PathBuf, Arc<dyn Fs>)> =
1782            vec![(self.path.join(TABLES_FOLDER), primary_fs)];
1783
1784        if let Some(routes) = &self.level_routes {
1785            for route in routes {
1786                let folder = route.path.join(TABLES_FOLDER);
1787                // Dedup by path: scanning the same directory twice would cause
1788                // already-recovered tables to be classified as orphans and
1789                // deleted. Routing the same path through different Fs backends
1790                // is a configuration error (level_routes validation in
1791                // Config::level_routes rejects overlapping ranges).
1792                if !folders.iter().any(|(p, _)| *p == folder) {
1793                    folders.push((folder, route.fs.clone()));
1794                }
1795            }
1796        }
1797
1798        folders
1799    }
1800
1801    /// Configures per-level filesystem routing for tiered storage.
1802    ///
1803    /// Each [`LevelRoute`] maps a range of LSM levels to a base directory
1804    /// and filesystem backend. Levels not covered by any route fall back to
1805    /// the primary `path` and `fs`.
1806    ///
1807    /// # Reopen contract
1808    ///
1809    /// The route configuration is **not persisted** in the manifest.
1810    /// On reopen, the [`Config`] must specify `level_routes` such that
1811    /// [`all_tables_folders`](Self::all_tables_folders) includes every
1812    /// directory and filesystem pair that may contain existing SST files
1813    /// for this tree.
1814    ///
1815    /// Changing the mapping from levels to paths is allowed as long as
1816    /// the previously used folders remain covered. If old folders are
1817    /// omitted, recovery may fail with
1818    /// [`RouteMismatch`](crate::Error::RouteMismatch) (when all missing
1819    /// tables are on uncovered levels) or
1820    /// [`Unrecoverable`](crate::Error::Unrecoverable) (when some missing
1821    /// tables are on levels that are still covered).
1822    ///
1823    /// # Panics
1824    ///
1825    /// Panics if any route has an empty range or if any two routes have
1826    /// overlapping level ranges.
1827    #[must_use]
1828    pub fn level_routes(mut self, routes: Vec<LevelRoute>) -> Self {
1829        // Validate no empty/inverted ranges
1830        for route in &routes {
1831            assert!(
1832                route.levels.start < route.levels.end,
1833                "empty or inverted level route range: {:?}",
1834                route.levels,
1835            );
1836        }
1837
1838        // Validate no overlapping ranges
1839        for (i, a) in routes.iter().enumerate() {
1840            for b in routes.iter().skip(i + 1) {
1841                assert!(
1842                    a.levels.end <= b.levels.start || b.levels.end <= a.levels.start,
1843                    "overlapping level routes: {:?} and {:?}",
1844                    a.levels,
1845                    b.levels,
1846                );
1847            }
1848        }
1849        self.level_routes = if routes.is_empty() {
1850            None
1851        } else {
1852            // Normalize paths the same way Config::new normalizes self.path
1853            Some(
1854                routes
1855                    .into_iter()
1856                    .map(|mut r| {
1857                        r.path = absolute_path(&r.path);
1858                        r
1859                    })
1860                    .collect(),
1861            )
1862        };
1863        self
1864    }
1865
1866    /// Overrides the sequence number generator.
1867    ///
1868    /// By default, [`SequenceNumberCounter`] is used. This allows plugging in
1869    /// a custom generator (e.g., HLC for distributed databases).
1870    #[must_use]
1871    pub fn seqno_generator(mut self, generator: SharedSequenceNumberGenerator) -> Self {
1872        self.seqno = generator;
1873        self
1874    }
1875
1876    /// Overrides the visible sequence number generator.
1877    #[must_use]
1878    pub fn visible_seqno_generator(mut self, generator: SharedSequenceNumberGenerator) -> Self {
1879        self.visible_seqno = generator;
1880        self
1881    }
1882
1883    /// Sets the global cache.
1884    ///
1885    /// You can create a global [`Cache`] and share it between multiple
1886    /// trees to cap global cache memory usage.
1887    ///
1888    /// Defaults to a cache with 16 MiB of capacity *per tree*.
1889    #[must_use]
1890    pub fn use_cache(mut self, cache: Arc<Cache>) -> Self {
1891        self.cache = cache;
1892        self
1893    }
1894
1895    /// Sets the file descriptor cache.
1896    ///
1897    /// Can be shared across trees.
1898    #[must_use]
1899    pub fn use_descriptor_table(mut self, descriptor_table: Option<Arc<DescriptorTable>>) -> Self {
1900        self.descriptor_table = descriptor_table;
1901        self
1902    }
1903
1904    /// If `true`, the last level will not build filters, reducing the filter size of a database
1905    /// by ~90% typically.
1906    ///
1907    /// **Enable this only if you know that point reads generally are expected to find a key-value pair.**
1908    #[must_use]
1909    pub fn expect_point_read_hits(mut self, b: bool) -> Self {
1910        self.expect_point_read_hits = b;
1911        self
1912    }
1913
1914    /// Whether a compaction ends its outputs on the boundaries between the
1915    /// tables of the level they are merged into next.
1916    ///
1917    /// An output straddling such a boundary drags both tables into that next
1918    /// merge, which reads and rewrites the extra one. Aligned, an output ends
1919    /// at the first boundary past half the target size; each boundary it
1920    /// passes without ending raises that share by five percent, up to ninety;
1921    /// it ends at twice the target whatever the boundaries, and at the target
1922    /// when no boundary is left ahead. A user key's versions stay in one output
1923    /// above every size bound. Off, outputs end at the target size alone.
1924    ///
1925    /// Default = `true`.
1926    ///
1927    /// # Examples
1928    ///
1929    /// ```
1930    /// # let folder = tempfile::tempdir()?;
1931    /// use lsm_tree::{Config, SequenceNumberCounter};
1932    ///
1933    /// let config = Config::new(
1934    ///     folder,
1935    ///     SequenceNumberCounter::default(),
1936    ///     SequenceNumberCounter::default(),
1937    /// )
1938    /// .compaction_output_alignment(false);
1939    /// # let _ = config;
1940    /// # Ok::<(), Box<dyn std::error::Error>>(())
1941    /// ```
1942    #[must_use]
1943    pub fn compaction_output_alignment(mut self, enabled: bool) -> Self {
1944        self.compaction_output_cuts = if enabled {
1945            OutputCuts::Aligned
1946        } else {
1947            OutputCuts::TargetSize
1948        };
1949        self
1950    }
1951
1952    /// Enables per-block Page ECC.
1953    ///
1954    /// When enabled, every block written by this tree carries a parity
1955    /// trailer; on read, if the block's XXH3 disagrees with the on-disk
1956    /// bytes, the reader attempts recovery from the trailer before surfacing
1957    /// the corruption. The correction scheme defaults to per-word SEC-DED and
1958    /// is selectable at runtime (`update_runtime_config`): per-word SEC-DED,
1959    /// single XOR parity, or Reed-Solomon.
1960    ///
1961    /// Opening a tree with `page_ecc = true` on a build that does not
1962    /// have the `page_ecc` cargo feature enabled returns
1963    /// [`crate::Error::PageEccUnsupported`] at `Tree::open` — the
1964    /// reader has no way to honour the parity trailer without the
1965    /// codec, so silently downgrading integrity is not an option.
1966    ///
1967    /// Wired into the on-disk write path via `MultiWriter::use_page_ecc`
1968    /// at every `Tree::open` / `Tree::ingestion` / compaction-worker
1969    /// `MultiWriter` construction site. With this flag set, every
1970    /// `Block::write_into` call those writers make upgrades its
1971    /// `BlockTransform` to the matching `*Ecc` variant — emitting the
1972    /// configured scheme's parity trailer and setting the `ECC_PARITY` flag
1973    /// in each block header (the trailer length is derived from
1974    /// `data_length`, not stored).
1975    #[must_use]
1976    pub fn page_ecc(mut self, enabled: bool) -> Self {
1977        self.page_ecc = enabled;
1978        self
1979    }
1980
1981    /// Enables or disables the cross-process directory lock (default: enabled).
1982    ///
1983    /// When enabled, [`Config::open`] and [`Config::repair`] acquire an
1984    /// exclusive advisory lock on a `LOCK` file in the tree directory, so a
1985    /// second process opening / repairing the same directory fails fast with
1986    /// [`Error::Locked`](crate::Error::Locked) rather than corrupting the shared
1987    /// manifest. Disable ONLY when exclusive directory ownership is already
1988    /// guaranteed at a higher layer (e.g. an embedding keyspace / journal
1989    /// manager that opens each directory at most once per host).
1990    #[must_use]
1991    pub fn with_directory_lock(mut self, enabled: bool) -> Self {
1992        self.directory_lock = enabled;
1993        self
1994    }
1995
1996    /// Wires shared live-progress counters into the repair / salvage paths.
1997    ///
1998    /// A repair over a large store streams every SST and blob file and can run
1999    /// for a long time; the handle set here is ticked as files are discovered
2000    /// and blocks / rows are recovered, so another thread can poll
2001    /// [`RecoveryProgress::snapshot`](crate::RecoveryProgress::snapshot) while
2002    /// [`Config::repair`] (or a salvage it triggers) runs. Without it, repair
2003    /// publishes no progress (zero overhead).
2004    ///
2005    /// # Examples
2006    ///
2007    /// ```no_run
2008    /// use lsm_tree::{Config, RecoveryProgress, SequenceNumberCounter};
2009    /// use std::sync::Arc;
2010    ///
2011    /// let progress = Arc::new(RecoveryProgress::default());
2012    /// let config = Config::new(
2013    ///     "my-tree",
2014    ///     SequenceNumberCounter::default(),
2015    ///     SequenceNumberCounter::default(),
2016    /// )
2017    /// .with_recovery_progress(progress.clone());
2018    /// // spawn the repair, then poll `progress.snapshot()` elsewhere
2019    /// let report = config.repair()?;
2020    /// # Ok::<_, lsm_tree::Error>(())
2021    /// ```
2022    #[cfg(feature = "std")]
2023    #[must_use]
2024    pub fn with_recovery_progress(mut self, progress: Arc<crate::RecoveryProgress>) -> Self {
2025        self.recovery_progress = Some(progress);
2026        self
2027    }
2028
2029    /// Sets the Page ECC scheme used when [`Self::page_ecc`] is enabled.
2030    ///
2031    /// ECC is off until `page_ecc(true)`. When on, this picks the
2032    /// algorithm:
2033    /// [`EccScheme::Secded`](crate::runtime_config::EccScheme::Secded)
2034    /// (per-word single-bit correct / double-bit detect, the default, supported
2035    /// at Block granularity),
2036    /// [`EccScheme::Xor`](crate::runtime_config::EccScheme::Xor) (RAID-5
2037    /// single-parity), or
2038    /// [`EccScheme::ReedSolomon`](crate::runtime_config::EccScheme::ReedSolomon).
2039    /// There is no implicit RS(4,2) default.
2040    #[must_use]
2041    pub fn ecc_scheme(mut self, scheme: crate::runtime_config::EccScheme) -> Self {
2042        self.initial_runtime_config.ecc_scheme = scheme;
2043        self
2044    }
2045
2046    /// Sets whether the writer clears per-file copy-on-write on newly created
2047    /// SST / blob files when the backing filesystem is copy-on-write (Btrfs).
2048    ///
2049    /// Default `true`: write-once SSTs gain no benefit from `CoW` but suffer a
2050    /// fragmentation penalty (~20% write throughput on Btrfs), so clearing it
2051    /// recovers the ext4-equivalent baseline. A no-op on non-`CoW` filesystems.
2052    /// Set `false` to preserve `CoW` (e.g. Btrfs subvolume snapshots that depend
2053    /// on it). See [`crate::runtime_config::RuntimeConfig::disable_cow_on_sst_files`].
2054    #[must_use]
2055    pub fn disable_cow_on_sst_files(mut self, enabled: bool) -> Self {
2056        self.initial_runtime_config.disable_cow_on_sst_files = enabled;
2057        self
2058    }
2059
2060    /// Sets whether [`crate::AbstractTree::create_checkpoint`] clones files via
2061    /// reflink (`FICLONE` / `clonefile`) when the filesystem supports it,
2062    /// falling back to a hard link otherwise.
2063    ///
2064    /// Default `true`: a reflinked checkpoint has an independent inode (no
2065    /// max-links constraint, modifications never touch the original) at O(1)
2066    /// cost via copy-on-write block sharing. A no-op (hard-link path) on
2067    /// filesystems without reflink. See
2068    /// [`crate::runtime_config::RuntimeConfig::use_reflink_for_checkpoint`].
2069    #[must_use]
2070    pub fn use_reflink_for_checkpoint(mut self, enabled: bool) -> Self {
2071        self.initial_runtime_config.use_reflink_for_checkpoint = enabled;
2072        self
2073    }
2074
2075    /// Sets the initial [`crate::runtime_config::RuntimeConfig`]
2076    /// snapshot the tree will start with.
2077    ///
2078    /// Seeds both the first manifest write and the live
2079    /// `RuntimeConfigHandle` exposed via
2080    /// [`crate::Tree::runtime_config`].
2081    ///
2082    /// Replaces the snapshot, including what the builders that seed it set
2083    /// earlier ([`Self::ecc_scheme`] and the other `RuntimeConfig`-backed
2084    /// builders), EXCEPT the three compression policies: those keep what
2085    /// [`Self::data_block_compression_policy`],
2086    /// [`Self::index_block_compression_policy`] and [`Self::blob_compression`]
2087    /// set, in whichever order they are called, and the policies in `runtime`
2088    /// are ignored. A caller that set its compression through the builders and
2089    /// its runtime toggles through this does not lose the former to the
2090    /// latter's defaults. On a live tree, all three change through
2091    /// [`crate::Tree::update_runtime_config`].
2092    ///
2093    /// **Manifest-hardening toggles** in the supplied snapshot
2094    /// that are currently wired through the writer
2095    /// (`manifest_footer_mirror`, `page_ecc` *as consumed by
2096    /// `manifest_blocks::writer` when picking the `BlockTransform`
2097    /// variant*) take effect from byte zero of the on-disk
2098    /// manifest rather than waiting for a post-open
2099    /// [`crate::Tree::update_runtime_config`] call. Subsequent
2100    /// updates still flow through the live handle and apply to
2101    /// the next manifest write.
2102    ///
2103    /// `manifest_kv_checksums` is plumbed in the snapshot but the
2104    /// writer does NOT yet consult or persist it (per-entry
2105    /// framing + footer-flag slot land in a follow-up). Setting
2106    /// it here today has no on-disk effect; it is exposed for
2107    /// forward-compat with no behaviour break.
2108    ///
2109    /// **Note on data-block ECC:** `RuntimeConfig::page_ecc`
2110    /// currently affects manifest Blocks only — data-block ECC is
2111    /// still gated by [`Config::page_ecc`] at tree-open time. The
2112    /// SST writer path consumes the tree-static config, not the
2113    /// runtime handle. Wiring through SST emission is a follow-up.
2114    #[must_use]
2115    pub fn with_runtime_config(mut self, runtime: crate::runtime_config::RuntimeConfig) -> Self {
2116        let kept = core::mem::replace(&mut self.initial_runtime_config, runtime);
2117        self.initial_runtime_config.data_block_compression_policy =
2118            kept.data_block_compression_policy;
2119        self.initial_runtime_config.index_block_compression_policy =
2120            kept.index_block_compression_policy;
2121        self.initial_runtime_config.blob_compression = kept.blob_compression;
2122        self
2123    }
2124
2125    /// Sets the partitioning policy for filter blocks.
2126    #[must_use]
2127    pub fn filter_block_partitioning_policy(mut self, policy: PinningPolicy) -> Self {
2128        self.filter_block_partitioning_policy = policy;
2129        self
2130    }
2131
2132    /// Sets the partitioning policy for index blocks.
2133    #[must_use]
2134    pub fn index_block_partitioning_policy(mut self, policy: PinningPolicy) -> Self {
2135        self.index_block_partitioning_policy = policy;
2136        self
2137    }
2138
2139    /// Sets the pinning policy for filter blocks.
2140    #[must_use]
2141    pub fn filter_block_pinning_policy(mut self, policy: PinningPolicy) -> Self {
2142        self.filter_block_pinning_policy = policy;
2143        self
2144    }
2145
2146    /// Sets the pinning policy for index blocks.
2147    #[must_use]
2148    pub fn index_block_pinning_policy(mut self, policy: PinningPolicy) -> Self {
2149        self.index_block_pinning_policy = policy;
2150        self
2151    }
2152
2153    /// Sets the restart interval inside data blocks.
2154    ///
2155    /// A higher restart interval saves space while increasing lookup times
2156    /// inside data blocks.
2157    ///
2158    /// Default = 16
2159    ///
2160    /// # Panics
2161    ///
2162    /// Panics if any restart interval in `policy` is zero.
2163    #[must_use]
2164    pub fn data_block_restart_interval_policy(mut self, policy: RestartIntervalPolicy) -> Self {
2165        assert!(
2166            policy.iter().all(|interval| *interval > 0),
2167            "data block restart interval must be greater than zero",
2168        );
2169        self.data_block_restart_interval_policy = policy;
2170        self
2171    }
2172
2173    /// Sets the restart interval inside index blocks.
2174    ///
2175    /// A higher restart interval saves space while increasing lookup times
2176    /// inside index blocks.
2177    ///
2178    /// Default = 1
2179    ///
2180    /// # Panics
2181    ///
2182    /// Panics if any restart interval in `policy` is zero.
2183    #[must_use]
2184    pub fn index_block_restart_interval_policy(mut self, policy: RestartIntervalPolicy) -> Self {
2185        assert!(
2186            policy.iter().all(|interval| *interval > 0),
2187            "index block restart interval must be greater than zero",
2188        );
2189        self.index_block_restart_interval_policy = policy;
2190        self
2191    }
2192
2193    /// Sets the filter construction policy.
2194    #[must_use]
2195    pub fn filter_policy(mut self, policy: FilterPolicy) -> Self {
2196        self.filter_policy = policy;
2197        self
2198    }
2199
2200    /// Allocates filter memory by measured negative-probe load; see
2201    /// [`FilterAdvisor`]. `None` (the default) leaves every filter to the
2202    /// [`FilterPolicy`] and counts nothing on the read path, so the tables it
2203    /// writes are the ones a tree without the advisor writes.
2204    ///
2205    /// With an advisor, each flush and compaction chooses the width of the
2206    /// filters it writes; [`AbstractTree::filter_memory`](crate::AbstractTree::filter_memory)
2207    /// reports the serialised and resident filter bytes against the budget.
2208    ///
2209    /// # Examples
2210    ///
2211    /// ```
2212    /// use lsm_tree::{Config, config::FilterAdvisor};
2213    ///
2214    /// # let folder = tempfile::tempdir()?;
2215    /// // 64 MiB of filters, widths from 8 to 16 bits per key.
2216    /// let advisor = FilterAdvisor::new(64 << 20).with_bits_per_key([8, 10, 12, 14, 16]);
2217    /// let tree = Config::new(&folder, Default::default(), Default::default())
2218    ///     .filter_advisor(Some(advisor))
2219    ///     .open()?;
2220    /// # Ok::<(), lsm_tree::Error>(())
2221    /// ```
2222    #[must_use]
2223    pub fn filter_advisor(mut self, advisor: Option<FilterAdvisor>) -> Self {
2224        self.filter_advisor = advisor;
2225        self
2226    }
2227
2228    /// Sets the retrieval-ribbon locator policy.
2229    ///
2230    /// On by default at [`LocatorPrecision::Block`] (see
2231    /// [`LocatorPolicy::block_level`]). When enabled for a level, written SSTs on
2232    /// that level carry an optional `locator` section mapping each key to its
2233    /// data block (and, at finer precisions, its slot), letting point reads skip
2234    /// the index-block binary search. Set [`LocatorPolicy::disabled`] to opt out;
2235    /// disabled levels emit byte-identical SSTs.
2236    #[must_use]
2237    pub fn locator_policy(mut self, policy: LocatorPolicy) -> Self {
2238        self.locator_policy = policy;
2239        self
2240    }
2241
2242    /// Sets the compression the tree starts with for data blocks.
2243    ///
2244    /// The initial value of
2245    /// [`RuntimeConfig::data_block_compression_policy`](crate::runtime_config::RuntimeConfig::data_block_compression_policy),
2246    /// which a live tree changes with [`crate::Tree::update_runtime_config`].
2247    #[must_use]
2248    pub fn data_block_compression_policy(mut self, policy: CompressionPolicy) -> Self {
2249        self.initial_runtime_config.data_block_compression_policy = policy;
2250        self
2251    }
2252
2253    /// Sets the compression the tree starts with for index blocks.
2254    ///
2255    /// The initial value of
2256    /// [`RuntimeConfig::index_block_compression_policy`](crate::runtime_config::RuntimeConfig::index_block_compression_policy),
2257    /// which a live tree changes with [`crate::Tree::update_runtime_config`].
2258    #[must_use]
2259    pub fn index_block_compression_policy(mut self, policy: CompressionPolicy) -> Self {
2260        self.initial_runtime_config.index_block_compression_policy = policy;
2261        self
2262    }
2263
2264    /// Sets the compression a KV-separated tree starts with for blob files.
2265    ///
2266    /// The initial value of
2267    /// [`RuntimeConfig::blob_compression`](crate::runtime_config::RuntimeConfig::blob_compression),
2268    /// which a live tree changes with [`crate::BlobTree::update_runtime_config`].
2269    /// A standard tree ignores it.
2270    #[must_use]
2271    pub fn blob_compression(mut self, compression: CompressionType) -> Self {
2272        self.initial_runtime_config.blob_compression = compression;
2273        self
2274    }
2275
2276    /// Sets whether a KV-separated tree starts caching the values it reads
2277    /// from blob files. It does not by default.
2278    ///
2279    /// The initial value of
2280    /// [`RuntimeConfig::blob_cache_fill`](crate::runtime_config::RuntimeConfig::blob_cache_fill),
2281    /// which a live tree changes with [`crate::BlobTree::update_runtime_config`].
2282    /// A standard tree ignores it.
2283    ///
2284    /// # Examples
2285    ///
2286    /// ```
2287    /// use lsm_tree::{Config, KvSeparationOptions, SequenceNumberCounter, config::BlobCacheFill};
2288    ///
2289    /// # let folder = tempfile::tempdir()?;
2290    /// let config = Config::new(&folder, SequenceNumberCounter::default(), SequenceNumberCounter::default())
2291    ///     .with_kv_separation(Some(KvSeparationOptions::default()))
2292    ///     .blob_cache_fill(BlobCacheFill::Always);
2293    /// # Ok::<(), Box<dyn std::error::Error>>(())
2294    /// ```
2295    #[must_use]
2296    pub fn blob_cache_fill(mut self, fill: BlobCacheFill) -> Self {
2297        self.initial_runtime_config.blob_cache_fill = fill;
2298        self
2299    }
2300
2301    // TODO: level count is fixed to 7 right now
2302    // /// Sets the number of levels of the LSM tree (depth of tree).
2303    // ///
2304    // /// Defaults to 7, like `LevelDB` and `RocksDB`.
2305    // ///
2306    // /// Cannot be changed once set.
2307    // ///
2308    // /// # Panics
2309    // ///
2310    // /// Panics if `n` is 0.
2311    // #[must_use]
2312    // pub fn level_count(mut self, n: u8) -> Self {
2313    //     assert!(n > 0);
2314
2315    //     self.level_count = n;
2316    //     self
2317    // }
2318
2319    /// Sets the data block size policy.
2320    #[must_use]
2321    pub fn data_block_size_policy(mut self, policy: BlockSizePolicy) -> Self {
2322        self.data_block_size_policy = policy;
2323        self
2324    }
2325
2326    /// Sets the size a partitioned block index cuts its partitions at, per
2327    /// level. Applies where [`Self::index_block_partitioning_policy`]
2328    /// partitions the index. The default is 4 KiB at every level.
2329    ///
2330    /// # Examples
2331    ///
2332    /// ```
2333    /// use lsm_tree::{Config, SequenceNumberCounter, config::BlockSizePolicy};
2334    /// # let folder = tempfile::tempdir()?;
2335    /// let config = Config::new(
2336    ///     folder.path(),
2337    ///     SequenceNumberCounter::default(),
2338    ///     SequenceNumberCounter::default(),
2339    /// )
2340    /// .index_block_partition_size_policy(BlockSizePolicy::all(16 * 1_024));
2341    /// # Ok::<(), Box<dyn std::error::Error>>(())
2342    /// ```
2343    #[must_use]
2344    pub fn index_block_partition_size_policy(mut self, policy: BlockSizePolicy) -> Self {
2345        self.index_block_partition_size_policy = policy;
2346        self
2347    }
2348
2349    /// Sets the size a partitioned filter cuts its partitions at, per level,
2350    /// in the bytes a partition takes on disk. Applies where
2351    /// [`Self::filter_block_partitioning_policy`] partitions the filter. A
2352    /// point read that misses the block cache loads one partition, so a
2353    /// smaller partition costs less per read and more top-level index
2354    /// entries. The default is 4 KiB at every level.
2355    ///
2356    /// # Examples
2357    ///
2358    /// ```
2359    /// use lsm_tree::{Config, SequenceNumberCounter, config::BlockSizePolicy};
2360    /// # let folder = tempfile::tempdir()?;
2361    /// let config = Config::new(
2362    ///     folder.path(),
2363    ///     SequenceNumberCounter::default(),
2364    ///     SequenceNumberCounter::default(),
2365    /// )
2366    /// .filter_block_partition_size_policy(BlockSizePolicy::all(16 * 1_024));
2367    /// # Ok::<(), Box<dyn std::error::Error>>(())
2368    /// ```
2369    #[must_use]
2370    pub fn filter_block_partition_size_policy(mut self, policy: BlockSizePolicy) -> Self {
2371        self.filter_block_partition_size_policy = policy;
2372        self
2373    }
2374
2375    /// Sets the size a columnar table's row groups are cut at, per level.
2376    ///
2377    /// Applies to the tables written while the runtime `columnar` switch is
2378    /// on; a row-major table cuts its blocks at
2379    /// [`Self::data_block_size_policy`]. The default is
2380    /// [`DEFAULT_COLUMNAR_ROW_GROUP_SIZE`] at every level.
2381    ///
2382    /// # Examples
2383    ///
2384    /// ```
2385    /// use lsm_tree::{Config, SequenceNumberCounter, config::BlockSizePolicy};
2386    /// # let folder = tempfile::tempdir()?;
2387    /// let config = Config::new(
2388    ///     folder.path(),
2389    ///     SequenceNumberCounter::default(),
2390    ///     SequenceNumberCounter::default(),
2391    /// )
2392    /// .columnar_row_group_size_policy(BlockSizePolicy::all(256 * 1_024));
2393    /// # Ok::<(), Box<dyn std::error::Error>>(())
2394    /// ```
2395    #[must_use]
2396    pub fn columnar_row_group_size_policy(mut self, policy: BlockSizePolicy) -> Self {
2397        self.columnar_row_group_size_policy = policy;
2398        self
2399    }
2400
2401    /// Sets the size a columnar row group's rows are cut into row pages at,
2402    /// per level.
2403    ///
2404    /// A row page closes once its rows reach this many uncompressed bytes, so
2405    /// a page holds at least one row and a group at least one page. A size at
2406    /// or above the row group size writes one row page per group the writer
2407    /// closes at its size; a group an ingested batch took past it is still cut
2408    /// into pages of this size. The default is [`DEFAULT_COLUMNAR_PAGE_SIZE`]
2409    /// at every level, four row pages to a group of the default size.
2410    ///
2411    /// # Examples
2412    ///
2413    /// ```
2414    /// use lsm_tree::{Config, SequenceNumberCounter, config::BlockSizePolicy};
2415    /// # let folder = tempfile::tempdir()?;
2416    /// let config = Config::new(
2417    ///     folder.path(),
2418    ///     SequenceNumberCounter::default(),
2419    ///     SequenceNumberCounter::default(),
2420    /// )
2421    /// .columnar_row_group_size_policy(BlockSizePolicy::all(256 * 1_024))
2422    /// .columnar_page_size_policy(BlockSizePolicy::all(8 * 1_024));
2423    /// # Ok::<(), Box<dyn std::error::Error>>(())
2424    /// ```
2425    #[must_use]
2426    pub fn columnar_page_size_policy(mut self, policy: BlockSizePolicy) -> Self {
2427        self.columnar_page_size_policy = policy;
2428        self
2429    }
2430
2431    /// Sets how a columnar table's column pages store their values, per
2432    /// level.
2433    ///
2434    /// The default is [`ColumnEncoding::Plain`] at every level: a read serves
2435    /// a plain page as a view of itself, while an encoded one is built back
2436    /// into its layout by each read that wants the column whole. Choose
2437    /// [`ColumnEncoding::Auto`] for the levels whose tables are mostly stored
2438    /// rather than read, as a stronger compression is chosen for them. A table
2439    /// reads the same under either; only what its pages hold differs.
2440    ///
2441    /// # Examples
2442    ///
2443    /// ```
2444    /// use lsm_tree::{
2445    ///     Config, SequenceNumberCounter,
2446    ///     config::{ColumnEncoding, ColumnEncodingPolicy},
2447    /// };
2448    /// # let folder = tempfile::tempdir()?;
2449    /// // Plain in the first three levels, encoded below them.
2450    /// let config = Config::new(
2451    ///     folder.path(),
2452    ///     SequenceNumberCounter::default(),
2453    ///     SequenceNumberCounter::default(),
2454    /// )
2455    /// .column_encoding_policy(ColumnEncodingPolicy::new([
2456    ///     ColumnEncoding::Plain,
2457    ///     ColumnEncoding::Plain,
2458    ///     ColumnEncoding::Plain,
2459    ///     ColumnEncoding::Auto,
2460    /// ]));
2461    /// # Ok::<(), Box<dyn std::error::Error>>(())
2462    /// ```
2463    #[must_use]
2464    pub fn column_encoding_policy(mut self, policy: ColumnEncodingPolicy) -> Self {
2465        self.column_encoding_policy = policy;
2466        self
2467    }
2468
2469    /// Sets how columnar reads of this tree fetch their pages: the bytes one
2470    /// request may ask for and the requests kept in flight at once. A reader
2471    /// setting, not a format one: a tree reopened under another budget reads
2472    /// the same tables and returns the same rows. The default is
2473    /// [`ReadBudget::default`].
2474    ///
2475    /// # Examples
2476    ///
2477    /// ```
2478    /// use lsm_tree::{Config, SequenceNumberCounter, config::ReadBudget};
2479    /// # let folder = tempfile::tempdir()?;
2480    /// let config = Config::new(
2481    ///     folder.path(),
2482    ///     SequenceNumberCounter::default(),
2483    ///     SequenceNumberCounter::default(),
2484    /// )
2485    /// .columnar_read_budget(ReadBudget::new(64 * 1_024, 4));
2486    /// # Ok::<(), Box<dyn std::error::Error>>(())
2487    /// ```
2488    #[must_use]
2489    pub fn columnar_read_budget(mut self, budget: ReadBudget) -> Self {
2490        self.columnar_read_budget = budget;
2491        self
2492    }
2493
2494    /// Sets the page bytes a projected columnar scan of this tree holds at
2495    /// once. A scan over overlapping segments shares it between them, so a
2496    /// scan that merges more of them reads each in smaller runs of row pages
2497    /// rather than holding proportionally more. A row page is the least a
2498    /// read takes, so one larger than a segment's share is read on its own
2499    /// and counted, not refused; so is a row group a pushed-down predicate
2500    /// reads whole to select its row pages. A reader setting: the rows returned are the
2501    /// same under any budget. The default is [`DEFAULT_COLUMNAR_SCAN_BUDGET`].
2502    ///
2503    /// # Examples
2504    ///
2505    /// ```
2506    /// use lsm_tree::{Config, SequenceNumberCounter};
2507    /// # let folder = tempfile::tempdir()?;
2508    /// let config = Config::new(
2509    ///     folder.path(),
2510    ///     SequenceNumberCounter::default(),
2511    ///     SequenceNumberCounter::default(),
2512    /// )
2513    /// .columnar_scan_budget(256 * 1_024);
2514    /// # Ok::<(), Box<dyn std::error::Error>>(())
2515    /// ```
2516    #[must_use]
2517    pub fn columnar_scan_budget(mut self, bytes: u64) -> Self {
2518        self.columnar_scan_budget = bytes;
2519        self
2520    }
2521
2522    /// Sets the filter and index bytes a multi-get, blocking or resumable,
2523    /// holds at once, in flight or read, across the tables of a level it
2524    /// reads stage by stage. A block
2525    /// in flight counts at the size it is read as, a block read at its decoded
2526    /// size. A table whose blocks would pass it waits until a table before it
2527    /// is planned and lets its blocks go; one that alone passes it is read
2528    /// when no other is in flight. A stage is let in at its on-disk size, so
2529    /// with compressed index blocks the read can go past the budget by what
2530    /// the stages let in together grow when decoded; no further stage is let
2531    /// in until it is back under. Independent of the block cache, so a small
2532    /// cache does not make the tables of a level wait for each other. A reader
2533    /// setting: the values returned are the same under any budget. The
2534    /// default is [`DEFAULT_MULTI_GET_METADATA_BUDGET`].
2535    ///
2536    /// # Examples
2537    ///
2538    /// ```
2539    /// use lsm_tree::{Config, SequenceNumberCounter};
2540    /// # let folder = tempfile::tempdir()?;
2541    /// let config = Config::new(
2542    ///     folder.path(),
2543    ///     SequenceNumberCounter::default(),
2544    ///     SequenceNumberCounter::default(),
2545    /// )
2546    /// .multi_get_metadata_budget(8 * 1_024 * 1_024);
2547    /// # Ok::<(), Box<dyn std::error::Error>>(())
2548    /// ```
2549    #[must_use]
2550    pub fn multi_get_metadata_budget(mut self, bytes: u64) -> Self {
2551        self.multi_get_metadata_budget = bytes;
2552        self
2553    }
2554
2555    /// Sets the hash ratio policy for data blocks.
2556    ///
2557    /// If greater than 0.0, a hash index is embedded into data blocks that can speed up reads
2558    /// inside the data block.
2559    #[must_use]
2560    pub fn data_block_hash_ratio_policy(mut self, policy: HashRatioPolicy) -> Self {
2561        self.data_block_hash_ratio_policy = policy;
2562        self
2563    }
2564
2565    /// Toggles key-value separation.
2566    #[must_use]
2567    pub fn with_kv_separation(mut self, opts: Option<KvSeparationOptions>) -> Self {
2568        self.kv_separation_opts = opts;
2569        self
2570    }
2571
2572    /// Installs a custom compaction filter.
2573    #[must_use]
2574    pub fn with_compaction_filter_factory(mut self, factory: Option<Arc<dyn Factory>>) -> Self {
2575        self.compaction_filter_factory = factory;
2576        self
2577    }
2578
2579    /// Sets the prefix extractor for prefix bloom filters.
2580    ///
2581    /// When configured, bloom filters will index key prefixes returned by
2582    /// the extractor. Prefix scans can then skip segments whose bloom
2583    /// filter reports no match for the scan prefix.
2584    ///
2585    /// The extractor may be added or replaced on reopen. A table records the
2586    /// [`name`](PrefixExtractor::name) of the extractor its filter indexes,
2587    /// and a prefix scan skips by a table's filter only when that is the
2588    /// configured extractor: tables written without it, or under another,
2589    /// are read instead, until compaction rewrites them.
2590    #[must_use]
2591    pub fn prefix_extractor(mut self, extractor: Arc<dyn PrefixExtractor>) -> Self {
2592        self.prefix_extractor = Some(extractor);
2593        self
2594    }
2595
2596    /// Ends every table that flush, ingest and compaction write before the
2597    /// first key of the next of `domains`, even below the target size, so no
2598    /// new table holds keys of two domains.
2599    ///
2600    /// The policy may be added or replaced on reopen: tables written before it
2601    /// keep their keys until compaction rewrites them.
2602    #[must_use]
2603    pub fn key_domains(mut self, domains: Arc<dyn crate::KeyDomains>) -> Self {
2604        self.key_domains = Some(domains);
2605        self
2606    }
2607
2608    /// Installs a merge operator for commutative operations.
2609    ///
2610    /// When set, enables [`crate::AbstractTree::merge`] which stores partial updates
2611    /// (operands) that are lazily combined during reads and compaction.
2612    #[must_use]
2613    pub fn with_merge_operator(mut self, op: Option<Arc<dyn MergeOperator>>) -> Self {
2614        self.merge_operator = op;
2615        self
2616    }
2617
2618    /// Sets a custom user key comparator.
2619    ///
2620    /// When configured, all key ordering (memtable, block index, merge,
2621    /// range scans) uses this comparator instead of the default lexicographic
2622    /// byte ordering.
2623    ///
2624    /// # Important
2625    ///
2626    /// The comparator's [`crate::UserComparator::name`] is persisted when a tree is
2627    /// first created. On subsequent opens the stored name is compared against
2628    /// the supplied comparator's name — a mismatch causes the open to fail
2629    /// with [`Error::ComparatorMismatch`](crate::Error::ComparatorMismatch).
2630    #[must_use]
2631    pub fn comparator(mut self, comparator: SharedComparator) -> Self {
2632        self.comparator = comparator;
2633        self
2634    }
2635
2636    /// Sets the block-level encryption provider for encryption at rest.
2637    ///
2638    /// When set, all blocks written to SST files are encrypted after
2639    /// compression and before checksumming, using the provided
2640    /// [`EncryptionProvider`]. The compression dictionaries the tree stores
2641    /// are sealed with it too, since a dictionary keeps literal stretches of
2642    /// the records it was trained on.
2643    ///
2644    /// The caller is responsible for key management and rotation.
2645    /// See `crate::Aes256GcmProvider` (behind the `encryption` feature)
2646    /// for a ready-to-use AES-256-GCM implementation.
2647    ///
2648    /// **Important constraints:**
2649    /// - Encryption state is NOT recorded in SST metadata. Opening an
2650    ///   encrypted tree without the correct provider (or vice versa) will
2651    ///   cause block validation errors, not silent corruption.
2652    /// - Blob files (KV-separated large values) are NOT covered by
2653    ///   block-level encryption. Large values stored via KV separation
2654    ///   remain in plaintext on disk.
2655    #[must_use]
2656    pub fn with_encryption(mut self, encryption: Option<Arc<dyn EncryptionProvider>>) -> Self {
2657        self.encryption = encryption;
2658        self
2659    }
2660
2661    /// Sets the wall clock this tree reads: the creation time stamped on its
2662    /// tables and blob files, those a repair salvages included, and the time
2663    /// FIFO compaction measures TTL against.
2664    ///
2665    /// Without one the tree reads the process clock: [`SystemClock`](crate::SystemClock)
2666    /// under `std`, or the clock registered with `set_clock` under `no_std`.
2667    ///
2668    /// A fixed clock makes the files a tree writes reproducible: two trees
2669    /// given the same fixed clock, the same writes in the same order and the
2670    /// same configuration write byte-identical tables and blob files, wherever
2671    /// their folders are. Encryption breaks that, since every sealed block
2672    /// takes a fresh nonce.
2673    ///
2674    /// # Examples
2675    ///
2676    /// ```
2677    /// # use lsm_tree::{Clock, Config, SequenceNumberCounter};
2678    /// # use std::{sync::Arc, time::Duration};
2679    /// struct Epoch;
2680    /// impl Clock for Epoch {
2681    ///     fn unix_time(&self) -> Duration {
2682    ///         Duration::from_secs(1_700_000_000)
2683    ///     }
2684    /// }
2685    /// # let folder = tempfile::tempdir()?;
2686    /// let config = Config::new(
2687    ///     folder.path(),
2688    ///     SequenceNumberCounter::default(),
2689    ///     SequenceNumberCounter::default(),
2690    /// )
2691    /// .with_clock(Arc::new(Epoch));
2692    /// # Ok::<(), Box<dyn std::error::Error>>(())
2693    /// ```
2694    #[must_use]
2695    pub fn with_clock(mut self, clock: Arc<dyn crate::Clock>) -> Self {
2696        self.clock = Some(clock);
2697        self
2698    }
2699
2700    /// Sets what `Tree::open` does with a torn tail of the manifest edit log.
2701    ///
2702    /// The only difference between the modes is an edit-log record the writer
2703    /// never finished appending (power loss mid-append): the default
2704    /// ([`ManifestRecoveryMode::AbsoluteConsistency`]) fails the open,
2705    /// [`ManifestRecoveryMode::TolerateCorruptedTailRecords`] drops it and
2706    /// opens at the last complete edit. That record was never acknowledged, so
2707    /// dropping it loses no committed state. Damage to anything committed (a
2708    /// snapshot section, a fully written edit) fails the open in either mode;
2709    /// [`Config::repair`] rebuilds the manifest from the tables on disk.
2710    ///
2711    /// See the [`ManifestRecoveryMode`] doc for per-variant semantics.
2712    #[must_use]
2713    pub fn manifest_recovery_mode(mut self, mode: ManifestRecoveryMode) -> Self {
2714        self.manifest_recovery_mode = mode;
2715        self
2716    }
2717
2718    /// Sets the durability level for every fsync the tree issues.
2719    ///
2720    /// Defaults to [`SyncMode::Normal`] (plain `fsync`, matching `RocksDB` /
2721    /// `SQLite` defaults). Pass [`SyncMode::Full`] to force `F_FULLFSYNC` on
2722    /// macOS for power-loss durability without an external journal — at a
2723    /// large per-flush cost. Pass [`SyncMode::Barrier`] to keep writes in
2724    /// order at a barrier's cost and make them durable with
2725    /// [`AbstractTree::sync_devices`](crate::AbstractTree::sync_devices), once
2726    /// per batch of flushes. On non-macOS platforms all three are plain
2727    /// `fsync`.
2728    #[must_use]
2729    pub fn sync_mode(mut self, mode: SyncMode) -> Self {
2730        self.sync_mode = mode;
2731        self
2732    }
2733
2734    /// Sets when the tables a flush writes become durable.
2735    ///
2736    /// Defaults to [`FlushDurability::Synced`]: a flush that returns `Ok` is
2737    /// on disk. [`FlushDurability::Deferred`] leaves a flush unsynced until
2738    /// [`AbstractTree::persist_flushes`](crate::AbstractTree::persist_flushes),
2739    /// for a caller whose own write-ahead log already makes its writes
2740    /// durable.
2741    ///
2742    /// # Examples
2743    ///
2744    /// ```
2745    /// # let folder = tempfile::tempdir()?;
2746    /// use lsm_tree::{AbstractTree, Config, SequenceNumberCounter, config::FlushDurability};
2747    ///
2748    /// let tree = Config::new(
2749    ///     folder.path(),
2750    ///     SequenceNumberCounter::default(),
2751    ///     SequenceNumberCounter::default(),
2752    /// )
2753    /// .flush_durability(FlushDurability::Deferred)
2754    /// .open()?;
2755    /// tree.insert("a", "1", 0);
2756    /// // Written and readable, not yet durable: the caller's log still holds it.
2757    /// tree.flush_active_memtable(0)?;
2758    /// // Durable from here: the caller may drop its log up to this point.
2759    /// tree.persist_flushes()?;
2760    /// # Ok::<(), lsm_tree::Error>(())
2761    /// ```
2762    #[must_use]
2763    pub fn flush_durability(mut self, durability: FlushDurability) -> Self {
2764        self.flush_durability = durability;
2765        self
2766    }
2767
2768    /// Sets how many bytes a table or blob file gathers before its writer
2769    /// starts writing them back, without waiting (default 1 MiB; `0` starts
2770    /// none).
2771    ///
2772    /// The final sync of a large file otherwise writes all its dirty pages at
2773    /// once, and on a device shared with the caller's journal that burst
2774    /// delays the journal's own syncs. Writeback makes nothing durable; the
2775    /// final sync still does. Only Linux starts one
2776    /// (`sync_file_range(SYNC_FILE_RANGE_WRITE)`); elsewhere this is unused.
2777    ///
2778    /// # Examples
2779    ///
2780    /// ```
2781    /// use lsm_tree::{Config, SequenceNumberCounter};
2782    ///
2783    /// let folder = tempfile::tempdir()?;
2784    /// let tree = Config::new(
2785    ///     &folder,
2786    ///     SequenceNumberCounter::default(),
2787    ///     SequenceNumberCounter::default(),
2788    /// )
2789    /// .writeback_bytes(4 * 1_024 * 1_024)
2790    /// .open()?;
2791    /// # Ok::<(), lsm_tree::Error>(())
2792    /// ```
2793    #[must_use]
2794    pub fn writeback_bytes(mut self, bytes: u64) -> Self {
2795        self.writeback_bytes = bytes;
2796        self
2797    }
2798
2799    /// Sets the size a flush or an ingestion cuts its output tables at
2800    /// (default [`DEFAULT_TABLE_TARGET_SIZE`], 64 MiB).
2801    ///
2802    /// A memtable larger than this flushes into several tables of one run;
2803    /// an ingestion cuts its stream the same way, at whatever level it writes
2804    /// to. Compaction output is sized by its strategy, not by this value.
2805    ///
2806    /// The leveled L0 trigger counts runs, so the cut does not change how
2807    /// often L0 merges into L1; it bounds how much per-key state a flush's
2808    /// writer holds. A flush of fewer, larger tables writes less in the
2809    /// merges that follow, so with a memtable much larger than the default a
2810    /// matching target is the cheaper shape, as far as the writer's memory
2811    /// allows.
2812    ///
2813    /// # Examples
2814    ///
2815    /// ```
2816    /// use lsm_tree::{Config, SequenceNumberCounter};
2817    ///
2818    /// let folder = tempfile::tempdir()?;
2819    /// let tree = Config::new(
2820    ///     &folder,
2821    ///     SequenceNumberCounter::default(),
2822    ///     SequenceNumberCounter::default(),
2823    /// )
2824    /// .table_target_size(8 * 1_024 * 1_024)
2825    /// .open()?;
2826    /// # Ok::<(), lsm_tree::Error>(())
2827    /// ```
2828    #[must_use]
2829    pub fn table_target_size(mut self, bytes: u64) -> Self {
2830        self.table_target_size = bytes;
2831        self
2832    }
2833
2834    /// Sets the edit-log rotation threshold in bytes (default 1 MiB).
2835    ///
2836    /// Once the manifest edit log exceeds this size, the next version upgrade
2837    /// writes a fresh full snapshot and starts an empty log instead of appending
2838    /// another edit. Lower it to shorten recovery replay and cap log size at the
2839    /// cost of more frequent full-snapshot writes; `0` rotates on every upgrade.
2840    #[must_use]
2841    pub fn manifest_log_rotate_bytes(mut self, bytes: u64) -> Self {
2842        self.manifest_log_rotate_bytes = bytes;
2843        self
2844    }
2845
2846    /// Sets how many empty table files each tables folder creates at a time,
2847    /// ahead of the tables written into them (default 16).
2848    ///
2849    /// A new table's name has to be durable before the manifest edit that
2850    /// names it, which costs a sync of its folder. A pooled file's name was
2851    /// made durable with its batch, one folder sync for the whole batch, so a
2852    /// flush or a compaction into one syncs its tables and the manifest edit
2853    /// only. The next batch is made off the writing thread once a claim
2854    /// leaves half a batch or less. An unused pooled file is empty and takes
2855    /// no space; the next open
2856    /// uses it again. `0` creates each table file as its table is written and
2857    /// syncs the folder at every install that names one.
2858    ///
2859    /// # Examples
2860    ///
2861    /// ```
2862    /// use lsm_tree::{Config, SequenceNumberCounter};
2863    ///
2864    /// let folder = tempfile::tempdir()?;
2865    /// let tree = Config::new(
2866    ///     &folder,
2867    ///     SequenceNumberCounter::default(),
2868    ///     SequenceNumberCounter::default(),
2869    /// )
2870    /// .table_file_pool(64)
2871    /// .open()?;
2872    /// # Ok::<(), lsm_tree::Error>(())
2873    /// ```
2874    #[must_use]
2875    pub fn table_file_pool(mut self, files: usize) -> Self {
2876        self.table_file_pool = files;
2877        self
2878    }
2879
2880    /// Refills a table file pool on the thread of the write that claims from
2881    /// it, rather than off it (default `false`). A write then waits on the
2882    /// batch it starts, and every sync the pool issues falls inside one: a
2883    /// test that counts an operation's syncs, or fails one of them, sees the
2884    /// same ones on every run.
2885    #[doc(hidden)]
2886    #[must_use]
2887    pub fn table_file_refill_inline(mut self, inline: bool) -> Self {
2888        use crate::table::file_pool::RefillOn;
2889        self.table_file_refill = if inline {
2890            RefillOn::ClaimingThread
2891        } else {
2892            RefillOn::Background
2893        };
2894        self
2895    }
2896
2897    /// Sets the retention floor a manifest repair seeds the rebuilt tree with
2898    /// (default `0`): after [`repair`](Self::repair) /
2899    /// [`open_or_repair`](Self::open_or_repair) every snapshot at or below
2900    /// `floor` is refused with
2901    /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention),
2902    /// exactly as the tree refused it before the manifest was lost.
2903    ///
2904    /// A normal open needs no help: the manifest carries the floor every
2905    /// retention-advancing install established (a GC compaction, `clear`, a
2906    /// table drop, a filtering compaction; see
2907    /// [`AbstractTree::retention_floor`](crate::AbstractTree::retention_floor)).
2908    /// A repair rebuilds the manifest from the tables, which do not record it
2909    /// (a GC compaction zeroes the seqnos of the rows it settles), so only the
2910    /// deployment knows it: record
2911    /// [`retention_floor()`](crate::AbstractTree::retention_floor) in your own
2912    /// durable state (it already folds in every operation that raised the
2913    /// boundary, so no per-operation bookkeeping is needed) and pass the last
2914    /// recorded value here. Left at `0`, a repaired tree serves every
2915    /// snapshot, which is correct only if history was never collected. Has no
2916    /// effect on an open that finds a manifest.
2917    #[must_use]
2918    pub fn repair_retention_floor(mut self, floor: crate::SeqNo) -> Self {
2919        self.repair_retention_floor = floor;
2920        self
2921    }
2922
2923    /// Sets the compaction I/O rate limit in bytes per second.
2924    ///
2925    /// Caps how fast the compaction worker may issue I/O so background
2926    /// compaction does not saturate the device and spike user read P99.
2927    /// `0` (the default) disables throttling. Only compaction is limited;
2928    /// flush and user reads always pass through.
2929    ///
2930    /// The tree builds its own limiter from this figure at open; change it
2931    /// later through [`AbstractTree::compaction_rate_limiter`](crate::AbstractTree::compaction_rate_limiter).
2932    /// Ignored when a shared limiter is supplied through
2933    /// [`Self::compaction_rate_limiter`].
2934    ///
2935    /// The figure is the share of the device the deployment gives compaction,
2936    /// and only the deployment knows it. Below what the write rate times the
2937    /// strategy's write amplification needs, compaction falls behind: runs
2938    /// pile up and every read visits more of them. The
2939    /// [`Planned`](crate::compaction::Planned) strategy reads the limit and
2940    /// plans a tree it can keep (see its section on the compaction budget);
2941    /// [`Leveled`](crate::compaction::Leveled) keeps its shape and lags.
2942    #[must_use]
2943    pub fn compaction_rate_limit(mut self, bytes_per_sec: u64) -> Self {
2944        self.compaction_rate_limit = bytes_per_sec;
2945        self
2946    }
2947
2948    /// Supplies a compaction rate limiter shared with other trees, used in
2949    /// place of the per-tree one built from [`Self::compaction_rate_limit`].
2950    ///
2951    /// Hand the same `Arc` to every tree on one device: their compactions are
2952    /// then bounded by it together, at the rate it holds, whatever each
2953    /// tree's configured [`Self::compaction_rate_limit`] says. A retune through
2954    /// [`RateLimiter::set_rate`](crate::rate_limiter::RateLimiter::set_rate)
2955    /// on any holder applies to all of them on their next request.
2956    ///
2957    /// # Examples
2958    ///
2959    /// ```
2960    /// use lsm_tree::rate_limiter::RateLimiter;
2961    /// use lsm_tree::Config;
2962    /// use std::sync::Arc;
2963    ///
2964    /// # let a = tempfile::tempdir()?;
2965    /// # let b = tempfile::tempdir()?;
2966    /// let device = Arc::new(RateLimiter::new(64 * 1_024 * 1_024));
2967    /// let first = Config::new(a.path(), Default::default(), Default::default())
2968    ///     .compaction_rate_limiter(Arc::clone(&device))
2969    ///     .open()?;
2970    /// let second = Config::new(b.path(), Default::default(), Default::default())
2971    ///     .compaction_rate_limiter(Arc::clone(&device))
2972    ///     .open()?;
2973    /// // Both trees now share the device's 64 MiB/s.
2974    /// # drop((first, second));
2975    /// # Ok::<(), lsm_tree::Error>(())
2976    /// ```
2977    #[must_use]
2978    pub fn compaction_rate_limiter(
2979        mut self,
2980        limiter: Arc<crate::rate_limiter::RateLimiter>,
2981    ) -> Self {
2982        self.compaction_rate_limiter = Some(limiter);
2983        self
2984    }
2985
2986    /// Supplies the pool this tree's compactions reserve the room for their
2987    /// outputs in, shared with other trees, in place of a pool of its own.
2988    ///
2989    /// Hand the same `Arc` to every tree whose files share volumes: each
2990    /// compaction then reserves against what the others have still to write,
2991    /// rather than two of them counting on the same free bytes. See
2992    /// [`CapacityPool`](crate::CapacityPool).
2993    #[must_use]
2994    pub fn compaction_capacity(mut self, pool: Arc<crate::CapacityPool>) -> Self {
2995        self.compaction_capacity = Some(pool);
2996        self
2997    }
2998
2999    /// The pool a tree opened with this configuration reserves its
3000    /// compactions' room in: the shared one if supplied, else its own.
3001    pub(crate) fn tree_compaction_capacity(&self) -> Arc<crate::CapacityPool> {
3002        self.compaction_capacity.clone().unwrap_or_default()
3003    }
3004
3005    /// Sets the byte rate writes are paced to while the tree's backpressure
3006    /// verdict is in its slowdown tier (default `0`: no byte pacing).
3007    ///
3008    /// With a rate set,
3009    /// [`AbstractTree::write_backpressure_for`](crate::AbstractTree::write_backpressure_for)
3010    /// answers a slowdown with what the write's bytes owe at this rate, so a
3011    /// caller honouring it writes no faster than the rate however large or
3012    /// small its writes are; below the slowdown thresholds nothing is debited
3013    /// and writes run at full speed. The tree builds its own limiter from
3014    /// this figure at open; change it later through
3015    /// [`AbstractTree::write_rate_limiter`](crate::AbstractTree::write_rate_limiter).
3016    /// Ignored when a shared limiter is supplied through
3017    /// [`Self::write_rate_limiter`].
3018    ///
3019    /// # Examples
3020    ///
3021    /// ```
3022    /// use lsm_tree::{Config, SequenceNumberCounter};
3023    ///
3024    /// let folder = tempfile::tempdir()?;
3025    /// let tree = Config::new(
3026    ///     &folder,
3027    ///     SequenceNumberCounter::default(),
3028    ///     SequenceNumberCounter::default(),
3029    /// )
3030    /// .write_rate_limit(16 * 1_024 * 1_024)
3031    /// .open()?;
3032    /// # Ok::<(), lsm_tree::Error>(())
3033    /// ```
3034    #[must_use]
3035    pub fn write_rate_limit(mut self, bytes_per_sec: u64) -> Self {
3036        self.write_rate_limit = bytes_per_sec;
3037        self
3038    }
3039
3040    /// Supplies a write rate limiter shared with other trees, used in place of
3041    /// the per-tree one built from [`Self::write_rate_limit`]: writes paced
3042    /// under slowdown by any of them draw on one budget, at the rate it holds.
3043    #[must_use]
3044    pub fn write_rate_limiter(mut self, limiter: Arc<crate::rate_limiter::RateLimiter>) -> Self {
3045        self.write_rate_limiter = Some(limiter);
3046        self
3047    }
3048
3049    /// Joins the tree to a [`WriteBufferBudget`](crate::WriteBufferBudget)
3050    /// shared with other trees: the memtable axis of its backpressure verdict
3051    /// then compares the unflushed memtable bytes of every member together.
3052    #[cfg(feature = "std")]
3053    #[must_use]
3054    pub fn write_buffer_budget(
3055        mut self,
3056        budget: Arc<crate::backpressure::WriteBufferBudget>,
3057    ) -> Self {
3058        self.write_buffer_budget = Some(budget);
3059        self
3060    }
3061
3062    /// The limiter a tree opened with this configuration paces slowed-down
3063    /// writes with: the shared one if supplied, else its own built from
3064    /// [`Self::write_rate_limit`].
3065    pub(crate) fn tree_write_rate_limiter(&self) -> Arc<crate::rate_limiter::RateLimiter> {
3066        self.write_rate_limiter.clone().unwrap_or_else(|| {
3067            Arc::new(crate::rate_limiter::RateLimiter::new(self.write_rate_limit))
3068        })
3069    }
3070
3071    /// The limiter a tree opened with this configuration throttles its
3072    /// compactions with: the shared one if supplied, else its own built from
3073    /// [`Self::compaction_rate_limit`].
3074    pub(crate) fn tree_compaction_rate_limiter(&self) -> Arc<crate::rate_limiter::RateLimiter> {
3075        self.compaction_rate_limiter.clone().unwrap_or_else(|| {
3076            Arc::new(crate::rate_limiter::RateLimiter::new(
3077                self.compaction_rate_limit,
3078            ))
3079        })
3080    }
3081
3082    /// Sets the compaction worker-thread count.
3083    ///
3084    /// Under `std` this both sizes the per-tree pool built at open when no
3085    /// shared pool is supplied (see [`Self::compaction_pool`]), which prepares
3086    /// a table's data blocks and builds its partitioned filter's partitions,
3087    /// and caps how many range-parallel sub-compactions a compaction splits
3088    /// into.
3089    /// `1` keeps compaction serial. Default is `max(1, available_parallelism /
3090    /// 2)`. Without the `parallel` feature there is no built-in pool, so the
3091    /// work runs serially even for a value > 1.
3092    #[cfg(feature = "std")]
3093    #[must_use]
3094    pub fn compaction_threads(mut self, threads: usize) -> Self {
3095        // Clamp to >= 1: the documented semantics treat `1` as "serial", and a
3096        // 0-thread pool would be an invalid state.
3097        self.compaction_threads = threads.max(1);
3098        self
3099    }
3100
3101    /// Sets the payload size (bytes) below which a block of a table compressed
3102    /// in parallel is prepared on the writer thread instead of a worker. A
3103    /// block that small costs less to compress than to hand to a worker and
3104    /// back. `None` (default) derives the threshold from the table's block
3105    /// codec: 1 KiB for lz4; 8 KiB with no codec, where only encryption or
3106    /// page ECC brings a table to the workers and costs less per block than
3107    /// lz4; and 0 for zstd, whose per-frame setup makes any block worth a
3108    /// worker. `Some(0)` sends every block to the workers.
3109    ///
3110    /// # Examples
3111    ///
3112    /// ```
3113    /// use lsm_tree::{Config, SequenceNumberCounter};
3114    /// # let folder = tempfile::tempdir()?;
3115    /// let config = Config::new(
3116    ///     folder.path(),
3117    ///     SequenceNumberCounter::default(),
3118    ///     SequenceNumberCounter::default(),
3119    /// )
3120    /// .parallel_compression_inline_below(Some(1_024));
3121    /// # Ok::<(), Box<dyn std::error::Error>>(())
3122    /// ```
3123    #[cfg(feature = "std")]
3124    #[must_use]
3125    pub fn parallel_compression_inline_below(mut self, bytes: Option<u32>) -> Self {
3126        self.parallel_compression_inline_below = bytes;
3127        self
3128    }
3129
3130    /// The parallel block compression a table writer of this tree uses, or
3131    /// `None` when no pool is configured.
3132    #[cfg(feature = "std")]
3133    pub(crate) fn parallel_compression(&self) -> Option<crate::table::writer::ParallelCompression> {
3134        self.compaction_pool
3135            .clone()
3136            .map(|spawner| crate::table::writer::ParallelCompression {
3137                spawner,
3138                threads: self.compaction_threads,
3139                inline_below: self.parallel_compression_inline_below,
3140            })
3141    }
3142
3143    /// Sets the minimum total input size (bytes) for a compaction to be split
3144    /// into parallel sub-compactions. Default 8 MiB. `0` splits every eligible
3145    /// compaction; a large value effectively disables sub-compaction (block
3146    /// compression still parallelizes via [`Self::compaction_threads`]).
3147    #[cfg(feature = "std")]
3148    #[must_use]
3149    pub fn subcompaction_min_bytes(mut self, bytes: u64) -> Self {
3150        self.subcompaction_min_bytes = bytes;
3151        self
3152    }
3153
3154    /// Supplies a shared compaction thread pool, used in place of the per-tree
3155    /// default. Pass one [`crate::table::writer::CompactionSpawner`] (e.g. a
3156    /// `RayonSpawner` wrapping a shared rayon thread pool) to several trees so
3157    /// the total worker-thread count stays bounded by the pool size rather than
3158    /// the number of open trees.
3159    #[cfg(feature = "std")]
3160    #[must_use]
3161    pub fn compaction_pool(
3162        mut self,
3163        pool: Option<Arc<dyn crate::table::writer::CompactionSpawner>>,
3164    ) -> Self {
3165        self.compaction_pool = pool;
3166        self
3167    }
3168
3169    /// Supplies a pre-trained zstd dictionary for the data block policy to
3170    /// name.
3171    ///
3172    /// The open registers it with the tree, which stores it, so a later open
3173    /// needs nothing supplied. Which blocks are written against it is decided
3174    /// by the policy: a [`CompressionType::ZstdDict`] entry names a dictionary
3175    /// by id, and every block reads back through the id it recorded. The
3176    /// dictionary should be trained on representative data samples for best
3177    /// results.
3178    ///
3179    /// Create a dictionary with [`ZstdDictionary::new`](crate::ZstdDictionary::new),
3180    /// then use [`CompressionType::zstd_dict`] to create a matching
3181    /// compression type:
3182    ///
3183    /// ```ignore
3184    /// use lsm_tree::{CompressionType, ZstdDictionary};
3185    ///
3186    /// let dict = ZstdDictionary::new(&training_data);
3187    /// let compression = CompressionType::zstd_dict(3, dict.id()).unwrap();
3188    ///
3189    /// config
3190    ///     .zstd_dictionary(Some(Arc::new(dict)))
3191    ///     .data_block_compression_policy(CompressionPolicy::all(compression));
3192    /// ```
3193    #[cfg(zstd_any)]
3194    #[must_use]
3195    pub fn zstd_dictionary(
3196        mut self,
3197        dictionary: Option<Arc<crate::compression::ZstdDictionary>>,
3198    ) -> Self {
3199        self.zstd_dictionary = dictionary;
3200        self
3201    }
3202}
3203
3204#[cfg(test)]
3205mod builder_tests;