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
5mod block_size;
6mod compression;
7mod delete_strategy;
8mod filter;
9mod hash_ratio;
10mod locator;
11mod pinning;
12mod restart_interval;
13
14pub use block_size::BlockSizePolicy;
15pub use compression::CompressionPolicy;
16pub use delete_strategy::{DeleteStrategy, DeleteStrategyPolicy};
17pub use filter::{BloomConstructionPolicy, FilterPolicy, FilterPolicyEntry};
18pub use hash_ratio::HashRatioPolicy;
19pub use locator::{LocatorPolicy, LocatorPolicyEntry, LocatorPrecision};
20pub use pinning::PinningPolicy;
21pub use restart_interval::RestartIntervalPolicy;
22
23/// Partitioning policy for indexes and filters
24pub type PartitioningPolicy = PinningPolicy;
25
26#[cfg(feature = "std")]
27use crate::fs::StdFs;
28use crate::path::PathBuf;
29use crate::{
30    AnyTree, BlobTree, Cache, CompressionType, DescriptorTable, SharedSequenceNumberGenerator,
31    Tree,
32    compaction::filter::Factory,
33    comparator::SharedComparator,
34    encryption::EncryptionProvider,
35    file::TABLES_FOLDER,
36    fs::{Fs, SyncMode},
37    merge_operator::MergeOperator,
38    path::absolute_path,
39    prefix::PrefixExtractor,
40};
41// std-only: used solely by the std-gated `Config::default` / `Config::new`
42// constructors (the no_std path builds `Config` field-by-field).
43#[cfg(feature = "std")]
44use crate::{SequenceNumberCounter, comparator, path::Path, version::DEFAULT_LEVEL_COUNT};
45use alloc::sync::Arc;
46#[cfg(not(feature = "std"))]
47use alloc::vec::Vec;
48use core::ops::Range;
49
50/// Per-level filesystem routing entry for tiered storage.
51///
52/// Maps a range of LSM levels to a base directory and filesystem backend.
53/// Tables at these levels are stored under `path/tables/`.
54///
55/// # Example
56///
57/// ```
58/// use lsm_tree::config::LevelRoute;
59/// use lsm_tree::fs::StdFs;
60/// use std::sync::Arc;
61///
62/// // Hot tier: L0-L1 on NVMe
63/// let hot = LevelRoute {
64///     levels: 0..2,
65///     path: "/mnt/nvme/db".into(),
66///     fs: Arc::new(StdFs),
67/// };
68///
69/// // Cold tier: L4-L6 on HDD
70/// let cold = LevelRoute {
71///     levels: 4..7,
72///     path: "/mnt/hdd/db".into(),
73///     fs: Arc::new(StdFs),
74/// };
75/// ```
76#[derive(Clone)]
77pub struct LevelRoute {
78    /// LSM levels this route covers (e.g., `0..2` for L0–L1).
79    pub levels: Range<u8>,
80
81    /// Base data directory for tables at these levels.
82    pub path: PathBuf,
83
84    /// Filesystem backend for I/O at these levels.
85    pub fs: Arc<dyn Fs>,
86}
87
88impl core::fmt::Debug for LevelRoute {
89    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
90        f.debug_struct("LevelRoute")
91            .field("levels", &self.levels)
92            .field("path", &self.path)
93            .finish_non_exhaustive()
94    }
95}
96
97/// Policy governing what `Tree::open` does when the on-disk MANIFEST
98/// contains corrupt records.
99///
100/// Mirrors `RocksDB`'s `WALRecoveryMode` semantics, but applied to the
101/// manifest layer (`src/version/recovery.rs`) — lsm-tree itself has no
102/// WAL (durability lives one layer up in the parent fjall/keyspace
103/// crate's `Journal`). The MANIFEST is the equivalent surface where
104/// "loss-tolerance vs strict-consistency" matters at open time.
105///
106/// The default is [`AbsoluteConsistency`](Self::AbsoluteConsistency) —
107/// any corrupt record fails the open. Switching to a more permissive
108/// mode is an explicit, informed operator decision: you are trading
109/// "the tree might silently come up with missing tables / blob files"
110/// for "the tree comes up at all". When a non-default mode drops
111/// records, the recovery path emits a `warn!` summary with the
112/// AGGREGATE dropped count per section (`tables` / `blob_files`) —
113/// individual table IDs / blob-file IDs are NOT enumerated, because
114/// they were never decoded in the first place. Operators wanting a
115/// per-record audit trail should pair tail-tolerant recovery with an
116/// out-of-band integrity scan ([`verify_integrity`](crate::verify::verify_integrity))
117/// of the recovered tree.
118#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
119pub enum ManifestRecoveryMode {
120    /// Production-safe default. Any per-record decode mismatch (bad
121    /// XXH3, invalid tag, truncated TOC entry, declared-count overrun)
122    /// aborts the open with the original error. Surfaces every byte
123    /// of corruption; never silently drops data.
124    #[default]
125    AbsoluteConsistency,
126
127    /// Power-loss-at-write-tail salvage. If the per-section iteration
128    /// over the `tables` / `blob_files` records runs out of bytes
129    /// before the declared count is reached (truncated tail), keep
130    /// everything that decoded cleanly before the cut and emit a
131    /// `warn!` listing the dropped record counts.
132    ///
133    /// A declared count that exceeds the section's payload capacity
134    /// (e.g. `table_count` claims more entries than the section has
135    /// bytes for) is treated as the same "writer committed a count
136    /// header then truncated the entries" shape — the recovery
137    /// downgrades the original hard fail to a `warn!` and lets the
138    /// per-entry decode loop walk bytes-actually-present until the
139    /// first `UnexpectedEof`.
140    ///
141    /// Any decode error that is NOT a clean tail truncation (bad
142    /// `checksum_type` tag, etc.) still aborts the open — this mode
143    /// is specifically for "the writer never finished" scenarios,
144    /// not for arbitrary bit-rot in already-committed bytes.
145    TolerateCorruptedTailRecords,
146
147    /// Recover the largest consistent prefix and discard the rest.
148    /// Adapts `RocksDB`'s `kPointInTimeRecovery` accept-the-prefix
149    /// rule to the level/run/table nesting: on the first
150    /// record-decode mismatch inside the `tables` section, the
151    /// recovery keeps the records that decoded cleanly *before*
152    /// the corrupt one in the current run, plus every complete
153    /// earlier run in the same level, plus every complete earlier
154    /// level. "Record-decode mismatch" covers ALL three failure
155    /// shapes the per-record loop can surface:
156    ///
157    /// 1. Framing-layer XXH3 mismatch (the 8-byte digest in the
158    ///    record header doesn't match `xxh3_64(payload)`).
159    /// 2. Framing-header structural failure (`len > MAX_FRAME_PAYLOAD`),
160    ///    surfaced as `BadHeader`. Note: `LenMismatch` (decoded `len`
161    ///    disagrees with a fixed-length pin) is a SEPARATE hard-abort
162    ///    case in every recovery mode, not a record-decode mismatch
163    ///    for the purpose of this mode.
164    /// 3. Payload decode failure AFTER a clean framing pass —
165    ///    e.g. `Error::InvalidTag` from a corrupt `checksum_type`
166    ///    byte inside an otherwise-framed-OK record. The framing
167    ///    XXH3 happens to cover the corrupt byte too (it's a
168    ///    digest of the whole payload), so the bytes decode
169    ///    cleanly at the framing layer; the corruption only
170    ///    surfaces inside the per-entry decode helper.
171    ///
172    /// PIT drops the corrupt record itself, the remaining records
173    /// of that run, and every level not yet read. The same rule
174    /// applies to the `blob_files` section. Clean tail-truncation
175    /// is still tolerated, same as
176    /// [`TolerateCorruptedTailRecords`](Self::TolerateCorruptedTailRecords).
177    PointInTimeRecovery,
178
179    /// Skip each corrupt record individually, keep all others.
180    /// Maximum-availability, lossy. On any per-record decode
181    /// mismatch — framing-layer XXH3 mismatch, payload-decode
182    /// failure inside an otherwise-framed-OK record (e.g.
183    /// `Error::InvalidTag` on a corrupt `checksum_type` byte), or
184    /// a framing-header `BadHeader` — the reader logs the skip
185    /// and advances exactly past the bad record using the
186    /// framing-supplied length field. If the length field itself
187    /// is unusable (the recorded length is outside the legal
188    /// range, so the next-record boundary is unknown), the rest
189    /// of that section is dropped. Intended companion to the
190    /// `repair_db` tooling tracked as `#303`: this mode recovers
191    /// what it can in-place; `repair_db` rebuilds the manifest
192    /// from the SST files
193    /// themselves when even this mode can't reach a usable state.
194    SkipAnyCorruptedRecords,
195}
196
197/// LSM-tree type
198#[derive(Copy, Clone, Debug, PartialEq, Eq)]
199pub enum TreeType {
200    /// Standard LSM-tree, see [`Tree`]
201    Standard,
202
203    /// Key-value separated LSM-tree, see [`BlobTree`]
204    Blob,
205}
206
207impl From<TreeType> for u8 {
208    fn from(val: TreeType) -> Self {
209        match val {
210            TreeType::Standard => 0,
211            TreeType::Blob => 1,
212        }
213    }
214}
215
216impl TryFrom<u8> for TreeType {
217    type Error = ();
218
219    fn try_from(value: u8) -> Result<Self, Self::Error> {
220        match value {
221            0 => Ok(Self::Standard),
222            1 => Ok(Self::Blob),
223            _ => Err(()),
224        }
225    }
226}
227
228#[cfg_attr(
229    not(feature = "std"),
230    allow(
231        dead_code,
232        reason = "default data-folder path used only on the std-gated default-config path"
233    )
234)]
235const DEFAULT_FILE_FOLDER: &str = ".lsm.data";
236
237/// Options for key-value separation
238#[derive(Clone, Debug, PartialEq)]
239pub struct KvSeparationOptions {
240    /// What type of compression is used for blobs
241    #[doc(hidden)]
242    pub compression: CompressionType,
243
244    /// Blob file target size in bytes
245    #[doc(hidden)]
246    pub file_target_size: u64,
247
248    /// Key-value separation threshold in bytes
249    #[doc(hidden)]
250    pub separation_threshold: u32,
251
252    #[doc(hidden)]
253    pub staleness_threshold: f32,
254
255    #[doc(hidden)]
256    pub age_cutoff: f32,
257
258    /// How many upcoming values a scan reads ahead in one coalesced batch.
259    ///
260    /// `0` disables read-ahead. See
261    /// [`scan_prefetch`](KvSeparationOptions::scan_prefetch).
262    #[doc(hidden)]
263    pub scan_prefetch: u16,
264
265    /// Pre-trained zstd dictionary for blob-file dictionary compression.
266    ///
267    /// Required when `compression` is [`CompressionType::ZstdDict`].
268    /// The `dict_id` in the compression type must match [`ZstdDictionary::id`](crate::ZstdDictionary::id).
269    #[cfg(zstd_any)]
270    #[doc(hidden)]
271    pub zstd_dictionary: Option<alloc::sync::Arc<crate::compression::ZstdDictionary>>,
272}
273
274impl Default for KvSeparationOptions {
275    fn default() -> Self {
276        Self {
277            #[cfg(feature="lz4")]
278            compression:   CompressionType::Lz4,
279
280            #[cfg(not(feature="lz4"))]
281            compression: CompressionType::None,
282
283            file_target_size: /* 64 MiB */ 64 * 1_024 * 1_024,
284            separation_threshold: /* 1 KiB */ 1_024,
285
286            staleness_threshold: 0.25,
287            age_cutoff: 0.25,
288
289            scan_prefetch: 64,
290
291            #[cfg(zstd_any)]
292            zstd_dictionary: None,
293        }
294    }
295}
296
297impl KvSeparationOptions {
298    /// Sets the blob compression method.
299    #[must_use]
300    pub fn compression(mut self, compression: CompressionType) -> Self {
301        self.compression = compression;
302        self
303    }
304
305    /// Sets the target size of blob files.
306    ///
307    /// Smaller blob files allow more granular garbage collection
308    /// which allows lower space amp for lower write I/O cost.
309    ///
310    /// Larger blob files decrease the number of files on disk and maintenance
311    /// overhead.
312    ///
313    /// Defaults to 64 MiB.
314    #[must_use]
315    pub fn file_target_size(mut self, bytes: u64) -> Self {
316        self.file_target_size = bytes;
317        self
318    }
319
320    /// Sets the key-value separation threshold in bytes.
321    ///
322    /// Smaller value will reduce compaction overhead and thus write amplification,
323    /// at the cost of lower read performance.
324    ///
325    /// Defaults to 1 KiB.
326    #[must_use]
327    pub fn separation_threshold(mut self, bytes: u32) -> Self {
328        self.separation_threshold = bytes;
329        self
330    }
331
332    /// Sets how many upcoming values a scan reads ahead in one batch.
333    ///
334    /// A scan resolves separated values one at a time, and each is its own read
335    /// of a few hundred bytes. Values sit in the blob file in the order the
336    /// flush wrote them, which is key order, so a scan's next values are
337    /// usually its immediate on-disk neighbours: reading a window of them at
338    /// once collapses that stream of small reads into a few large ones.
339    ///
340    /// Read-ahead starts only once the scan's caller resolves a value
341    /// unconditionally, so a scan that reads keys alone never pays for it, and
342    /// neither does one that decides per key whether the value is worth
343    /// reading. Larger windows amortize better on long scans; smaller ones
344    /// waste less on a scan that stops early. `0` disables it.
345    ///
346    /// Applies to the sequential scans: `iter`, `range`, `prefix` and
347    /// `batch_range_scan`. NOT to the seekable iterator, where a seek would
348    /// throw away whatever was read ahead, and which exists for jumping around
349    /// rather than walking a run.
350    ///
351    /// Defaults to 64.
352    #[must_use]
353    pub fn scan_prefetch(mut self, items: u16) -> Self {
354        self.scan_prefetch = items;
355        self
356    }
357
358    /// Sets the staleness threshold percentage.
359    ///
360    /// The staleness percentage determines how much a blob file needs to be fragmented to be
361    /// picked up by the garbage collection.
362    ///
363    /// Defaults to 33%.
364    #[must_use]
365    pub fn staleness_threshold(mut self, ratio: f32) -> Self {
366        self.staleness_threshold = ratio;
367        self
368    }
369
370    /// Sets the age cutoff threshold.
371    ///
372    /// Defaults to 20%.
373    #[must_use]
374    pub fn age_cutoff(mut self, ratio: f32) -> Self {
375        self.age_cutoff = ratio;
376        self
377    }
378
379    /// Sets the zstd dictionary for blob-file dictionary compression.
380    ///
381    /// Required when [`compression`](Self::compression) is set to
382    /// [`CompressionType::ZstdDict`].  The `dict_id` encoded in the
383    /// compression type must equal [`ZstdDictionary::id()`](crate::ZstdDictionary::id) of the
384    /// supplied dictionary; [`Config::open`] will return
385    /// [`Error::ZstdDictMismatch`](crate::Error::ZstdDictMismatch) if
386    /// they disagree.
387    #[cfg(zstd_any)]
388    #[must_use]
389    pub fn dict(
390        mut self,
391        dictionary: alloc::sync::Arc<crate::compression::ZstdDictionary>,
392    ) -> Self {
393        self.zstd_dictionary = Some(dictionary);
394        self
395    }
396}
397
398/// Tree configuration builder
399// Clone: every shared handle is `Arc`-backed, so a clone is a cheap second
400// reference to the same backends — which is what lets `open_or_repair` retry
401// an `open(self)` after a repair.
402#[derive(Clone)]
403pub struct Config {
404    /// Folder path
405    #[doc(hidden)]
406    pub path: PathBuf,
407
408    /// Default filesystem backend for levels without an explicit route.
409    ///
410    /// Defaults to [`StdFs`]. Use [`Config::with_fs`] to plug in an
411    /// alternative backend such as [`MemFs`](crate::fs::MemFs).
412    ///
413    /// Both fresh tree creation and reopening (recovery) are supported
414    /// for any backend that implements [`Fs`].
415    #[doc(hidden)]
416    pub fs: Arc<dyn Fs>,
417
418    /// Per-level filesystem routing for tiered storage.
419    ///
420    /// When set, tables at different LSM levels can be stored on different
421    /// storage devices (e.g., NVMe for L0–L1, SSD for L2–L4, HDD for L5–L6).
422    /// Each entry maps a range of levels to a base directory and filesystem
423    /// backend. Uncovered levels fall back to the primary `path` and `fs`.
424    ///
425    /// Zero additional overhead when `None` — only a single branch check;
426    /// path construction allocations are unchanged.
427    #[doc(hidden)]
428    pub level_routes: Option<Vec<LevelRoute>>,
429
430    /// Block cache to use
431    #[doc(hidden)]
432    pub cache: Arc<Cache>,
433
434    /// Descriptor table to use
435    #[doc(hidden)]
436    pub descriptor_table: Option<Arc<DescriptorTable>>,
437
438    /// Number of levels of the LSM tree (depth of tree)
439    ///
440    /// Once set, the level count is fixed (in the "manifest" file)
441    pub level_count: u8,
442
443    /// What type of compression is used for data blocks
444    pub data_block_compression_policy: CompressionPolicy,
445
446    /// What type of compression is used for index blocks
447    pub index_block_compression_policy: CompressionPolicy,
448
449    /// Restart interval inside data blocks
450    pub data_block_restart_interval_policy: RestartIntervalPolicy,
451
452    /// Restart interval inside index blocks
453    pub index_block_restart_interval_policy: RestartIntervalPolicy,
454
455    /// Block size of data blocks
456    pub data_block_size_policy: BlockSizePolicy,
457
458    /// Whether to pin index blocks
459    pub index_block_pinning_policy: PinningPolicy,
460
461    /// Whether to pin filter blocks
462    pub filter_block_pinning_policy: PinningPolicy,
463
464    /// Whether to pin top level index of partitioned index
465    pub top_level_index_block_pinning_policy: PinningPolicy,
466
467    /// Whether to pin top level index of partitioned filter
468    pub top_level_filter_block_pinning_policy: PinningPolicy,
469
470    /// Data block hash ratio
471    pub data_block_hash_ratio_policy: HashRatioPolicy,
472
473    /// Whether to partition index blocks
474    pub index_block_partitioning_policy: PartitioningPolicy,
475
476    /// Whether to partition filter blocks
477    pub filter_block_partitioning_policy: PartitioningPolicy,
478
479    /// Partition size when using partitioned indexes
480    pub index_block_partition_size_policy: BlockSizePolicy,
481
482    /// Partition size when using partitioned filters
483    pub filter_block_partition_size_policy: BlockSizePolicy,
484
485    /// If `true`, the last level will not build filters, reducing the filter size of a database
486    /// by ~90% typically
487    pub(crate) expect_point_read_hits: bool,
488
489    /// Per-block Page ECC. When `true`, every block on disk carries a parity
490    /// trailer; on read, if the block's XXH3 disagrees with the on-disk bytes,
491    /// the reader attempts recovery from the trailer before surfacing the
492    /// corruption. The correction scheme is selected at runtime
493    /// (`update_runtime_config`): per-word SEC-DED (the default), single XOR
494    /// parity, or Reed-Solomon. Requires the `page_ecc` cargo feature — opening a
495    /// tree with `page_ecc = true` on a build without the feature returns
496    /// [`crate::Error::PageEccUnsupported`].
497    ///
498    /// Off by default. `RocksDB` ships per-block ECC as an operator-
499    /// chosen knob (typically off on RAID-protected media, on on
500    /// single-drive) and the cost is non-trivial on the write path,
501    /// so the default keeps the existing behaviour.
502    pub(crate) page_ecc: bool,
503
504    /// Initial [`crate::runtime_config::RuntimeConfig`] snapshot
505    /// the tree starts with. Seeds both the first
506    /// `persist_version` call and the Tree's
507    /// `RuntimeConfigHandle`, so a non-default value supplied via
508    /// [`Config::with_runtime_config`] is honoured from byte zero
509    /// of the manifest. Defaults to `RuntimeConfig::default()` —
510    /// matches the pre-existing implicit behaviour.
511    #[expect(
512        clippy::struct_field_names,
513        reason = "name mirrors the type for grep-ability across the persist + Tree handle init wiring"
514    )]
515    pub(crate) initial_runtime_config: crate::runtime_config::RuntimeConfig,
516
517    /// Filter construction policy
518    pub filter_policy: FilterPolicy,
519
520    /// Retrieval-ribbon locator policy (per level). Defaults to
521    /// [`LocatorPolicy::block_level`]: written SSTs carry an optional `locator`
522    /// section mapping each key to its data block for O(1) point reads (skipping
523    /// the index-block binary search). Set [`LocatorPolicy::disabled`] to opt
524    /// out — disabled levels produce byte-identical SSTs (no section).
525    pub locator_policy: LocatorPolicy,
526
527    /// Compaction filter factory
528    pub compaction_filter_factory: Option<Arc<dyn Factory>>,
529
530    /// Prefix extractor for prefix bloom filters.
531    ///
532    /// When set, the bloom filter indexes extracted prefixes in addition to
533    /// full keys, allowing prefix scans to skip segments that contain no
534    /// matching prefixes.
535    pub prefix_extractor: Option<Arc<dyn PrefixExtractor>>,
536
537    /// Merge operator for commutative operations
538    ///
539    /// When set, enables `merge()` operations that store partial updates
540    /// which are lazily combined during reads and compaction.
541    pub merge_operator: Option<Arc<dyn MergeOperator>>,
542
543    #[doc(hidden)]
544    pub kv_separation_opts: Option<KvSeparationOptions>,
545
546    /// Custom user key comparator.
547    ///
548    /// When set, all key comparisons use this comparator instead of the
549    /// default lexicographic byte ordering. Once a tree is opened with a
550    /// comparator, it must always be re-opened with the same comparator.
551    // Not `pub` — use `Config::comparator()` builder method as the public API.
552    #[doc(hidden)]
553    pub(crate) comparator: SharedComparator,
554
555    /// Block-level encryption provider for encryption at rest.
556    ///
557    /// When set, all blocks (data, index, filter, meta) are encrypted
558    /// using this provider after compression and before checksumming.
559    pub(crate) encryption: Option<Arc<dyn EncryptionProvider>>,
560
561    /// Policy governing what `Tree::open` does when the on-disk
562    /// MANIFEST contains corrupt records. Defaults to
563    /// [`ManifestRecoveryMode::AbsoluteConsistency`], the only
564    /// production-safe choice — any corruption aborts the open. Other
565    /// modes trade strict correctness for partial-availability after a
566    /// disaster; see the enum doc for the operational scenarios that
567    /// motivate each mode.
568    pub(crate) manifest_recovery_mode: ManifestRecoveryMode,
569
570    /// Durability level for every fsync the tree issues (SST writes,
571    /// manifest, version persist, directory syncs).
572    ///
573    /// Defaults to [`SyncMode::Normal`] (plain `fsync`), matching the
574    /// out-of-the-box durability of `RocksDB` and `SQLite`. Only observable on
575    /// macOS, where [`SyncMode::Full`] opts into the much slower
576    /// `F_FULLFSYNC` barrier; on other platforms both modes are plain
577    /// `fsync`. Set via [`Config::sync_mode`].
578    pub(crate) sync_mode: SyncMode,
579
580    /// When `true` (the default), [`Config::open`] and [`Config::repair`]
581    /// acquire an exclusive cross-process lock on a `LOCK` file in the tree
582    /// directory (an advisory OS file lock) and hold it for the lifetime of the
583    /// [`Tree`] (open) or the duration of the call (repair). A
584    /// second process attempting to open / repair the same directory fails fast
585    /// with [`Error::Locked`](crate::Error::Locked) instead of racing on the
586    /// manifest. Set `false` via [`Config::with_directory_lock`] only when the
587    /// embedder already enforces exclusive directory ownership at a higher layer
588    /// (e.g. a keyspace / journal manager). Best-effort per `Fs` backend: real
589    /// on-disk backends enforce it, in-memory backends are single-process and
590    /// satisfy it vacuously.
591    pub(crate) directory_lock: bool,
592
593    /// Shared live-progress counters the repair / salvage paths tick while
594    /// they run, or `None` (the default) to skip publishing. Set via
595    /// [`Config::with_recovery_progress`]; observed by polling
596    /// [`RecoveryProgress::snapshot`](crate::RecoveryProgress::snapshot) from
597    /// another thread.
598    #[cfg(feature = "std")]
599    pub(crate) recovery_progress: Option<Arc<crate::RecoveryProgress>>,
600
601    /// Edit-log size (bytes) past which the next manifest persist rotates: it
602    /// writes a fresh full snapshot and starts an empty log instead of appending
603    /// another [`VersionEdit`](crate::version::edit::VersionEdit). Bounds both
604    /// recovery replay time (edits to re-apply) and log disk use, while keeping
605    /// the common per-flush path a tiny `O(changed-levels)` append rather than an
606    /// `O(all-SSTs)` full manifest rewrite.
607    ///
608    /// Defaults to 1 MiB (≈ tens of thousands of edits). Set via
609    /// [`Config::manifest_log_rotate_bytes`]. A smaller value rotates more
610    /// often (shorter recovery, more frequent full-snapshot writes); `0` rotates
611    /// on every upgrade, degenerating to the full-rewrite-per-version behaviour.
612    pub(crate) manifest_log_rotate_bytes: u64,
613
614    /// Retention floor a manifest REPAIR seeds the rebuilt version with: the
615    /// highest snapshot seqno the repaired tree refuses to serve. The lost
616    /// manifest was the only record of the floor a past GC compaction or
617    /// `clear` established, and the tables cannot stand in for it, so the
618    /// deployment that ran those compactions (and knows the watermark it
619    /// passed) supplies it here. Defaults to `0`: a repaired tree serves
620    /// every snapshot, which is right only if history was never collected.
621    /// Set via [`Config::repair_retention_floor`].
622    pub(crate) repair_retention_floor: crate::SeqNo,
623
624    /// Compaction I/O rate limit in bytes per second.
625    ///
626    /// Caps the rate at which the compaction worker is allowed to issue
627    /// I/O, so background compaction cannot saturate the device and starve
628    /// user point reads / range scans (P99 stability). `0` (the default)
629    /// means unlimited — no throttling, no behaviour change. Flush and
630    /// user reads are never throttled, only compaction. Set via
631    /// [`Config::compaction_rate_limit`].
632    pub(crate) compaction_rate_limit: u64,
633
634    /// Worker-thread count for compaction parallelism (`std` only), used two
635    /// ways: it sizes the per-tree block-compression pool built at open when
636    /// [`Self::compaction_pool`] is `None`, and it caps how many range-parallel
637    /// sub-compactions a single compaction is split into. Default
638    /// `max(1, available_parallelism / 2)` — leaves half the cores for
639    /// application work. `1` forces the serial path for both. Without the
640    /// `parallel` feature there is no built-in pool, so block compression and
641    /// sub-compaction ranges run serially even for a value > 1. Set via
642    /// [`Config::compaction_threads`].
643    #[cfg(feature = "std")] // no-std: parallel compaction unavailable (no threads)
644    pub(crate) compaction_threads: usize,
645
646    /// Optional shared compaction thread pool. `None` (default) = a per-tree
647    /// pool is built at [`crate::Tree::open`] sized by [`Self::compaction_threads`]
648    /// (predictable, matches the per-DB pattern). `Some` = caller-supplied
649    /// executor shared across every tree holding this `Arc`, bounding total
650    /// threads regardless of tree count. Set via [`Config::compaction_pool`].
651    #[cfg(feature = "std")]
652    pub(crate) compaction_pool: Option<Arc<dyn crate::table::writer::CompactionSpawner>>,
653
654    /// Minimum total input size (bytes) for a compaction to be split into
655    /// parallel sub-compactions. Below it the compaction stays single-threaded
656    /// (per-thread setup + extra output tables outweigh the parallelism on small
657    /// compactions). Default
658    /// [`SUBCOMPACTION_MIN_INPUT_BYTES`](crate::compaction::worker::SUBCOMPACTION_MIN_INPUT_BYTES)
659    /// (8 MiB). Set via [`Config::subcompaction_min_bytes`].
660    #[cfg(feature = "std")]
661    pub(crate) subcompaction_min_bytes: u64,
662
663    /// Test-only failpoint: when armed, the first parallel sub-compaction range
664    /// that observes it returns an error and disarms it, so the crash-safety
665    /// rollback paths (sibling output rollback, input restore) can be exercised
666    /// deterministically. Behind `cfg(test)`, never compiled into release builds.
667    #[cfg(all(test, feature = "std"))]
668    pub(crate) fail_one_subcompaction: Arc<core::sync::atomic::AtomicBool>,
669
670    /// Test-only failpoint: when armed, a tight-space compaction returns an error
671    /// immediately after durably installing (and punching) its FIRST slice, so
672    /// the crash-mid-loop recovery path (reopen a tree whose manifest carries a
673    /// persisted input restriction) can be exercised deterministically. Behind
674    /// `cfg(test)`, never compiled into release builds.
675    #[cfg(all(test, feature = "std"))]
676    pub(crate) fail_tight_after_first_slice: Arc<core::sync::atomic::AtomicBool>,
677
678    /// Test-only failpoint: when armed, a tight-space relocation fails at the
679    /// restricted-blob reopen step of its current slice — after the slice's
680    /// outputs were finalized but before the install — so the pre-install
681    /// rollback (retract the finalized-but-unreferenced outputs) can be
682    /// exercised deterministically. Behind `cfg(test)`, never compiled into
683    /// release builds.
684    #[cfg(all(test, feature = "std"))]
685    pub(crate) fail_tight_blob_reopen: Arc<core::sync::atomic::AtomicBool>,
686
687    /// Pre-trained zstd dictionary for dictionary compression.
688    ///
689    /// When set together with a [`CompressionType::ZstdDict`] compression
690    /// policy, data blocks are compressed using this dictionary. The
691    /// dictionary must remain the same for the lifetime of the tree —
692    /// opening a tree with a different dictionary will produce
693    /// [`Error::ZstdDictMismatch`](crate::Error::ZstdDictMismatch) errors.
694    #[cfg(zstd_any)]
695    pub(crate) zstd_dictionary: Option<Arc<crate::compression::ZstdDictionary>>,
696
697    /// The global sequence number generator.
698    ///
699    /// Should be shared between multiple trees of a database.
700    pub(crate) seqno: SharedSequenceNumberGenerator,
701
702    /// Sequence number watermark that is visible to readers.
703    ///
704    /// Used for MVCC snapshots and to control which updates are
705    /// observable in a given view of the database.
706    pub(crate) visible_seqno: SharedSequenceNumberGenerator,
707}
708
709// TODO: remove default?
710// std-only: the default backend is `StdFs` and the default path is resolved
711// via std::path::absolute. no_std callers construct `Config` explicitly with a
712// caller-provided `Fs`.
713#[cfg(feature = "std")]
714impl Default for Config {
715    fn default() -> Self {
716        Self {
717            path: absolute_path(Path::new(DEFAULT_FILE_FOLDER)),
718            fs: Arc::new(StdFs),
719            level_routes: None,
720            descriptor_table: Some(Arc::new(DescriptorTable::new(256))),
721            seqno: SharedSequenceNumberGenerator::from(SequenceNumberCounter::default()),
722            visible_seqno: SharedSequenceNumberGenerator::from(SequenceNumberCounter::default()),
723
724            cache: Arc::new(Cache::with_capacity_bytes(
725                /* 16 MiB */ 16 * 1_024 * 1_024,
726            )),
727
728            data_block_restart_interval_policy: RestartIntervalPolicy::all(16),
729            index_block_restart_interval_policy: RestartIntervalPolicy::all(1),
730
731            level_count: DEFAULT_LEVEL_COUNT,
732
733            data_block_size_policy: BlockSizePolicy::all(4_096),
734
735            index_block_pinning_policy: PinningPolicy::new([true, true, false]),
736            filter_block_pinning_policy: PinningPolicy::new([true, false]),
737
738            top_level_index_block_pinning_policy: PinningPolicy::all(true), // TODO: implement
739            top_level_filter_block_pinning_policy: PinningPolicy::all(true), // TODO: implement
740
741            // Partitioned at every level so a bit-flip inside one
742            // sub-index block only takes out the keys covered by that
743            // partition, not the entire SST. A full-index SST has no
744            // within-block redundancy: one corrupt byte in the single
745            // index block makes every data block in the table
746            // unreachable. See tests/partitioned_index_blast_radius.rs
747            // for the isolation property this default relies on.
748            index_block_partitioning_policy: PinningPolicy::all(true),
749            // Filter-block default intentionally left at the pre-#329
750            // shape (L3+ only). A corrupt filter block can produce a
751            // false negative (filter says "not present" → read short-
752            // circuits → caller misses an existing key), which is a
753            // correctness hazard distinct from index corruption (where
754            // the read fails loudly). Flipping this default is tracked
755            // as a separate decision pending a filter blast-radius /
756            // false-negative analysis; symmetry with index is not
757            // sufficient justification on its own.
758            filter_block_partitioning_policy: PinningPolicy::new([false, false, false, true]),
759
760            index_block_partition_size_policy: BlockSizePolicy::all(4_096), // TODO: implement
761            filter_block_partition_size_policy: BlockSizePolicy::all(4_096), // TODO: implement
762
763            data_block_compression_policy: ({
764                #[cfg(feature = "lz4")]
765                let c = CompressionPolicy::new([CompressionType::None, CompressionType::Lz4]);
766
767                #[cfg(not(feature = "lz4"))]
768                let c = CompressionPolicy::new([CompressionType::None]);
769
770                c
771            }),
772            index_block_compression_policy: CompressionPolicy::all(CompressionType::None),
773
774            data_block_hash_ratio_policy: HashRatioPolicy::all(0.0),
775
776            locator_policy: LocatorPolicy::block_level(),
777            filter_policy: FilterPolicy::all(FilterPolicyEntry::Bloom(
778                BloomConstructionPolicy::BitsPerKey(10.0),
779            )),
780
781            compaction_filter_factory: None,
782            merge_operator: None,
783
784            prefix_extractor: None,
785
786            expect_point_read_hits: false,
787
788            page_ecc: false,
789
790            initial_runtime_config: crate::runtime_config::RuntimeConfig::default(),
791
792            kv_separation_opts: None,
793
794            #[cfg(zstd_any)]
795            zstd_dictionary: None,
796
797            comparator: comparator::default_comparator(),
798            encryption: None,
799            manifest_recovery_mode: ManifestRecoveryMode::AbsoluteConsistency,
800            sync_mode: SyncMode::Normal,
801            directory_lock: true,
802            #[cfg(feature = "std")]
803            recovery_progress: None,
804            manifest_log_rotate_bytes: 1024 * 1024,
805            repair_retention_floor: 0,
806            compaction_rate_limit: 0,
807
808            #[cfg(feature = "std")]
809            compaction_threads: std::thread::available_parallelism()
810                .map_or(1, |n| (n.get() / 2).max(1)),
811            #[cfg(feature = "std")]
812            compaction_pool: None,
813            #[cfg(feature = "std")]
814            subcompaction_min_bytes: crate::compaction::worker::SUBCOMPACTION_MIN_INPUT_BYTES,
815            #[cfg(all(test, feature = "std"))]
816            fail_one_subcompaction: Arc::new(core::sync::atomic::AtomicBool::new(false)),
817            #[cfg(all(test, feature = "std"))]
818            fail_tight_after_first_slice: Arc::new(core::sync::atomic::AtomicBool::new(false)),
819            #[cfg(all(test, feature = "std"))]
820            fail_tight_blob_reopen: Arc::new(core::sync::atomic::AtomicBool::new(false)),
821        }
822    }
823}
824
825/// Name of the lock file created in a tree directory for the cross-process
826/// exclusive directory lock.
827#[cfg_attr(
828    not(feature = "std"),
829    allow(
830        dead_code,
831        reason = "directory-lock filename used only by the std-gated lock-acquisition path"
832    )
833)]
834pub(crate) const DIRECTORY_LOCK_FILE: &str = "LOCK";
835
836/// Acquires the cross-process exclusive directory lock when `enabled`.
837///
838/// Opens (creating if absent) a `LOCK` file under `dir` and takes a
839/// non-blocking exclusive advisory lock on it through the `Fs` backend. Returns
840/// the locked handle to hold for as long as exclusivity is required; dropping it
841/// releases the lock (the OS frees an advisory lock when the fd / handle
842/// closes). `Ok(None)` when `enabled` is false. Fails with
843/// [`Error::Locked`](crate::Error::Locked) when another live instance holds the
844/// lock. The directory must already exist (the caller creates it for a fresh
845/// tree before acquiring).
846#[cfg(feature = "std")]
847pub(crate) fn acquire_directory_lock(
848    fs: &dyn Fs,
849    dir: &Path,
850    enabled: bool,
851) -> crate::Result<Option<Box<dyn crate::fs::FsFile>>> {
852    if !enabled {
853        return Ok(None);
854    }
855    let lock_path = dir.join(DIRECTORY_LOCK_FILE);
856    let file = fs.open(
857        &lock_path,
858        &crate::fs::FsOpenOptions::new()
859            .read(true)
860            .write(true)
861            .create(true),
862    )?;
863    if file.try_lock_exclusive()? {
864        Ok(Some(file))
865    } else {
866        Err(crate::Error::Locked(dir.display().to_string()))
867    }
868}
869
870impl Config {
871    /// Initializes a new config
872    // std-only: seeds the remaining fields from `Config::default`, whose
873    // default `Fs` is `StdFs`. no_std callers build `Config` field-by-field
874    // with a caller-provided `Fs`.
875    #[cfg(feature = "std")]
876    pub fn new<P: AsRef<Path>>(
877        path: P,
878        seqno: SequenceNumberCounter,
879        visible_seqno: SequenceNumberCounter,
880    ) -> Self {
881        Self {
882            path: absolute_path(path.as_ref()),
883            seqno: Arc::new(seqno),
884            visible_seqno: Arc::new(visible_seqno),
885            ..Default::default()
886        }
887    }
888
889    /// Sets the default filesystem backend used for levels without an explicit route.
890    ///
891    /// Defaults to [`StdFs`]. Use [`MemFs`](crate::fs::MemFs) for
892    /// in-memory trees (testing, ephemeral indexes).
893    ///
894    /// # Example
895    ///
896    /// ```
897    /// # fn main() -> lsm_tree::Result<()> {
898    /// use lsm_tree::{Config, SequenceNumberCounter};
899    /// use lsm_tree::fs::MemFs;
900    ///
901    /// let tree = Config::new(
902    ///     "/virtual/tree",
903    ///     SequenceNumberCounter::default(),
904    ///     SequenceNumberCounter::default(),
905    /// )
906    /// .with_fs(MemFs::new())
907    /// .open()?;
908    /// # Ok(())
909    /// # }
910    /// ```
911    #[must_use]
912    pub fn with_fs<F: Fs>(mut self, fs: F) -> Self {
913        self.fs = Arc::new(fs);
914        self
915    }
916
917    /// Sets the default filesystem backend from an existing shared handle.
918    ///
919    /// Useful when multiple configs should reuse the same backend
920    /// instance, including trait objects and backends that are not `Clone`.
921    ///
922    #[must_use]
923    pub fn with_shared_fs(mut self, fs: Arc<dyn Fs>) -> Self {
924        self.fs = fs;
925        self
926    }
927
928    /// Opens a tree using the config.
929    ///
930    /// # Errors
931    ///
932    /// Will return `Err` if an IO error occurs.
933    /// Returns [`Error::ZstdDictMismatch`](crate::Error::ZstdDictMismatch) if
934    /// the compression policy references a `dict_id` that doesn't match the
935    /// configured dictionary.
936    pub fn open(self) -> crate::Result<AnyTree> {
937        #[cfg(zstd_any)]
938        self.validate_zstd_dictionary()?;
939
940        // On a zstd build the live block path seals encrypted blocks through
941        // the AAD-bound envelope, so the configured provider MUST implement it.
942        // Reject an opaque-only provider here, at open time, instead of letting
943        // it fail on the first encrypted read/write.
944        #[cfg(zstd_any)]
945        if self
946            .encryption
947            .as_ref()
948            .is_some_and(|enc| !enc.supports_aad_block_path())
949        {
950            return Err(crate::Error::Encrypt(
951                "encryption provider does not implement the AAD-bound block path \
952                 (encrypt_block_aad / decrypt_block_aad) required for encrypted \
953                 blocks on a zstd build",
954            ));
955        }
956
957        Ok(if self.kv_separation_opts.is_some() {
958            AnyTree::Blob(BlobTree::open(self)?)
959        } else {
960            AnyTree::Standard(Tree::open(self)?)
961        })
962    }
963
964    /// Validates that every `ZstdDict` entry in compression policies references
965    /// a `dict_id` that matches the configured dictionary. Catches mismatches
966    /// at open time rather than at first block write/read.
967    #[cfg(zstd_any)]
968    fn validate_zstd_dictionary(&self) -> crate::Result<()> {
969        let dict_id = self.zstd_dictionary.as_ref().map(|d| d.id());
970
971        // NOTE: Only data block policies are validated. Index blocks never
972        // carry a dictionary — Writer::use_index_block_compression() downgrades
973        // ZstdDict to plain Zstd. Validating index policies here would reject
974        // configs that use ZstdDict solely for index blocks even though the
975        // writer handles them correctly.
976        for ct in self.data_block_compression_policy.iter() {
977            if let &CompressionType::ZstdDict {
978                dict_id: required, ..
979            } = ct
980            {
981                match dict_id {
982                    None => {
983                        return Err(crate::Error::ZstdDictMismatch {
984                            expected: required,
985                            got: None,
986                        });
987                    }
988                    Some(actual) if actual != required => {
989                        return Err(crate::Error::ZstdDictMismatch {
990                            expected: required,
991                            got: Some(actual),
992                        });
993                    }
994                    _ => {}
995                }
996            }
997        }
998
999        // Blob files with ZstdDict compression must have a matching dictionary.
1000        if let Some(ref kv_opts) = self.kv_separation_opts
1001            && let CompressionType::ZstdDict {
1002                dict_id: required, ..
1003            } = kv_opts.compression
1004        {
1005            match kv_opts.zstd_dictionary.as_ref().map(|d| d.id()) {
1006                None => {
1007                    return Err(crate::Error::ZstdDictMismatch {
1008                        expected: required,
1009                        got: None,
1010                    });
1011                }
1012                Some(actual) if actual != required => {
1013                    return Err(crate::Error::ZstdDictMismatch {
1014                        expected: required,
1015                        got: Some(actual),
1016                    });
1017                }
1018                _ => {}
1019            }
1020        }
1021
1022        Ok(())
1023    }
1024
1025    /// Like [`Config::new`], but accepts pre-built shared generators.
1026    ///
1027    /// This is useful when the caller already has
1028    /// [`SharedSequenceNumberGenerator`] instances (e.g., from a higher-level
1029    /// database that shares generators across multiple trees).
1030    // std-only: see [`Config::new`] — seeds via `Config::default` (`StdFs`).
1031    #[cfg(feature = "std")]
1032    pub fn new_with_generators<P: AsRef<Path>>(
1033        path: P,
1034        seqno: SharedSequenceNumberGenerator,
1035        visible_seqno: SharedSequenceNumberGenerator,
1036    ) -> Self {
1037        Self {
1038            path: absolute_path(path.as_ref()),
1039            seqno,
1040            visible_seqno,
1041            ..Default::default()
1042        }
1043    }
1044}
1045
1046#[cfg(all(test, zstd_any))]
1047mod tests;
1048
1049impl Config {
1050    /// Returns the tables folder path and [`Fs`] backend for the given level.
1051    ///
1052    /// If [`level_routes`](Self::level_routes) has an entry covering this
1053    /// level, uses that entry's path and `Fs`. Otherwise falls back to the
1054    /// primary [`path`](Self::path) and [`fs`](Self::fs).
1055    #[must_use]
1056    pub fn tables_folder_for_level(&self, level: u8) -> (PathBuf, Arc<dyn Fs>) {
1057        if let Some(routes) = &self.level_routes {
1058            for route in routes {
1059                if route.levels.contains(&level) {
1060                    return (route.path.join(TABLES_FOLDER), route.fs.clone());
1061                }
1062            }
1063        }
1064        (self.path.join(TABLES_FOLDER), self.fs.clone())
1065    }
1066
1067    /// Best-effort minimum free space (bytes) across every filesystem this tree
1068    /// writes to: the primary [`path`](Self::path) plus each
1069    /// [`level_routes`](Self::level_routes) volume.
1070    ///
1071    /// The tightest volume bounds storage admission and compaction space gating,
1072    /// since a full routed (cold-tier) volume fails a flush / compaction
1073    /// targeting it even while the primary still has room. A backend that cannot
1074    /// report free space (or an I/O hiccup) contributes `u64::MAX`, so a probe
1075    /// failure never fabricates disk pressure.
1076    #[must_use]
1077    pub(crate) fn min_available_space(&self) -> u64 {
1078        let mut free = self.fs.available_space(&self.path).unwrap_or(u64::MAX);
1079        if let Some(routes) = &self.level_routes {
1080            for route in routes {
1081                free = free.min(route.fs.available_space(&route.path).unwrap_or(u64::MAX));
1082            }
1083        }
1084        free
1085    }
1086
1087    /// Returns all unique tables folders that need to be scanned during
1088    /// recovery: the primary folder plus every [`LevelRoute`] folder.
1089    #[must_use]
1090    pub fn all_tables_folders(&self) -> Vec<(PathBuf, Arc<dyn Fs>)> {
1091        let primary_fs: Arc<dyn Fs> = self.fs.clone();
1092        let mut folders: Vec<(PathBuf, Arc<dyn Fs>)> =
1093            vec![(self.path.join(TABLES_FOLDER), primary_fs)];
1094
1095        if let Some(routes) = &self.level_routes {
1096            for route in routes {
1097                let folder = route.path.join(TABLES_FOLDER);
1098                // Dedup by path: scanning the same directory twice would cause
1099                // already-recovered tables to be classified as orphans and
1100                // deleted. Routing the same path through different Fs backends
1101                // is a configuration error (level_routes validation in
1102                // Config::level_routes rejects overlapping ranges).
1103                if !folders.iter().any(|(p, _)| *p == folder) {
1104                    folders.push((folder, route.fs.clone()));
1105                }
1106            }
1107        }
1108
1109        folders
1110    }
1111
1112    /// Configures per-level filesystem routing for tiered storage.
1113    ///
1114    /// Each [`LevelRoute`] maps a range of LSM levels to a base directory
1115    /// and filesystem backend. Levels not covered by any route fall back to
1116    /// the primary `path` and `fs`.
1117    ///
1118    /// # Reopen contract
1119    ///
1120    /// The route configuration is **not persisted** in the manifest.
1121    /// On reopen, the [`Config`] must specify `level_routes` such that
1122    /// [`all_tables_folders`](Self::all_tables_folders) includes every
1123    /// directory and filesystem pair that may contain existing SST files
1124    /// for this tree.
1125    ///
1126    /// Changing the mapping from levels to paths is allowed as long as
1127    /// the previously used folders remain covered. If old folders are
1128    /// omitted, recovery may fail with
1129    /// [`RouteMismatch`](crate::Error::RouteMismatch) (when all missing
1130    /// tables are on uncovered levels) or
1131    /// [`Unrecoverable`](crate::Error::Unrecoverable) (when some missing
1132    /// tables are on levels that are still covered).
1133    ///
1134    /// # Panics
1135    ///
1136    /// Panics if any route has an empty range or if any two routes have
1137    /// overlapping level ranges.
1138    #[must_use]
1139    pub fn level_routes(mut self, routes: Vec<LevelRoute>) -> Self {
1140        // Validate no empty/inverted ranges
1141        for route in &routes {
1142            assert!(
1143                route.levels.start < route.levels.end,
1144                "empty or inverted level route range: {:?}",
1145                route.levels,
1146            );
1147        }
1148
1149        // Validate no overlapping ranges
1150        for (i, a) in routes.iter().enumerate() {
1151            for b in routes.iter().skip(i + 1) {
1152                assert!(
1153                    a.levels.end <= b.levels.start || b.levels.end <= a.levels.start,
1154                    "overlapping level routes: {:?} and {:?}",
1155                    a.levels,
1156                    b.levels,
1157                );
1158            }
1159        }
1160        self.level_routes = if routes.is_empty() {
1161            None
1162        } else {
1163            // Normalize paths the same way Config::new normalizes self.path
1164            Some(
1165                routes
1166                    .into_iter()
1167                    .map(|mut r| {
1168                        r.path = absolute_path(&r.path);
1169                        r
1170                    })
1171                    .collect(),
1172            )
1173        };
1174        self
1175    }
1176
1177    /// Overrides the sequence number generator.
1178    ///
1179    /// By default, [`SequenceNumberCounter`] is used. This allows plugging in
1180    /// a custom generator (e.g., HLC for distributed databases).
1181    #[must_use]
1182    pub fn seqno_generator(mut self, generator: SharedSequenceNumberGenerator) -> Self {
1183        self.seqno = generator;
1184        self
1185    }
1186
1187    /// Overrides the visible sequence number generator.
1188    #[must_use]
1189    pub fn visible_seqno_generator(mut self, generator: SharedSequenceNumberGenerator) -> Self {
1190        self.visible_seqno = generator;
1191        self
1192    }
1193
1194    /// Sets the global cache.
1195    ///
1196    /// You can create a global [`Cache`] and share it between multiple
1197    /// trees to cap global cache memory usage.
1198    ///
1199    /// Defaults to a cache with 16 MiB of capacity *per tree*.
1200    #[must_use]
1201    pub fn use_cache(mut self, cache: Arc<Cache>) -> Self {
1202        self.cache = cache;
1203        self
1204    }
1205
1206    /// Sets the file descriptor cache.
1207    ///
1208    /// Can be shared across trees.
1209    #[must_use]
1210    pub fn use_descriptor_table(mut self, descriptor_table: Option<Arc<DescriptorTable>>) -> Self {
1211        self.descriptor_table = descriptor_table;
1212        self
1213    }
1214
1215    /// If `true`, the last level will not build filters, reducing the filter size of a database
1216    /// by ~90% typically.
1217    ///
1218    /// **Enable this only if you know that point reads generally are expected to find a key-value pair.**
1219    #[must_use]
1220    pub fn expect_point_read_hits(mut self, b: bool) -> Self {
1221        self.expect_point_read_hits = b;
1222        self
1223    }
1224
1225    /// Enables per-block Page ECC.
1226    ///
1227    /// When enabled, every block written by this tree carries a parity
1228    /// trailer; on read, if the block's XXH3 disagrees with the on-disk
1229    /// bytes, the reader attempts recovery from the trailer before surfacing
1230    /// the corruption. The correction scheme defaults to per-word SEC-DED and
1231    /// is selectable at runtime (`update_runtime_config`): per-word SEC-DED,
1232    /// single XOR parity, or Reed-Solomon.
1233    ///
1234    /// Opening a tree with `page_ecc = true` on a build that does not
1235    /// have the `page_ecc` cargo feature enabled returns
1236    /// [`crate::Error::PageEccUnsupported`] at `Tree::open` — the
1237    /// reader has no way to honour the parity trailer without the
1238    /// codec, so silently downgrading integrity is not an option.
1239    ///
1240    /// Wired into the on-disk write path via `MultiWriter::use_page_ecc`
1241    /// at every `Tree::open` / `Tree::ingestion` / compaction-worker
1242    /// `MultiWriter` construction site. With this flag set, every
1243    /// `Block::write_into` call those writers make upgrades its
1244    /// `BlockTransform` to the matching `*Ecc` variant — emitting the
1245    /// configured scheme's parity trailer and setting the `ECC_PARITY` flag
1246    /// in each block header (the trailer length is derived from
1247    /// `data_length`, not stored).
1248    #[must_use]
1249    pub fn page_ecc(mut self, enabled: bool) -> Self {
1250        self.page_ecc = enabled;
1251        self
1252    }
1253
1254    /// Enables or disables the cross-process directory lock (default: enabled).
1255    ///
1256    /// When enabled, [`Config::open`] and [`Config::repair`] acquire an
1257    /// exclusive advisory lock on a `LOCK` file in the tree directory, so a
1258    /// second process opening / repairing the same directory fails fast with
1259    /// [`Error::Locked`](crate::Error::Locked) rather than corrupting the shared
1260    /// manifest. Disable ONLY when exclusive directory ownership is already
1261    /// guaranteed at a higher layer (e.g. an embedding keyspace / journal
1262    /// manager that opens each directory at most once per host).
1263    #[must_use]
1264    pub fn with_directory_lock(mut self, enabled: bool) -> Self {
1265        self.directory_lock = enabled;
1266        self
1267    }
1268
1269    /// Wires shared live-progress counters into the repair / salvage paths.
1270    ///
1271    /// A repair over a large store streams every SST and blob file and can run
1272    /// for a long time; the handle set here is ticked as files are discovered
1273    /// and blocks / rows are recovered, so another thread can poll
1274    /// [`RecoveryProgress::snapshot`](crate::RecoveryProgress::snapshot) while
1275    /// [`Config::repair`] (or a salvage it triggers) runs. Without it, repair
1276    /// publishes no progress (zero overhead).
1277    ///
1278    /// # Examples
1279    ///
1280    /// ```no_run
1281    /// use lsm_tree::{Config, RecoveryProgress, SequenceNumberCounter};
1282    /// use std::sync::Arc;
1283    ///
1284    /// let progress = Arc::new(RecoveryProgress::default());
1285    /// let config = Config::new(
1286    ///     "my-tree",
1287    ///     SequenceNumberCounter::default(),
1288    ///     SequenceNumberCounter::default(),
1289    /// )
1290    /// .with_recovery_progress(progress.clone());
1291    /// // spawn the repair, then poll `progress.snapshot()` elsewhere
1292    /// let report = config.repair()?;
1293    /// # Ok::<_, lsm_tree::Error>(())
1294    /// ```
1295    #[cfg(feature = "std")]
1296    #[must_use]
1297    pub fn with_recovery_progress(mut self, progress: Arc<crate::RecoveryProgress>) -> Self {
1298        self.recovery_progress = Some(progress);
1299        self
1300    }
1301
1302    /// Sets the Page ECC scheme used when [`Self::page_ecc`] is enabled.
1303    ///
1304    /// ECC is off until `page_ecc(true)`. When on, this picks the
1305    /// algorithm:
1306    /// [`EccScheme::Secded`](crate::runtime_config::EccScheme::Secded)
1307    /// (per-word single-bit correct / double-bit detect, the default, supported
1308    /// at Block granularity),
1309    /// [`EccScheme::Xor`](crate::runtime_config::EccScheme::Xor) (RAID-5
1310    /// single-parity), or
1311    /// [`EccScheme::ReedSolomon`](crate::runtime_config::EccScheme::ReedSolomon).
1312    /// There is no implicit RS(4,2) default.
1313    #[must_use]
1314    pub fn ecc_scheme(mut self, scheme: crate::runtime_config::EccScheme) -> Self {
1315        self.initial_runtime_config.ecc_scheme = scheme;
1316        self
1317    }
1318
1319    /// Sets whether the writer clears per-file copy-on-write on newly created
1320    /// SST / blob files when the backing filesystem is copy-on-write (Btrfs).
1321    ///
1322    /// Default `true`: write-once SSTs gain no benefit from `CoW` but suffer a
1323    /// fragmentation penalty (~20% write throughput on Btrfs), so clearing it
1324    /// recovers the ext4-equivalent baseline. A no-op on non-`CoW` filesystems.
1325    /// Set `false` to preserve `CoW` (e.g. Btrfs subvolume snapshots that depend
1326    /// on it). See [`crate::runtime_config::RuntimeConfig::disable_cow_on_sst_files`].
1327    #[must_use]
1328    pub fn disable_cow_on_sst_files(mut self, enabled: bool) -> Self {
1329        self.initial_runtime_config.disable_cow_on_sst_files = enabled;
1330        self
1331    }
1332
1333    /// Sets whether [`crate::AbstractTree::create_checkpoint`] clones files via
1334    /// reflink (`FICLONE` / `clonefile`) when the filesystem supports it,
1335    /// falling back to a hard link otherwise.
1336    ///
1337    /// Default `true`: a reflinked checkpoint has an independent inode (no
1338    /// max-links constraint, modifications never touch the original) at O(1)
1339    /// cost via copy-on-write block sharing. A no-op (hard-link path) on
1340    /// filesystems without reflink. See
1341    /// [`crate::runtime_config::RuntimeConfig::use_reflink_for_checkpoint`].
1342    #[must_use]
1343    pub fn use_reflink_for_checkpoint(mut self, enabled: bool) -> Self {
1344        self.initial_runtime_config.use_reflink_for_checkpoint = enabled;
1345        self
1346    }
1347
1348    /// Sets the initial [`crate::runtime_config::RuntimeConfig`]
1349    /// snapshot the tree will start with.
1350    ///
1351    /// Seeds both the first manifest write and the live
1352    /// `RuntimeConfigHandle` exposed via
1353    /// [`crate::Tree::runtime_config`].
1354    ///
1355    /// **Manifest-hardening toggles** in the supplied snapshot
1356    /// that are currently wired through the writer
1357    /// (`manifest_footer_mirror`, `page_ecc` *as consumed by
1358    /// `manifest_blocks::writer` when picking the `BlockTransform`
1359    /// variant*) take effect from byte zero of the on-disk
1360    /// manifest rather than waiting for a post-open
1361    /// [`crate::Tree::update_runtime_config`] call. Subsequent
1362    /// updates still flow through the live handle and apply to
1363    /// the next manifest write.
1364    ///
1365    /// `manifest_kv_checksums` is plumbed in the snapshot but the
1366    /// writer does NOT yet consult or persist it (per-entry
1367    /// framing + footer-flag slot land in a follow-up). Setting
1368    /// it here today has no on-disk effect; it is exposed for
1369    /// forward-compat with no behaviour break.
1370    ///
1371    /// **Note on data-block ECC:** `RuntimeConfig::page_ecc`
1372    /// currently affects manifest Blocks only — data-block ECC is
1373    /// still gated by [`Config::page_ecc`] at tree-open time. The
1374    /// SST writer path consumes the tree-static config, not the
1375    /// runtime handle. Wiring through SST emission is a follow-up.
1376    #[must_use]
1377    pub fn with_runtime_config(mut self, runtime: crate::runtime_config::RuntimeConfig) -> Self {
1378        self.initial_runtime_config = runtime;
1379        self
1380    }
1381
1382    /// Sets the partitioning policy for filter blocks.
1383    #[must_use]
1384    pub fn filter_block_partitioning_policy(mut self, policy: PinningPolicy) -> Self {
1385        self.filter_block_partitioning_policy = policy;
1386        self
1387    }
1388
1389    /// Sets the partitioning policy for index blocks.
1390    #[must_use]
1391    pub fn index_block_partitioning_policy(mut self, policy: PinningPolicy) -> Self {
1392        self.index_block_partitioning_policy = policy;
1393        self
1394    }
1395
1396    /// Sets the pinning policy for filter blocks.
1397    #[must_use]
1398    pub fn filter_block_pinning_policy(mut self, policy: PinningPolicy) -> Self {
1399        self.filter_block_pinning_policy = policy;
1400        self
1401    }
1402
1403    /// Sets the pinning policy for index blocks.
1404    #[must_use]
1405    pub fn index_block_pinning_policy(mut self, policy: PinningPolicy) -> Self {
1406        self.index_block_pinning_policy = policy;
1407        self
1408    }
1409
1410    /// Sets the restart interval inside data blocks.
1411    ///
1412    /// A higher restart interval saves space while increasing lookup times
1413    /// inside data blocks.
1414    ///
1415    /// Default = 16
1416    ///
1417    /// # Panics
1418    ///
1419    /// Panics if any restart interval in `policy` is zero.
1420    #[must_use]
1421    pub fn data_block_restart_interval_policy(mut self, policy: RestartIntervalPolicy) -> Self {
1422        assert!(
1423            policy.iter().all(|interval| *interval > 0),
1424            "data block restart interval must be greater than zero",
1425        );
1426        self.data_block_restart_interval_policy = policy;
1427        self
1428    }
1429
1430    /// Sets the restart interval inside index blocks.
1431    ///
1432    /// A higher restart interval saves space while increasing lookup times
1433    /// inside index blocks.
1434    ///
1435    /// Default = 1
1436    ///
1437    /// # Panics
1438    ///
1439    /// Panics if any restart interval in `policy` is zero.
1440    #[must_use]
1441    pub fn index_block_restart_interval_policy(mut self, policy: RestartIntervalPolicy) -> Self {
1442        assert!(
1443            policy.iter().all(|interval| *interval > 0),
1444            "index block restart interval must be greater than zero",
1445        );
1446        self.index_block_restart_interval_policy = policy;
1447        self
1448    }
1449
1450    /// Sets the filter construction policy.
1451    #[must_use]
1452    pub fn filter_policy(mut self, policy: FilterPolicy) -> Self {
1453        self.filter_policy = policy;
1454        self
1455    }
1456
1457    /// Sets the retrieval-ribbon locator policy.
1458    ///
1459    /// On by default at [`LocatorPrecision::Block`] (see
1460    /// [`LocatorPolicy::block_level`]). When enabled for a level, written SSTs on
1461    /// that level carry an optional `locator` section mapping each key to its
1462    /// data block (and, at finer precisions, its slot), letting point reads skip
1463    /// the index-block binary search. Set [`LocatorPolicy::disabled`] to opt out;
1464    /// disabled levels emit byte-identical SSTs.
1465    #[must_use]
1466    pub fn locator_policy(mut self, policy: LocatorPolicy) -> Self {
1467        self.locator_policy = policy;
1468        self
1469    }
1470
1471    /// Sets the compression method for data blocks.
1472    #[must_use]
1473    pub fn data_block_compression_policy(mut self, policy: CompressionPolicy) -> Self {
1474        self.data_block_compression_policy = policy;
1475        self
1476    }
1477
1478    /// Sets the compression method for index blocks.
1479    #[must_use]
1480    pub fn index_block_compression_policy(mut self, policy: CompressionPolicy) -> Self {
1481        self.index_block_compression_policy = policy;
1482        self
1483    }
1484
1485    // TODO: level count is fixed to 7 right now
1486    // /// Sets the number of levels of the LSM tree (depth of tree).
1487    // ///
1488    // /// Defaults to 7, like `LevelDB` and `RocksDB`.
1489    // ///
1490    // /// Cannot be changed once set.
1491    // ///
1492    // /// # Panics
1493    // ///
1494    // /// Panics if `n` is 0.
1495    // #[must_use]
1496    // pub fn level_count(mut self, n: u8) -> Self {
1497    //     assert!(n > 0);
1498
1499    //     self.level_count = n;
1500    //     self
1501    // }
1502
1503    /// Sets the data block size policy.
1504    #[must_use]
1505    pub fn data_block_size_policy(mut self, policy: BlockSizePolicy) -> Self {
1506        self.data_block_size_policy = policy;
1507        self
1508    }
1509
1510    /// Sets the hash ratio policy for data blocks.
1511    ///
1512    /// If greater than 0.0, a hash index is embedded into data blocks that can speed up reads
1513    /// inside the data block.
1514    #[must_use]
1515    pub fn data_block_hash_ratio_policy(mut self, policy: HashRatioPolicy) -> Self {
1516        self.data_block_hash_ratio_policy = policy;
1517        self
1518    }
1519
1520    /// Toggles key-value separation.
1521    #[must_use]
1522    pub fn with_kv_separation(mut self, opts: Option<KvSeparationOptions>) -> Self {
1523        self.kv_separation_opts = opts;
1524        self
1525    }
1526
1527    /// Installs a custom compaction filter.
1528    #[must_use]
1529    pub fn with_compaction_filter_factory(mut self, factory: Option<Arc<dyn Factory>>) -> Self {
1530        self.compaction_filter_factory = factory;
1531        self
1532    }
1533
1534    /// Sets the prefix extractor for prefix bloom filters.
1535    ///
1536    /// When configured, bloom filters will index key prefixes returned by
1537    /// the extractor. Prefix scans can then skip segments whose bloom
1538    /// filter reports no match for the scan prefix.
1539    #[must_use]
1540    pub fn prefix_extractor(mut self, extractor: Arc<dyn PrefixExtractor>) -> Self {
1541        self.prefix_extractor = Some(extractor);
1542        self
1543    }
1544
1545    /// Installs a merge operator for commutative operations.
1546    ///
1547    /// When set, enables [`crate::AbstractTree::merge`] which stores partial updates
1548    /// (operands) that are lazily combined during reads and compaction.
1549    #[must_use]
1550    pub fn with_merge_operator(mut self, op: Option<Arc<dyn MergeOperator>>) -> Self {
1551        self.merge_operator = op;
1552        self
1553    }
1554
1555    /// Sets a custom user key comparator.
1556    ///
1557    /// When configured, all key ordering (memtable, block index, merge,
1558    /// range scans) uses this comparator instead of the default lexicographic
1559    /// byte ordering.
1560    ///
1561    /// # Important
1562    ///
1563    /// The comparator's [`crate::UserComparator::name`] is persisted when a tree is
1564    /// first created. On subsequent opens the stored name is compared against
1565    /// the supplied comparator's name — a mismatch causes the open to fail
1566    /// with [`Error::ComparatorMismatch`](crate::Error::ComparatorMismatch).
1567    #[must_use]
1568    pub fn comparator(mut self, comparator: SharedComparator) -> Self {
1569        self.comparator = comparator;
1570        self
1571    }
1572
1573    /// Sets the block-level encryption provider for encryption at rest.
1574    ///
1575    /// When set, all blocks written to SST files are encrypted after
1576    /// compression and before checksumming, using the provided
1577    /// [`EncryptionProvider`].
1578    ///
1579    /// The caller is responsible for key management and rotation.
1580    /// See `crate::Aes256GcmProvider` (behind the `encryption` feature)
1581    /// for a ready-to-use AES-256-GCM implementation.
1582    ///
1583    /// **Important constraints:**
1584    /// - Encryption state is NOT recorded in SST metadata. Opening an
1585    ///   encrypted tree without the correct provider (or vice versa) will
1586    ///   cause block validation errors, not silent corruption.
1587    /// - Blob files (KV-separated large values) are NOT covered by
1588    ///   block-level encryption. Large values stored via KV separation
1589    ///   remain in plaintext on disk.
1590    #[must_use]
1591    pub fn with_encryption(mut self, encryption: Option<Arc<dyn EncryptionProvider>>) -> Self {
1592        self.encryption = encryption;
1593        self
1594    }
1595
1596    /// Sets the MANIFEST recovery policy for `Tree::open`.
1597    ///
1598    /// The default ([`ManifestRecoveryMode::AbsoluteConsistency`]) is the
1599    /// only choice that's safe for live production: any corrupt record
1600    /// in the on-disk manifest aborts the open. Switching to a more
1601    /// permissive mode trades strict correctness for partial
1602    /// availability after a disaster. The recovery path emits a
1603    /// `warn!` summary per affected section (aggregate counts: total
1604    /// table records dropped, total blob-file records dropped,
1605    /// header truncations) rather than one log line per dropped
1606    /// record — the dropped records were never decoded in the first
1607    /// place, so no per-record IDs are available. Always pair the
1608    /// non-default modes with an out-of-band integrity scan
1609    /// ([`verify_integrity`](crate::verify::verify_integrity) for
1610    /// whole-file XXH3 over every SST + blob file, or
1611    /// [`verify_block_checksums`](crate::verify::verify_block_checksums)
1612    /// for per-block granularity) before trusting the recovered tree
1613    /// for writes.
1614    ///
1615    /// See the [`ManifestRecoveryMode`] doc for per-variant semantics.
1616    #[must_use]
1617    pub fn manifest_recovery_mode(mut self, mode: ManifestRecoveryMode) -> Self {
1618        self.manifest_recovery_mode = mode;
1619        self
1620    }
1621
1622    /// Sets the durability level for every fsync the tree issues.
1623    ///
1624    /// Defaults to [`SyncMode::Normal`] (plain `fsync`, matching `RocksDB` /
1625    /// `SQLite` defaults). Pass [`SyncMode::Full`] to force `F_FULLFSYNC` on
1626    /// macOS for power-loss durability without an external journal — at a
1627    /// large per-flush cost. On non-macOS platforms both modes are
1628    /// identical (plain `fsync`).
1629    #[must_use]
1630    pub fn sync_mode(mut self, mode: SyncMode) -> Self {
1631        self.sync_mode = mode;
1632        self
1633    }
1634
1635    /// Sets the edit-log rotation threshold in bytes (default 1 MiB).
1636    ///
1637    /// Once the manifest edit log exceeds this size, the next version upgrade
1638    /// writes a fresh full snapshot and starts an empty log instead of appending
1639    /// another edit. Lower it to shorten recovery replay and cap log size at the
1640    /// cost of more frequent full-snapshot writes; `0` rotates on every upgrade.
1641    #[must_use]
1642    pub fn manifest_log_rotate_bytes(mut self, bytes: u64) -> Self {
1643        self.manifest_log_rotate_bytes = bytes;
1644        self
1645    }
1646
1647    /// Sets the retention floor a manifest repair seeds the rebuilt tree with
1648    /// (default `0`): after [`repair`](Self::repair) /
1649    /// [`open_or_repair`](Self::open_or_repair) every snapshot at or below
1650    /// `floor` is refused with
1651    /// [`Error::SnapshotBelowRetention`](crate::Error::SnapshotBelowRetention),
1652    /// exactly as the tree refused it before the manifest was lost.
1653    ///
1654    /// A normal open needs no help: the manifest carries the floor every
1655    /// retention-advancing install established (a GC compaction, `clear`, a
1656    /// table drop, a filtering compaction; see
1657    /// [`AbstractTree::retention_floor`](crate::AbstractTree::retention_floor)).
1658    /// A repair rebuilds the manifest from the tables, which do not record it
1659    /// (a GC compaction zeroes the seqnos of the rows it settles), so only the
1660    /// deployment knows it: record
1661    /// [`retention_floor()`](crate::AbstractTree::retention_floor) in your own
1662    /// durable state (it already folds in every operation that raised the
1663    /// boundary, so no per-operation bookkeeping is needed) and pass the last
1664    /// recorded value here. Left at `0`, a repaired tree serves every
1665    /// snapshot, which is correct only if history was never collected. Has no
1666    /// effect on an open that finds a manifest.
1667    #[must_use]
1668    pub fn repair_retention_floor(mut self, floor: crate::SeqNo) -> Self {
1669        self.repair_retention_floor = floor;
1670        self
1671    }
1672
1673    /// Sets the compaction I/O rate limit in bytes per second.
1674    ///
1675    /// Caps how fast the compaction worker may issue I/O so background
1676    /// compaction does not saturate the device and spike user read P99.
1677    /// `0` (the default) disables throttling. Only compaction is limited;
1678    /// flush and user reads always pass through.
1679    #[must_use]
1680    pub fn compaction_rate_limit(mut self, bytes_per_sec: u64) -> Self {
1681        self.compaction_rate_limit = bytes_per_sec;
1682        self
1683    }
1684
1685    /// Sets the compaction worker-thread count.
1686    ///
1687    /// Under `std` this both sizes the per-tree block-compression pool built at
1688    /// open when no shared pool is supplied (see [`Self::compaction_pool`]) and
1689    /// caps how many range-parallel sub-compactions a compaction splits into.
1690    /// `1` keeps compaction serial. Default is `max(1, available_parallelism /
1691    /// 2)`. Without the `parallel` feature there is no built-in pool, so the
1692    /// work runs serially even for a value > 1.
1693    #[cfg(feature = "std")]
1694    #[must_use]
1695    pub fn compaction_threads(mut self, threads: usize) -> Self {
1696        // Clamp to >= 1: the documented semantics treat `1` as "serial", and a
1697        // 0-thread pool would be an invalid state.
1698        self.compaction_threads = threads.max(1);
1699        self
1700    }
1701
1702    /// Sets the minimum total input size (bytes) for a compaction to be split
1703    /// into parallel sub-compactions. Default 8 MiB. `0` splits every eligible
1704    /// compaction; a large value effectively disables sub-compaction (block
1705    /// compression still parallelizes via [`Self::compaction_threads`]).
1706    #[cfg(feature = "std")]
1707    #[must_use]
1708    pub fn subcompaction_min_bytes(mut self, bytes: u64) -> Self {
1709        self.subcompaction_min_bytes = bytes;
1710        self
1711    }
1712
1713    /// Supplies a shared compaction thread pool, used in place of the per-tree
1714    /// default. Pass one [`crate::table::writer::CompactionSpawner`] (e.g. a
1715    /// `RayonSpawner` wrapping a shared rayon thread pool) to several trees so
1716    /// the total worker-thread count stays bounded by the pool size rather than
1717    /// the number of open trees.
1718    #[cfg(feature = "std")]
1719    #[must_use]
1720    pub fn compaction_pool(
1721        mut self,
1722        pool: Option<Arc<dyn crate::table::writer::CompactionSpawner>>,
1723    ) -> Self {
1724        self.compaction_pool = pool;
1725        self
1726    }
1727
1728    /// Sets the pre-trained zstd dictionary for dictionary compression.
1729    ///
1730    /// When set, data blocks using [`CompressionType::ZstdDict`] will be
1731    /// compressed and decompressed with this dictionary. The dictionary
1732    /// should be trained on representative data samples for best results.
1733    ///
1734    /// Create a dictionary with [`ZstdDictionary::new`](crate::ZstdDictionary::new),
1735    /// then use [`CompressionType::zstd_dict`] to create a matching
1736    /// compression type:
1737    ///
1738    /// ```ignore
1739    /// use lsm_tree::{CompressionType, ZstdDictionary};
1740    ///
1741    /// let dict = ZstdDictionary::new(&training_data);
1742    /// let compression = CompressionType::zstd_dict(3, dict.id()).unwrap();
1743    ///
1744    /// config
1745    ///     .zstd_dictionary(Some(Arc::new(dict)))
1746    ///     .data_block_compression_policy(CompressionPolicy::all(compression));
1747    /// ```
1748    #[cfg(zstd_any)]
1749    #[must_use]
1750    pub fn zstd_dictionary(
1751        mut self,
1752        dictionary: Option<Arc<crate::compression::ZstdDictionary>>,
1753    ) -> Self {
1754        self.zstd_dictionary = dictionary;
1755        self
1756    }
1757}
1758
1759#[cfg(test)]
1760mod builder_tests;