Skip to main content

lsm_tree/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-present, fjall-rs
3// Copyright (c) 2026-present, Dmitry Prudnikov
4
5// no-std foundation: when the `std` feature is OFF the crate root opts into
6// `no_std`. Default builds keep `std` enabled (file I/O, threading, system
7// clock all live in `std`), so existing consumers see no behaviour change.
8// The migration to a fully no-std-clean build is incremental. Two patterns
9// coexist while the port is in progress:
10//   1. Modules with std-only dependencies that have no consumers above the
11//      `fs` / `version` / `tree` layer stay gated behind `#[cfg(feature =
12//      "std")]` and are ported in isolation.
13//   2. Modules whose std-only dependencies cascade through every consumer
14//      (`manifest_blocks`, vendored `sfa`, others noted at their `pub mod`
15//      site) remain UNCONDITIONAL today — gating them would require
16//      `#[cfg]` annotations on dozens of call sites without changing the
17//      no-std error count, because those call sites are themselves
18//      std-bound for unrelated reasons. They migrate in lockstep with the
19//      consumer layer rather than ahead of it.
20// The CI job `no-std-check` exercises `cargo check --no-default-features
21// --features alloc` against a real no-std target (`thumbv7em-none-eabihf`)
22// and tracks remaining work via the error count, which must monotonically
23// decrease across PRs.
24#![cfg_attr(not(feature = "std"), no_std)]
25
26//! Embedded LSM-tree storage engine.
27//!
28//! Provides keyed point reads, prefix and range scans, MVCC snapshots, block
29//! and file-descriptor caching, and a configurable compaction subsystem. No
30//! write-ahead log — durability is the caller's responsibility (`flush_active_memtable`
31//! forces persistence when needed).
32//!
33//! ## Highlights
34//!
35//! - **AMQ filter**: `BuRR` (Bumped Ribbon Retrieval, Walzer & Dillinger 2022) for
36//!   per-key and per-prefix membership checks. ~30% smaller filter blocks than a
37//!   same-FPR Bloom filter, or ~10× tighter FPR at the same memory budget.
38//! - **Compression**: pure-Rust zstd (incl. dictionary mode), LZ4, or none —
39//!   per-table and per-level policy.
40//! - **Encryption at rest**: AES-256-GCM block encryption with a caller-supplied
41//!   key.
42//! - **Range tombstones**: `delete_range` / `delete_prefix` (SST-encoded; the
43//!   feature was added in disk format V4 and remains supported in the current
44//!   V5 format — the V5 break extends the block header for per-block Reed-
45//!   Solomon Page ECC, not the tombstone encoding).
46//! - **Merge operators**: commutative-merge LSM operations with lazy resolution.
47//! - **K/V separation (`BlobTree`)**: large-value workloads with automatic GC.
48//! - **Pluggable `Fs`**: standard, in-memory, `io_uring`, or custom backends.
49//! - **MVCC**: snapshot reads at a chosen `SeqNo`, custom `UserComparator`.
50//! - **Concurrency**: thread-safe `BTreeMap`-like API.
51//!
52//! Keys: up to 65,535 bytes (`u16` length field). Values: up to 4,294,967,295
53//! bytes (`u32` length field, `2³² − 1`). Larger keys and values
54//! carry a proportional performance cost.
55//!
56//! ## Quick start
57//!
58//! ```no_run
59//! use lsm_tree::{AbstractTree, Config, SequenceNumberCounter};
60//!
61//! let folder = tempfile::tempdir().unwrap();
62//! let seqno = SequenceNumberCounter::default();
63//! let tree = Config::new(&folder, seqno.clone(), SequenceNumberCounter::default())
64//!     .open()
65//!     .unwrap();
66//!
67//! tree.insert("key", "value", seqno.next());
68//! let value = tree.get("key", lsm_tree::SeqNo::MAX).unwrap();
69//! assert_eq!(value.map(|v| v.to_vec()), Some(b"value".to_vec()));
70//! ```
71//!
72//! ## On-disk format
73//!
74//! Current version: **V5**. V5 introduces the `BuRR` filter wire format,
75//! per-block Reed-Solomon Page ECC, and per-entry (per-KV) checksum
76//! footers (collapsed into the same version because V5 had not shipped
77//! when they landed): the self-describing block types (`Meta` / `Manifest` /
78//! `ManifestFooter`) gain a `block_flags` byte whose `ECC_PARITY` bit marks a
79//! parity trailer and whose `KV_CHECKSUM_FOOTER` bit marks a per-entry
80//! checksum footer. SST block types (`Data` / `Index` / `Filter` /
81//! `RangeTombstone`) keep the compact header WITHOUT that byte and derive parity / footer
82//! presence from the per-SST meta descriptors (`descriptor#page_ecc`,
83//! `descriptor#kv_checksum`). The block magic is bumped so a pre-V5 reader
84//! rejects V5 blocks immediately at header decode.
85//! A V5 SST written with every optional transform off (no Page ECC, no per-KV
86//! footers) is still NOT byte-identical to a pre-V5 table — the bumped block
87//! magic and the per-SST meta descriptor keys always differ. Any
88//! "byte-identical when off" guarantees in the feature docs are within-V5 and
89//! payload-level (e.g. index entries when `seqno_in_index = false`), not a
90//! cross-version equivalence.
91//! V3-V4 databases are not readable by this version and vice versa. The
92//! manifest version gate rejects pre-V5 databases at `Tree::open` time.
93//! V4 introduced range tombstones (still supported).
94#![deny(clippy::all, missing_docs, clippy::cargo)]
95#![deny(clippy::unwrap_used)]
96#![deny(clippy::indexing_slicing)]
97#![warn(clippy::pedantic, clippy::nursery)]
98#![warn(clippy::expect_used)]
99#![allow(clippy::missing_const_for_fn)]
100#![warn(clippy::multiple_crate_versions)]
101#![allow(clippy::option_if_let_else)]
102#![warn(clippy::redundant_feature_names)]
103// The `#[test_log::test]` attribute macro expands to a body that places
104// `use` items after statements, which trips `items_after_statements` in
105// the test build (`clippy --all-targets`). The lint fires only on the
106// macro's generated code, not on anything hand-written, so allow it
107// crate-wide rather than annotating every instrumented test.
108#![allow(clippy::items_after_statements)]
109// Test fixtures routinely bind closely-named locals (`dict_a` / `dict_b`,
110// `tree_1` / `tree_2`) where the similarity is the point; `similar_names`
111// is pedantic noise on that style.
112#![allow(clippy::similar_names)]
113// Long scenario tests (fuzz harnesses, multi-phase integration cases)
114// legitimately run past the `too_many_lines` ceiling; splitting them
115// would obscure the scenario. The lint is only reached under
116// `--all-targets`, which lints test bodies.
117#![allow(clippy::too_many_lines)]
118#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
119
120// `alloc` is the minimal hard dependency — the crate uses `Arc`, `Vec`,
121// `Box`, and other heap types throughout. `extern crate alloc` makes the
122// `alloc` crate root visible to `no_std` builds; under `std` it is a
123// no-op alias because the standard library re-exports the same types.
124#[macro_use]
125extern crate alloc;
126
127// f64 ceil / round: the native `f64` methods are std-only (they bind to
128// platform float intrinsics), so the `no_std` build routes through `libm`.
129// std keeps the native path (equal or faster); the results are identical.
130#[cfg(feature = "std")]
131#[inline]
132pub(crate) fn f64_ceil(x: f64) -> f64 {
133    x.ceil()
134}
135#[cfg(not(feature = "std"))]
136#[inline]
137pub(crate) fn f64_ceil(x: f64) -> f64 {
138    libm::ceil(x)
139}
140#[cfg(feature = "std")]
141#[inline]
142pub(crate) fn f32_round(x: f32) -> f32 {
143    x.round()
144}
145#[cfg(not(feature = "std"))]
146#[inline]
147pub(crate) fn f32_round(x: f32) -> f32 {
148    libm::roundf(x)
149}
150#[cfg(feature = "std")]
151#[inline]
152pub(crate) fn f32_ceil(x: f32) -> f32 {
153    x.ceil()
154}
155#[cfg(not(feature = "std"))]
156#[inline]
157pub(crate) fn f32_ceil(x: f32) -> f32 {
158    libm::ceilf(x)
159}
160#[cfg(feature = "std")]
161#[inline]
162pub(crate) fn f64_log2(x: f64) -> f64 {
163    x.log2()
164}
165#[cfg(not(feature = "std"))]
166#[inline]
167pub(crate) fn f64_log2(x: f64) -> f64 {
168    libm::log2(x)
169}
170#[cfg(feature = "std")]
171#[inline]
172pub(crate) fn f32_log2(x: f32) -> f32 {
173    x.log2()
174}
175#[cfg(not(feature = "std"))]
176#[inline]
177pub(crate) fn f32_log2(x: f32) -> f32 {
178    libm::log2f(x)
179}
180
181// `hashbrown` (not `std::collections`) so the crate-wide map / set aliases
182// compile on `no_std + alloc`. hashbrown IS the implementation std's HashMap
183// wraps, so the API and performance match; using it directly just drops the
184// std dependency. Hasher stays `FxHasher` (fast, non-DoS — internal keys are
185// not attacker-controlled).
186#[doc(hidden)]
187pub type HashMap<K, V> = hashbrown::HashMap<K, V, rustc_hash::FxBuildHasher>;
188
189pub(crate) type HashSet<K> = hashbrown::HashSet<K, rustc_hash::FxBuildHasher>;
190
191macro_rules! fail_iter {
192    ($e:expr) => {
193        match $e {
194            Ok(v) => v,
195            Err(e) => return Some(Err(e.into())),
196        }
197    };
198}
199
200macro_rules! unwrap {
201    ($x:expr) => {{ $x.expect("should read") }};
202}
203
204mod any_tree;
205
206mod abstract_tree;
207
208pub(crate) mod deletion_pause;
209pub mod heal_hints;
210
211/// Computed write-backpressure verdict (caller-honoured, see [`backpressure`]).
212pub mod backpressure;
213
214// `checkpoint` is `pub(crate)`: it contains internal helpers
215// (`link_or_copy_cross_fs`, `prepare_target`, `run_checkpoint`) used by
216// `Tree::create_checkpoint` and `BlobTree::create_checkpoint`. Exposing
217// it via `pub` (even with `#[doc(hidden)]`) would lock the helpers into
218// the stable surface; tests that need to exercise them live inline as
219// unit tests inside `src/checkpoint.rs`.
220#[cfg(feature = "std")]
221pub(crate) mod checkpoint;
222
223#[doc(hidden)]
224pub mod blob_tree;
225
226// Vendored, `no_std`-ported `byteview` (backs `Slice`); kept in-tree so the
227// engine carries no external dependency that fails to compile on `no_std`.
228mod byteview;
229
230mod comparator;
231
232#[doc(hidden)]
233mod cache;
234
235/// In-tree sharded S3-FIFO cache backing `cache` and `descriptor_table`
236/// (replaces `quick_cache`; works on `std` and `no_std + alloc`).
237mod sharded_cache;
238
239#[doc(hidden)]
240pub mod checksum;
241
242#[doc(hidden)]
243pub mod coding;
244
245pub mod compaction;
246#[doc(hidden)]
247pub mod compression;
248
249/// Block-level encryption at rest.
250pub mod encryption;
251
252/// Configuration
253pub mod config;
254
255#[doc(hidden)]
256pub mod descriptor_table;
257
258/// Shard-based Page ECC (XOR single-parity and Reed-Solomon).
259///
260/// Gated behind the `page_ecc` cargo feature so the
261/// `reed-solomon-simd` dependency is only pulled in when the feature
262/// is enabled.
263#[cfg(feature = "page_ecc")]
264pub mod ecc;
265
266#[doc(hidden)]
267pub mod file_accessor;
268
269mod double_ended_peekable;
270mod error;
271
272#[doc(hidden)]
273pub mod file;
274
275/// Pluggable filesystem abstraction for I/O backends.
276pub mod fs;
277
278pub mod hash;
279
280/// Local I/O trait surface mirroring `std::io::{Read, Write, Seek}`.
281///
282/// Provides `Error` / `ErrorKind` / `SeekFrom` plus the three trait
283/// definitions so the bounds on the [`fs`] traits no longer carry
284/// `std::io::*` directly. Under the `std` feature, supertrait
285/// aliases + blanket impls forward to `std::io` types so existing
286/// std-backed backends satisfy the trait surface automatically; the
287/// alias form also propagates BACK to `std::io`, so a `dyn FsFile`
288/// bounded on `crate::io::Read` still flows into `std::io::BufReader`,
289/// `byteorder`, and friends.
290///
291/// Scope: this prerequisite slice (per #311) lifts only
292/// `Read`/`Write`/`Seek` out of the [`fs`] trait bounds. The
293/// `io::Result<T>` return types and `&Path` argument types in
294/// `fs::Fs` / `fs::FsFile` still resolve to `std::io::Result<T>` and
295/// `std::path::Path` and migrate in follow-up commits; the full
296/// `--no-default-features --features alloc` build of the `fs::*`
297/// surface arrives once those two follow-ups land.
298pub mod io;
299
300mod heap;
301mod ingestion;
302mod iter_guard;
303mod key;
304mod key_range;
305mod loser_tree;
306mod manifest;
307#[doc(hidden)]
308pub mod manifest_blocks;
309mod memtable;
310mod merge_operator;
311pub(crate) mod rate_limiter;
312mod reseek;
313mod run_reader;
314mod run_scanner;
315// Vendored sfa is std-only internally (`std::io` / `std::fs` /
316// `std::path`). Unconditional for the same cascading reason as
317// `manifest_blocks` above: ~20 consumers across the table /
318// blob-file / inspect / verify / checkpoint paths reference sfa
319// types in unconditional code. Gating sfa alone explodes the
320// `no-std-check` error count via unresolved-module failures on
321// every consumer that hasn't been gated yet. Migration is the
322// whole std-bound layer at once (tracked as issue #358), not sfa
323// in isolation.
324#[doc(hidden)]
325pub mod sfa;
326
327// Shared on-disk forgery helpers for corruption tests (std: reads/writes
328// real files through std::fs like the tests that consume it).
329#[cfg(all(test, feature = "std"))]
330pub(crate) mod test_forge;
331
332#[doc(hidden)]
333pub mod merge;
334
335#[doc(hidden)]
336pub mod merge_source;
337#[doc(hidden)]
338pub mod seeking_merger;
339
340#[cfg(feature = "metrics")]
341pub(crate) mod metrics;
342
343// mod multi_reader;
344
345#[doc(hidden)]
346pub mod mvcc_stream;
347
348mod path;
349mod pinnable_slice;
350mod prefix;
351
352#[doc(hidden)]
353pub mod range;
354
355/// Runtime-toggleable configuration (`RuntimeConfig` + atomic-swap handle).
356pub mod runtime_config;
357
358/// Disaster-recovery: rebuild a missing/corrupt manifest from on-disk SSTs.
359// std-only: scans table folders and rewrites the manifest via std::fs.
360#[cfg(feature = "std")]
361pub mod repair;
362
363// Tight-space restriction sidecar: `{id}.restrict-bound` records the exact
364// lower bound of a hole-punched SST next to it, so manifest repair recovers the
365// restriction without mutating (and thus invalidating the manifest checksum of)
366// the SST itself.
367#[cfg(feature = "std")]
368pub mod restrict_bound;
369
370// std-only: block-granular SST salvage; reads source blocks and writes a
371// recovered SST via std::fs (see also `crate::repair`, `crate::verify`).
372#[cfg(feature = "std")]
373pub mod salvage;
374
375/// Live progress counters for long-running recovery (repair / salvage).
376// std-only: its only producers (repair, salvage) are std-gated.
377#[cfg(feature = "std")]
378pub mod recovery_progress;
379
380pub(crate) mod active_tombstone_set;
381pub(crate) mod range_tombstone;
382pub(crate) mod range_tombstone_filter;
383
384#[doc(hidden)]
385pub mod table;
386
387mod background_deleter;
388mod scan_since;
389
390/// Single-error-correct / double-error-detect word codecs for the Page ECC
391/// read path (pluggable SEC-DED shapes; default Hsiao `(72, 64)`).
392///
393/// Gated behind `page_ecc` and crate-internal: the codec only runs on the
394/// Page ECC recovery path. Trailer sizing on the read path uses a plain
395/// `ceil(len / 8)` so reading a SEC-DED SST does not require this module.
396#[cfg(feature = "page_ecc")]
397pub(crate) mod secded;
398
399mod seqno;
400mod slice;
401mod slice_windows;
402
403#[doc(hidden)]
404pub mod stop_signal;
405
406mod format_version;
407mod time;
408mod tree;
409
410pub use time::Clock;
411#[cfg(feature = "std")]
412pub use time::SystemClock;
413#[cfg(not(feature = "std"))]
414pub use time::set_clock;
415
416/// Utility functions
417pub mod util;
418
419mod value;
420mod value_type;
421mod write_batch;
422
423/// Integrity verification for SST and blob files.
424///
425/// The block-level scrub (per-block + per-KV checksum walking) runs over the
426/// injected [`Fs`](crate::fs::Fs) backend through `crate::io`, so it compiles on
427/// `no_std` (serial path; the multi-threaded fan-out and the full-file
428/// hash-by-path convenience stay behind `std`).
429pub mod verify;
430
431/// Out-of-band inspection of a single SST file.
432///
433/// Public read-only view of stored metadata (table id, key range,
434/// counts, compression, timestamp) without spinning up a `Tree`.
435/// Used by `sst-dump properties` and similar diagnostic tools. See
436/// the module docs for the recovery semantics (mirrors
437/// `Table::recover`'s TAIL-first / MID-fallback path from #295).
438#[cfg(feature = "std")]
439pub mod inspect;
440
441/// ECC patrol scrub: a proactive sweep over Page-ECC-protected SST blocks.
442///
443/// Reads blocks to detect and correct latent bit-rot before it accumulates
444/// past the parity budget. Std-gated for the same reason as [`verify`]: the
445/// sweep needs real filesystem I/O and thread-based parallelism.
446#[cfg(feature = "std")]
447pub mod scrub;
448
449pub mod storage_stats;
450pub use storage_stats::{
451    ApproximateRangeStats, LevelStats, RangeCardinality, SegmentStats, StorageStatistics,
452    StorageStats, StorageStatus,
453};
454
455mod version;
456mod vlog;
457
458// Reproducible single-byte-bitrot heal/read fuzzer. `#[ignore]`d, so it is
459// excluded from the normal suite and run as its own CI step; needs crate-internal
460// `Table` / `Writer` access, so it lives here rather than in `tests/`.
461#[cfg(all(test, feature = "std"))]
462mod fuzz_heal;
463
464/// User defined key (byte array)
465pub type UserKey = Slice;
466
467/// User defined data (byte array)
468pub type UserValue = Slice;
469
470/// KV-tuple (key + value)
471pub type KvPair = (UserKey, UserValue);
472
473// The `#[doc(hidden)]` block below re-exports crate internals that are reachable
474// at the crate root for benchmarks and integration tests, but are NOT part of the
475// public API contract — they carry no semver guarantee and may be renamed, moved,
476// or removed without a major version bump. External callers that import these
477// hidden items do so at their own risk. `cargo doc` excludes them from generated
478// rustdoc output; only intra-crate test/bench code is expected to use them.
479#[doc(hidden)]
480pub use {
481    checksum::Checksum,
482    iter_guard::{IterGuardImpl, SeekableGuardIter},
483    key_range::KeyRange,
484    merge::BoxedIterator,
485    slice::Builder,
486    // Re-exported for `benches/lsp.rs` only — see hidden-block contract above.
487    table::util::longest_shared_prefix_length,
488    table::{GlobalTableId, Table, TableId},
489    value::InternalValue,
490};
491
492#[doc(hidden)]
493pub use {
494    blob_tree::{Guard as BlobGuard, handle::BlobIndirection},
495    tree::Guard as StandardGuard,
496    tree::inner::TreeId,
497};
498
499pub use encryption::EncryptionProvider;
500
501#[cfg(feature = "encryption")]
502pub use encryption::Aes256GcmProvider;
503
504#[doc(hidden)]
505#[cfg(feature = "std")]
506pub use background_deleter::BackgroundDeleter;
507pub use pinnable_slice::PinnableSlice;
508#[cfg(feature = "std")]
509pub use recovery_progress::{RecoveryPhase, RecoveryProgress, RecoveryProgressSnapshot};
510#[cfg(feature = "std")]
511pub use repair::{RepairPolicy, RepairReport, WalReplayScope};
512pub use write_batch::WriteBatch;
513
514pub use {
515    cache::Cache,
516    comparator::{DefaultUserComparator, SharedComparator, UserComparator},
517    compression::CompressionType,
518    config::{Config, KvSeparationOptions, TreeType},
519    error::{Error, Result},
520    format_version::FormatVersion,
521    iter_guard::IterGuard as Guard,
522    memtable::{Memtable, MemtableId},
523    merge_operator::MergeOperator,
524    prefix::PrefixExtractor,
525    seqno::{
526        MAX_SEQNO, SequenceNumberCounter, SequenceNumberGenerator, SharedSequenceNumberGenerator,
527    },
528    slice::Slice,
529    value::SeqNo,
530    value_type::ValueType,
531};
532
533pub use {
534    abstract_tree::{AbstractTree, CheckpointInfo},
535    any_tree::AnyTree,
536    blob_tree::BlobTree,
537    descriptor_table::DescriptorTable,
538    ingestion::AnyIngestion,
539    scan_since::ScanSinceEvent,
540    tree::Tree,
541    vlog::BlobFile,
542};
543
544#[cfg(feature = "columnar")]
545pub use tree::columnar_scan::ColumnarScan;
546
547#[cfg(zstd_any)]
548pub use compression::ZstdDictionary;
549
550#[cfg(feature = "metrics")]
551pub use metrics::{CacheStats, Metrics};
552
553pub use backpressure::{Backpressure, BackpressureThresholds};
554
555#[cfg(feature = "std")]
556#[doc(hidden)]
557#[must_use]
558#[allow(missing_docs, clippy::missing_errors_doc, clippy::unwrap_used)]
559pub fn get_tmp_folder() -> tempfile::TempDir {
560    if let Ok(p) = std::env::var("LSMT_TMP_FOLDER") {
561        tempfile::tempdir_in(p)
562    } else {
563        tempfile::tempdir()
564    }
565    .unwrap()
566}