Skip to main content

regolith/
lib.rs

1#![doc(
2    html_logo_url = "https://raw.githubusercontent.com/sourcenetwork/regolith/main/art/regolith_banner_2048x512.png"
3)]
4//! Regolith: ACID, performance oriented, embedded key-value database engine for edge systems.
5//!
6//! Regolith provides a fast, embedded key-value store with:
7//! - **Read committed, snapshot isolation, or serializable** per
8//!   transaction, via MVCC sequence numbers
9//! - **Lock-free transactions** whose reads and writes take `&self`, so
10//!   one transaction can be shared across threads without a lock
11//! - **Crash recovery** via write-ahead logging (WAL)
12//! - **LZ4 compression** for data blocks
13//! - **Bloom filters** for fast negative lookups
14//! - **Level-based compaction** on a dedicated OS thread
15//! - **Lock-free reads** via an arena-backed skip list memtable
16//! - **Zero-copy reads** via [`DbSlice`], which borrows the bytes the
17//!   database already holds
18//!
19//! # Quick Start
20//!
21//! ```no_run
22//! use regolith::{Db, Options};
23//!
24//! let db = Db::open("/tmp/my_db", Options::default()).unwrap();
25//!
26//! // Write
27//! db.put(b"hello", b"world").unwrap();
28//!
29//! // Read
30//! let value = db.get(b"hello").unwrap();
31//! assert_eq!(value, Some(b"world".to_vec()));
32//!
33//! // Delete
34//! db.delete(b"hello").unwrap();
35//!
36//! // Batch write
37//! let mut batch = regolith::WriteBatch::new();
38//! batch.put(b"key1", b"val1");
39//! batch.put(b"key2", b"val2");
40//! batch.delete(b"key3");
41//! db.write(batch).unwrap();
42//!
43//! // Snapshot reads
44//! let snap = db.snapshot();
45//! db.put(b"key1", b"val_new").unwrap();
46//! // Snapshot still sees old value
47//! assert_eq!(snap.get(b"key1").unwrap(), Some(b"val1".to_vec()));
48//! ```
49
50// `unsafe` is confined to the modules that need raw pointers to hand
51// out zero-copy views of bytes the engine already owns. Every other
52// module inherits the crate-level deny.
53#![deny(unsafe_code)]
54#![warn(missing_docs)]
55
56mod backup;
57mod checkpoint;
58mod column_family;
59mod engine;
60pub mod env;
61mod error;
62mod event_listener;
63mod iter;
64mod mvcc;
65mod options;
66mod perf_context;
67mod portability;
68mod rate_limiter;
69mod slice;
70mod sst_file_writer;
71mod statistics;
72mod stream_writer;
73mod sync;
74mod tailing;
75mod transaction;
76mod ttl;
77mod txn_buffer;
78
79pub use backup::{BackupEngine, BackupId, BackupInfo};
80pub use checkpoint::Checkpoint;
81pub use column_family::{ColumnFamilyHandle, DEFAULT_CF_NAME};
82#[cfg(target_os = "wasi")]
83pub use env::WasiEnv;
84pub use env::{Capabilities, Env, MemEnv, StdEnv};
85pub use error::Error;
86pub use event_listener::{
87    BackgroundErrorReason, CompactionJobInfo, EventListener, ExternalFileIngestionInfo,
88    FlushJobInfo, TableFileCreationInfo, TableFileCreationReason, TableFileDeletionInfo,
89    WalFullInfo,
90};
91pub use iter::Iter;
92pub use options::{
93    ArenaProfile, CompactionDecision, CompactionFilter, CompactionStyle, CompressionType,
94    DEFAULT_MAX_BACKGROUND_COMPACTIONS, DEFAULT_MAX_KEY_SIZE, DEFAULT_MAX_VALUE_SIZE,
95    DEFAULT_TRANSACTION_KEYS_INLINE, DurabilityMode, FifoCompactionOptions, FixedLengthPrefix,
96    MAX_BLOCK_CACHE_SHARD_BITS, MAX_BLOOM_BITS_PER_KEY, MergeOperator, Options, PrefixExtractor,
97    UniversalCompactionOptions, WriteOptions,
98};
99pub use perf_context::{PerfContext, PerfContextSnapshot, PerfLevel};
100pub use rate_limiter::{Priority, RateLimiter, TokenBucketRateLimiter};
101pub use slice::DbSlice;
102pub use sst_file_writer::{IngestOptions, SstFileMeta, SstFileWriter};
103pub use statistics::{Histogram, HistogramSnapshot, Statistics, Ticker};
104pub use stream_writer::{StreamOptions, StreamingWriter};
105pub use tailing::TailingIter;
106pub use transaction::{
107    IsolationLevel, OptimisticTransactionDb, OwnedTransaction, ScanDirection, Transaction,
108    TransactionDb, TransactionError, TxResult, TxnScanStream,
109};
110pub use ttl::{DbWithTtl, TtlCompactionFilter, strip_timestamp};
111
112#[cfg(loom)]
113#[doc(hidden)]
114pub mod loom_exports {
115    //! Model-checking entry points, published only in a `--cfg loom`
116    //! build.
117    //!
118    //! `tests/loom_memtable.rs` is an integration test and so sees only
119    //! the public API, while everything the models drive - the arena,
120    //! the skip list, the memtable, the read horizon - is crate-private.
121    //! The models therefore live inside the crate, next to the code they
122    //! check, and this module is the seam that lets the test target call
123    //! them. It does not exist in an ordinary build.
124
125    pub use crate::engine::loom_model::{handoff, skiplist, slice, version};
126}
127
128#[cfg(feature = "fuzzing")]
129#[doc(hidden)]
130pub mod fuzzing {
131    //! Fuzz-only entry points for private on-disk decoders.
132    //!
133    //! These helpers intentionally swallow decoder results: fuzz targets
134    //! care that arbitrary bytes never panic or trigger undefined behavior.
135
136    use std::fs;
137    use std::path::{Path, PathBuf};
138    use std::sync::atomic::{AtomicU64, Ordering};
139
140    static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(0);
141
142    /// Decode arbitrary bytes as an SSTable data block.
143    pub fn decode_block(data: &[u8]) {
144        let _ = crate::engine::block::Block::decode_data_block(data.to_vec());
145    }
146
147    /// Decode arbitrary bytes as an SSTable range-tombstone block.
148    pub fn decode_range_tombstones(data: &[u8]) {
149        let _ = crate::engine::sstable::decode_range_tombstone_block(data);
150    }
151
152    /// Replay arbitrary bytes as a WAL file.
153    pub fn replay_wal(data: &[u8]) {
154        with_temp_file("wal", "log", data, |path| {
155            let Ok(mut iter) = crate::engine::wal_replay::WalReplayIter::open(
156                &crate::env::std_env(),
157                path,
158                crate::engine::wal_replay::WalPosition::Newest,
159            ) else {
160                return;
161            };
162            while matches!(iter.next_entry(), Ok(Some(_))) {}
163        });
164    }
165
166    /// Open arbitrary bytes as a complete SSTable file.
167    pub fn open_sst(data: &[u8]) {
168        with_temp_file("sst", "sst", data, |path| {
169            // `open_with`, not `open`: the latter is `#[cfg(test)]`, so under
170            // the `fuzzing` feature it does not exist and the crate does not build.
171            let _ = crate::engine::sstable::SsTableReader::open_with(
172                &crate::env::std_env(),
173                path,
174                0,
175                crate::engine::sstable::MetadataPolicy::Pinned,
176            );
177        });
178    }
179
180    /// Replay arbitrary bytes as a MANIFEST file.
181    pub fn replay_manifest(data: &[u8]) {
182        with_temp_dir("manifest", |db_dir| {
183            let sst_dir = db_dir.join("sst");
184            let manifest_path = db_dir.join("MANIFEST");
185            if fs::create_dir_all(&sst_dir).is_ok() && fs::write(&manifest_path, data).is_ok() {
186                let _ = crate::engine::manifest::VersionSet::open(db_dir, &sst_dir);
187            }
188        });
189    }
190
191    fn with_temp_file(label: &str, extension: &str, data: &[u8], f: impl FnOnce(&Path)) {
192        let path = temp_path(label).with_extension(extension);
193        if fs::write(&path, data).is_ok() {
194            f(&path);
195        }
196        let _ = fs::remove_file(path);
197    }
198
199    fn with_temp_dir(label: &str, f: impl FnOnce(&Path)) {
200        let path = temp_path(label);
201        if fs::create_dir_all(&path).is_ok() {
202            f(&path);
203        }
204        let _ = fs::remove_dir_all(path);
205    }
206
207    fn temp_path(label: &str) -> PathBuf {
208        let id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
209        std::env::temp_dir().join(format!("regolith-fuzz-{label}-{}-{id}", std::process::id()))
210    }
211}
212
213use column_family::{
214    CfRegistry, DEFAULT_CF_ID, META_CF_ID, cf_lower_bound, cf_upper_bound, meta, prefix_key,
215};
216use engine::lookup_key::LookupKey;
217
218use std::collections::BTreeMap;
219use std::path::Path;
220use std::sync::Arc;
221
222use engine::RegolithEngine;
223
224fn invalid_cf_handle_error(cf: &ColumnFamilyHandle) -> Error {
225    Error::invalid_column_family(format!(
226        "column family handle '{}' with id {} is not live",
227        cf.name(),
228        cf.id()
229    ))
230}
231
232fn invalid_input_error(message: impl Into<String>) -> Error {
233    Error::invalid_argument(message)
234}
235
236fn invalid_cf_id_error(cf_id: u32) -> Error {
237    Error::invalid_column_family(format!("column family id {cf_id} is not live"))
238}
239
240fn invalid_cf_id_io_error(cf_id: u32) -> std::io::Error {
241    std::io::Error::new(
242        std::io::ErrorKind::InvalidInput,
243        format!("column family id {cf_id} is not live"),
244    )
245}
246
247fn map_point_read_error(err: std::io::Error, prefixed_key: &[u8]) -> Error {
248    let user_key = prefixed_key.get(4..).unwrap_or(prefixed_key);
249    map_point_read_error_for(err, user_key)
250}
251
252/// [`map_point_read_error`] for a caller that already holds the user
253/// key. Stripping the column-family prefix a second time would eat
254/// four bytes of the key itself and report a truncated one, so the
255/// strip lives in exactly one place and both entries share the
256/// decision below.
257fn map_point_read_error_for(err: std::io::Error, user_key: &[u8]) -> Error {
258    if err.kind() == std::io::ErrorKind::InvalidData
259        && err.to_string().starts_with("merge operator ")
260    {
261        Error::MergeFailed(user_key.to_vec())
262    } else {
263        Error::from(err)
264    }
265}
266
267fn strip_cf_prefix_key(key: &[u8]) -> Result<Vec<u8>> {
268    key.get(4..)
269        .map(|user_key| user_key.to_vec())
270        .ok_or_else(|| Error::corruption("internal key is shorter than the column-family prefix"))
271}
272
273fn strip_cf_prefix_entries(raw: Vec<(Vec<u8>, Vec<u8>)>) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
274    raw.into_iter()
275        .map(|(k, v)| strip_cf_prefix_key(&k).map(|user_key| (user_key, v)))
276        .collect()
277}
278
279/// One bounded page of ordered scan results.
280///
281/// Returned by [`Db::scan_page`], [`Db::scan_page_cf`],
282/// [`Snapshot::scan_page`], and [`Snapshot::scan_page_cf`] when callers
283/// want an explicit memory cap without manually driving an iterator.
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct ScanPage {
286    /// Key-value pairs returned for this page, ordered by key.
287    pub entries: Vec<(Vec<u8>, Vec<u8>)>,
288    /// Inclusive start key to pass to the next page request, or
289    /// `None` when the requested range has been exhausted.
290    ///
291    /// This key is the first matching key that was not returned in
292    /// [`ScanPage::entries`].
293    pub next_start: Option<Vec<u8>>,
294}
295
296fn prefixed_cf_id(prefixed_key: &[u8]) -> std::io::Result<u32> {
297    let prefix = prefixed_key.get(..4).ok_or_else(|| {
298        std::io::Error::new(
299            std::io::ErrorKind::InvalidInput,
300            "prefixed key is shorter than the column-family id",
301        )
302    })?;
303    let mut bytes = [0; 4];
304    bytes.copy_from_slice(prefix);
305    Ok(u32::from_be_bytes(bytes))
306}
307
308/// Minimal snapshot of the currently-configured options, returned
309/// by `Db::get_property("regolith.options")`. Deliberately small -
310/// regolith doesn't retain the full `Options` past `Db::open`, and
311/// the Debug impl of this struct is the property's string value.
312#[derive(Debug)]
313#[allow(dead_code)]
314struct OptionsSnapshot {
315    durability: engine::DurabilityMode,
316    default_cf: &'static str,
317    read_only: bool,
318    max_key_size: usize,
319    max_value_size: usize,
320    transaction_keys_inline: usize,
321}
322
323/// Format a raw engine key for inclusion in a property string.
324/// Internal keys in regolith carry a 4-byte CF prefix; if the key
325/// is long enough we strip it and ASCII-escape the remainder.
326/// Anything non-printable (or keys too short to strip) falls
327/// back to a hex rendering so the output stays single-line.
328fn format_key_for_display(key: &[u8]) -> String {
329    let payload = if key.len() > 4 { &key[4..] } else { key };
330    if payload.iter().all(|&b| b.is_ascii_graphic() || b == b' ') {
331        format!("\"{}\"", String::from_utf8_lossy(payload))
332    } else {
333        let hex: String = payload.iter().map(|b| format!("{b:02x}")).collect();
334        format!("0x{hex}")
335    }
336}
337
338/// A half-open key range `[start, end)` passed to the approximate-size
339/// APIs. Borrowed; cheap to construct inline.
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341pub struct Range<'a> {
342    /// Inclusive lower bound.
343    pub start: &'a [u8],
344    /// Exclusive upper bound.
345    pub end: &'a [u8],
346}
347
348impl<'a> Range<'a> {
349    /// Construct a new `[start, end)` range.
350    pub fn new(start: &'a [u8], end: &'a [u8]) -> Self {
351        Self { start, end }
352    }
353}
354
355/// Approximate memtable stats returned by
356/// [`Db::get_approximate_memtable_stats`]. `count` is the number of
357/// raw entries (including every version and every tombstone) for
358/// user keys in the queried range; `size` is the sum of
359/// `internal_key.len() + value.len()` over those entries. Both
360/// values are exact with respect to the current active memtable -
361/// this method walks the skip list.
362#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
363pub struct MemTableStats {
364    /// Number of raw entries in the range.
365    pub count: u64,
366    /// Approximate total bytes in the range.
367    pub size: u64,
368}
369
370/// Result type for regolith operations.
371pub type Result<T> = std::result::Result<T, Error>;
372
373/// A key-value database backed by an LSM-tree.
374pub struct Db {
375    engine: Arc<RegolithEngine>,
376    durability: engine::DurabilityMode,
377    cfs: Arc<CfRegistry>,
378    read_only: bool,
379    max_key_size: usize,
380    max_value_size: usize,
381    transaction_keys_inline: usize,
382}
383
384impl std::fmt::Debug for Db {
385    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386        f.debug_struct("Db")
387            .field("durability", &self.durability)
388            .field("read_only", &self.read_only)
389            .finish_non_exhaustive()
390    }
391}
392
393impl Db {
394    /// Open or create a database at the given path.
395    ///
396    /// On a fresh database the default column family (`"default"`)
397    /// is created automatically and every non-`*_cf` method uses
398    /// it. Callers who want logical keyspace isolation can then
399    /// call [`Db::create_column_family`] for additional CFs; those
400    /// calls persist into the database and survive reopen. Invalid
401    /// option combinations fail before any filesystem work starts.
402    ///
403    /// Returns [`Error::Io`] with [`std::io::ErrorKind::Unsupported`]
404    /// when the platform cannot start the background compaction
405    /// thread, as a single-threaded target does.
406    pub fn open<P: AsRef<Path>>(path: P, opts: Options) -> Result<Self> {
407        opts.validate()?;
408        let read_only = opts.read_only;
409        let max_key_size = opts.max_key_size;
410        let max_value_size = opts.max_value_size;
411        let transaction_keys_inline = opts.transaction_keys_inline;
412        let durability = match opts.durability {
413            DurabilityMode::Immediate => engine::DurabilityMode::Immediate,
414            DurabilityMode::Eventual => engine::DurabilityMode::Eventual,
415        };
416        let engine_opts = opts.to_engine_options();
417        let engine = if read_only {
418            RegolithEngine::open_read_only(path.as_ref(), engine_opts)?
419        } else {
420            RegolithEngine::open(path.as_ref(), engine_opts)?
421        };
422        let cfs = Arc::new(CfRegistry::new());
423        let db = Self {
424            engine,
425            durability,
426            cfs,
427            read_only,
428            max_key_size,
429            max_value_size,
430            transaction_keys_inline,
431        };
432        db.load_cf_registry()?;
433        Ok(db)
434    }
435
436    /// Open an existing database in read-only mode.
437    ///
438    /// The handle replays any existing WAL files into memory so reads
439    /// see committed-but-unflushed writes, but it does not create,
440    /// rewrite, compact, or delete files. Mutating APIs return
441    /// [`Error::ReadOnly`].
442    pub fn open_read_only<P: AsRef<Path>>(path: P, mut opts: Options) -> Result<Self> {
443        opts.read_only = true;
444        Self::open(path, opts)
445    }
446
447    /// Populate the in-memory [`CfRegistry`] from the on-disk
448    /// metadata CF, creating the default CF entry if this is a
449    /// fresh database. Called once from [`Db::open`].
450    fn load_cf_registry(&self) -> Result<()> {
451        // Scan every `name:*` entry in the meta CF to rebuild the
452        // name→id map. The default CF is not persisted to disk -
453        // it's always injected into the in-memory registry with a
454        // hardcoded id so an empty database stays byte-free on
455        // disk. User-created CFs are the only thing that produces
456        // on-disk metadata writes.
457        let mut entries: Vec<(String, u32)> = Vec::new();
458        let pairs = collect_range(
459            self.engine.new_iter_latest(),
460            Some(&meta::name_scan_prefix()),
461            Some(&meta::name_scan_upper()),
462        )?;
463        for (key, value) in pairs {
464            // A meta value that is not a 4-byte id is not one of ours.
465            let Ok(id_bytes) = <[u8; 4]>::try_from(value.as_slice()) else {
466                continue;
467            };
468            let Some(name) = meta::name_from_key(&key) else {
469                continue;
470            };
471            entries.push((name.to_string(), u32::from_be_bytes(id_bytes)));
472        }
473
474        // Recover `next_id`. Absent on a fresh database.
475        let next_id_raw = self
476            .engine
477            .get_latest(&meta::next_id_key())
478            .map_err(Error::from)?;
479        let next_id = next_id_raw
480            .and_then(|bytes| <[u8; 4]>::try_from(bytes.as_slice()).ok())
481            .map_or(DEFAULT_CF_ID + 1, u32::from_be_bytes);
482
483        // Inject the default CF into the in-memory registry so
484        // `Db::default_cf()` always succeeds. It's never persisted
485        // to the meta CF - any re-open computes the same id.
486        entries.push((DEFAULT_CF_NAME.to_string(), DEFAULT_CF_ID));
487        self.cfs.load(entries, next_id);
488        Ok(())
489    }
490
491    /// Get the value for a key from the default column family.
492    /// Returns `None` if the key doesn't exist.
493    ///
494    /// This is [`Db::get_slice`] followed by [`DbSlice::into_vec`].
495    pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
496        Ok(self.get_slice(key)?.map(DbSlice::into_vec))
497    }
498
499    /// Read a value from the default column family without copying it.
500    ///
501    /// The returned [`DbSlice`] borrows bytes the database already owns
502    /// and holds a reference count on their owner, so nothing is copied
503    /// on the way out. Holding one pins that owner: see [`DbSlice`].
504    ///
505    /// `Option<DbSlice>` does not compare against `Option<Vec<u8>>`
506    /// (`core`'s `PartialEq` for `Option` is homogeneous), so this
507    /// method is additive and [`Db::get`] keeps its signature.
508    pub fn get_slice(&self, key: &[u8]) -> Result<Option<DbSlice>> {
509        self.lookup_slice_latest(DEFAULT_CF_ID, key)
510    }
511
512    /// [`Db::get_slice`] scoped to a column family.
513    pub fn get_slice_cf(&self, cf: &ColumnFamilyHandle, key: &[u8]) -> Result<Option<DbSlice>> {
514        self.validate_cf_handle(cf)?;
515        self.lookup_slice_latest(cf.id(), key)
516    }
517
518    /// Whether a live value exists for `key` in the default column
519    /// family.
520    ///
521    /// Reads the same sources [`Db::get`] does and pays the same block
522    /// reads: a bloom filter is probabilistic, so ruling a key *in*
523    /// still requires consulting the data block. What it skips is the
524    /// copy, so no value bytes are ever materialized and nothing is
525    /// pinned after it returns. On a bloom-negative lookup `has` and
526    /// `get` cost the same and neither touches a block.
527    ///
528    /// When a [`MergeOperator`] is configured this method does
529    /// materialize: the operator decides inside `full_merge` whether a
530    /// value exists at all, so there is no way to answer without
531    /// collapsing the chain.
532    pub fn has(&self, key: &[u8]) -> Result<bool> {
533        Ok(self.get_size(key)?.is_some())
534    }
535
536    /// [`Db::has`] scoped to a column family.
537    pub fn has_cf(&self, cf: &ColumnFamilyHandle, key: &[u8]) -> Result<bool> {
538        Ok(self.get_size_cf(cf, key)?.is_some())
539    }
540
541    /// Length in bytes of the live value for `key` in the default
542    /// column family, or `None` when there is none.
543    ///
544    /// Same sources and same block reads as [`Db::get`], without the
545    /// copy. See [`Db::has`] for what that does and does not save, and
546    /// for the [`MergeOperator`] caveat.
547    pub fn get_size(&self, key: &[u8]) -> Result<Option<usize>> {
548        self.lookup_size_latest(DEFAULT_CF_ID, key)
549    }
550
551    /// [`Db::get_size`] scoped to a column family.
552    pub fn get_size_cf(&self, cf: &ColumnFamilyHandle, key: &[u8]) -> Result<Option<usize>> {
553        self.validate_cf_handle(cf)?;
554        self.lookup_size_latest(cf.id(), key)
555    }
556
557    /// [`Db::lookup_slice`] for a read of the newest visible value.
558    ///
559    /// The engine samples the read horizon against the view it walks,
560    /// which a caller cannot do for it: building a `LookupKey` here
561    /// would fix the sequence before the engine loads its view, and a
562    /// compaction in between can drop the newest version at or below
563    /// it, so the read reports the key absent or an older value.
564    fn lookup_slice_latest(&self, cf_id: u32, key: &[u8]) -> Result<Option<DbSlice>> {
565        let stats = self.stats();
566        let _scope = statistics::TimeScope::new(stats, Histogram::DbGet);
567        if let Some(s) = stats {
568            s.add(Ticker::KeysRead, 1);
569        }
570        perf_context::record_get_call();
571        let result = self
572            .engine
573            .get_slice_latest(cf_id, key)
574            .map_err(|err| map_point_read_error_for(err, key));
575        if let (Some(s), Ok(Some(v))) = (stats, &result) {
576            s.add(Ticker::BytesRead, v.len() as u64);
577            s.record(Histogram::BytesPerRead, v.len() as u64);
578        }
579        result
580    }
581
582    /// The length-only twin of [`Db::lookup_slice_latest`].
583    fn lookup_size_latest(&self, cf_id: u32, key: &[u8]) -> Result<Option<usize>> {
584        let stats = self.stats();
585        let _scope = statistics::TimeScope::new(stats, Histogram::DbGet);
586        if let Some(s) = stats {
587            s.add(Ticker::KeysRead, 1);
588        }
589        perf_context::record_get_call();
590        self.engine
591            .get_size_latest(cf_id, key)
592            .map_err(|err| map_point_read_error_for(err, key))
593    }
594
595    /// Helper that exposes a borrowed reference to the engine's
596    /// `Statistics` (if any) so instrumented methods can call
597    /// `stats.add(..)` / `stats.record(..)` through a single
598    /// `Option::is_some` check.
599    fn stats(&self) -> Option<&Statistics> {
600        self.engine.statistics()
601    }
602
603    /// Look up a batch of keys in the default column family.
604    /// Returns a vector with one entry per input key (preserving
605    /// order and duplicates); each entry is `None` if the key does
606    /// not exist or is tombstoned.
607    ///
608    /// All keys in a single call see the **same** consistent view -
609    /// a concurrent writer cannot make two keys disagree about
610    /// visibility.
611    pub fn multi_get(&self, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>> {
612        let owned: Vec<Vec<u8>> = keys.iter().map(|k| prefix_key(DEFAULT_CF_ID, k)).collect();
613        let refs: Vec<&[u8]> = owned.iter().map(|k| k.as_slice()).collect();
614        self.engine.multi_get_latest(&refs).map_err(Error::from)
615    }
616
617    /// Set a key-value pair in the default column family using
618    /// the database-global durability mode and default write
619    /// options.
620    pub fn put(&self, key: &[u8], value: &[u8]) -> Result<()> {
621        self.put_opt(&WriteOptions::default(), key, value)
622    }
623
624    /// Set a key-value pair in the default column family with an
625    /// explicit [`WriteOptions`] override.
626    pub fn put_opt(&self, opts: &WriteOptions, key: &[u8], value: &[u8]) -> Result<()> {
627        self.ensure_writable()?;
628        self.validate_write_kv_sizes(key, value)?;
629        self.wait_for_write_capacity(opts)?;
630        let stats = self.stats();
631        let _scope = statistics::TimeScope::new(stats, Histogram::DbWrite);
632        let bytes = (key.len() + value.len()) as u64;
633        if let Some(s) = stats {
634            s.add(Ticker::KeysWritten, 1);
635            s.add(Ticker::BytesWritten, bytes);
636            s.record(Histogram::BytesPerWrite, bytes);
637        }
638        perf_context::record_write_call();
639        let (dm, disable_wal) = self.resolve_write_opts(opts);
640        self.engine
641            .apply_single_put(
642                prefix_key(DEFAULT_CF_ID, key),
643                value.to_vec(),
644                dm,
645                disable_wal,
646            )
647            .map_err(Error::from)
648            .map(|_| ())
649    }
650
651    /// Delete a key from the default column family using the
652    /// database-global durability mode.
653    pub fn delete(&self, key: &[u8]) -> Result<()> {
654        self.delete_opt(&WriteOptions::default(), key)
655    }
656
657    /// Delete a key from the default column family with an
658    /// explicit [`WriteOptions`] override.
659    pub fn delete_opt(&self, opts: &WriteOptions, key: &[u8]) -> Result<()> {
660        self.ensure_writable()?;
661        self.validate_key_size(key)?;
662        self.wait_for_write_capacity(opts)?;
663        if let Some(s) = self.stats() {
664            s.add(Ticker::KeysDeleted, 1);
665        }
666        let mut batch = BTreeMap::new();
667        batch.insert(prefix_key(DEFAULT_CF_ID, key), None);
668        let (dm, disable_wal) = self.resolve_write_opts(opts);
669        self.engine
670            .apply_grouped_batch(batch, Vec::new(), Vec::new(), dm, disable_wal)
671            .map(|_| ())
672            .map_err(Error::from)
673    }
674
675    /// Layer a merge operand on top of `key` in the default
676    /// column family.
677    ///
678    /// Requires an [`Options::merge_operator`] to be configured. The
679    /// operand is written cheaply (no read-modify-write); readers
680    /// collapse the chain of merges plus any base value via the
681    /// configured operator at visibility time.
682    pub fn merge(&self, key: &[u8], operand: &[u8]) -> Result<()> {
683        self.merge_opt(&WriteOptions::default(), key, operand)
684    }
685
686    /// [`Db::merge`] with an explicit [`WriteOptions`] override.
687    pub fn merge_opt(&self, opts: &WriteOptions, key: &[u8], operand: &[u8]) -> Result<()> {
688        self.ensure_writable()?;
689        self.validate_write_kv_sizes(key, operand)?;
690        self.wait_for_write_capacity(opts)?;
691        if let Some(s) = self.stats() {
692            s.add(Ticker::MergesWritten, 1);
693        }
694        let (dm, disable_wal) = self.resolve_write_opts(opts);
695        self.engine
696            .apply_grouped_batch(
697                BTreeMap::new(),
698                Vec::new(),
699                vec![(prefix_key(DEFAULT_CF_ID, key), operand.to_vec())],
700                dm,
701                disable_wal,
702            )
703            .map(|_| ())
704            .map_err(Error::from)
705    }
706
707    /// Delete every key in `[start, end)` in the default column
708    /// family.
709    ///
710    /// Range deletes are cheap regardless of how many keys the range
711    /// covers - internally they are stored as a single range-tombstone
712    /// record rather than as one point tombstone per key. The delete
713    /// is durable under the same rules as [`Db::put`] / [`Db::delete`]
714    /// and is atomic with respect to concurrent readers.
715    ///
716    /// If `start >= end` the call is a no-op.
717    pub fn delete_range(&self, start: &[u8], end: &[u8]) -> Result<()> {
718        self.delete_range_opt(&WriteOptions::default(), start, end)
719    }
720
721    /// Delete every key in `[start, end)` in the default column
722    /// family with an explicit [`WriteOptions`] override.
723    ///
724    /// An empty range is still a write: a read-only or closed handle
725    /// rejects it with [`Error::ReadOnly`] or [`Error::Closed`] rather
726    /// than returning `Ok`.
727    pub fn delete_range_opt(&self, opts: &WriteOptions, start: &[u8], end: &[u8]) -> Result<()> {
728        self.ensure_writable()?;
729        if start >= end {
730            return Ok(());
731        }
732        self.validate_key_size(start)?;
733        self.validate_key_size(end)?;
734        self.wait_for_write_capacity(opts)?;
735        if let Some(s) = self.stats() {
736            s.add(Ticker::RangeDeletesWritten, 1);
737        }
738        let (dm, disable_wal) = self.resolve_write_opts(opts);
739        self.engine
740            .apply_grouped_batch(
741                BTreeMap::new(),
742                vec![(
743                    prefix_key(DEFAULT_CF_ID, start),
744                    prefix_key(DEFAULT_CF_ID, end),
745                )],
746                Vec::new(),
747                dm,
748                disable_wal,
749            )
750            .map(|_| ())
751            .map_err(Error::from)
752    }
753
754    /// Keys a transaction buffers before indexing them. See
755    /// [`Options::transaction_keys_inline`].
756    pub(crate) fn transaction_keys_inline(&self) -> usize {
757        self.transaction_keys_inline
758    }
759
760    /// Open a write stream that bounds its own memory.
761    ///
762    /// [`WriteBatch`] costs memory proportional to its input, which a
763    /// caller feeding an unbounded stream cannot afford. A
764    /// [`StreamingWriter`] costs the configured budget instead, at the
765    /// price of atomicity across the whole stream. Read
766    /// [`StreamingWriter`]'s own documentation before choosing it: that
767    /// tradeoff is the entire reason the type exists.
768    pub fn streaming_writer(&self, opts: StreamOptions) -> StreamingWriter<'_> {
769        StreamingWriter::new(self, opts)
770    }
771
772    /// Apply a batch of writes atomically using the database-global
773    /// durability mode.
774    pub fn write(&self, batch: WriteBatch) -> Result<()> {
775        self.write_opt(&WriteOptions::default(), batch)
776    }
777
778    /// Apply a batch of writes atomically with an explicit
779    /// [`WriteOptions`] override.
780    ///
781    /// Apply a batch and return the sequence it committed at.
782    ///
783    /// The returned sequence is the engine's visibility horizon once the batch
784    /// is durable and applied: a [`Snapshot`] taken afterwards reports at least
785    /// this value from [`Snapshot::sequence`], and one taken before reports
786    /// less. An upper layer can therefore order its own versions against regolith's
787    /// without holding a lock across the write, because the horizon publishes
788    /// atomically inside this call.
789    ///
790    /// An empty batch commits nothing and returns the current horizon.
791    pub fn write_sequenced(&self, batch: WriteBatch) -> Result<u64> {
792        self.write_sequenced_opt(&WriteOptions::default(), batch)
793    }
794
795    /// [`Db::write_sequenced`] with explicit [`WriteOptions`].
796    pub fn write_sequenced_opt(&self, opts: &WriteOptions, batch: WriteBatch) -> Result<u64> {
797        self.write_opt_inner(opts, batch)
798    }
799
800    /// The newest sequence visible to a snapshot taken now.
801    ///
802    /// Monotonic and never decreasing for an open database.
803    pub fn latest_sequence(&self) -> u64 {
804        self.engine.snapshot_seq()
805    }
806
807    /// Apply a batch of writes atomically with explicit [`WriteOptions`].
808    ///
809    /// Use [`Db::write_sequenced_opt`] instead when the caller needs the
810    /// sequence the batch committed at.
811    pub fn write_opt(&self, opts: &WriteOptions, batch: WriteBatch) -> Result<()> {
812        self.write_opt_inner(opts, batch).map(|_| ())
813    }
814
815    fn write_opt_inner(&self, opts: &WriteOptions, batch: WriteBatch) -> Result<u64> {
816        // Checked before the empty-batch shortcut: an empty batch is
817        // still a write, so a read-only or closed handle rejects it
818        // rather than reporting a horizon it could not have advanced.
819        self.ensure_writable()?;
820        if batch.is_empty() {
821            // Nothing committed: report the current horizon, which is what a
822            // snapshot taken now would read at.
823            return Ok(self.engine.snapshot_seq());
824        }
825        self.validate_batch_cf_liveness(&batch)?;
826        self.validate_batch_sizes(&batch)?;
827        self.wait_for_write_capacity(opts)?;
828        perf_context::record_write_call();
829        let stats = self.stats();
830        let _scope = statistics::TimeScope::new(stats, Histogram::DbWrite);
831        if let Some(s) = stats {
832            let mut bytes: u64 = 0;
833            let mut puts: u64 = 0;
834            let mut deletes: u64 = 0;
835            let mut range_deletes: u64 = 0;
836            let mut merges: u64 = 0;
837            for op in &batch.ops {
838                match op {
839                    WriteBatchOp::Put { key, value } => {
840                        puts += 1;
841                        bytes += (key.len() + value.len()) as u64;
842                    }
843                    WriteBatchOp::Delete { key } => {
844                        deletes += 1;
845                        bytes += key.len() as u64;
846                    }
847                    WriteBatchOp::DeleteRange { .. } => {
848                        range_deletes += 1;
849                    }
850                    WriteBatchOp::Merge { .. } => {
851                        merges += 1;
852                    }
853                }
854            }
855            s.add(Ticker::KeysWritten, puts);
856            s.add(Ticker::KeysDeleted, deletes);
857            s.add(Ticker::BytesWritten, bytes);
858            s.add(Ticker::RangeDeletesWritten, range_deletes);
859            s.add(Ticker::MergesWritten, merges);
860            s.record(Histogram::BytesPerWrite, bytes);
861        }
862        let (dm, disable_wal) = self.resolve_write_opts(opts);
863        self.engine
864            .apply_batch(batch.ops, dm, disable_wal)
865            .map_err(Error::from)
866    }
867
868    /// Apply a batch of writes atomically with an explicit
869    /// [`DurabilityMode`] override. Retained for backwards
870    /// compatibility - prefer [`Db::write_opt`] for new code.
871    pub fn write_with_durability(
872        &self,
873        batch: WriteBatch,
874        durability: DurabilityMode,
875    ) -> Result<()> {
876        let opts = WriteOptions {
877            sync: matches!(durability, DurabilityMode::Immediate),
878            ..WriteOptions::default()
879        };
880        self.write_opt(&opts, batch)
881    }
882
883    /// Resolve a [`WriteOptions`] into the pair the engine's
884    /// `apply_batch` actually consumes: a concrete
885    /// `engine::DurabilityMode` and a `disable_wal` bool. `sync: true`
886    /// maps to `Immediate` regardless of the database-global default;
887    /// otherwise the default wins. `low_pri` is accepted but is
888    /// currently a no-op; `no_slowdown` is handled separately by
889    /// the write-stall pre-check.
890    fn resolve_write_opts(&self, opts: &WriteOptions) -> (engine::DurabilityMode, bool) {
891        let dm = if opts.sync {
892            engine::DurabilityMode::Immediate
893        } else {
894            self.durability
895        };
896        (dm, opts.disable_wal)
897    }
898
899    /// Run the write-stall pre-check. Block the caller until the
900    /// engine is ready to accept another write, or return
901    /// [`Error::Busy`] immediately if `opts.no_slowdown` is set and
902    /// any stall condition is currently active.
903    fn wait_for_write_capacity(&self, opts: &WriteOptions) -> Result<()> {
904        self.engine.wait_for_write_capacity(opts.no_slowdown)?;
905        Ok(())
906    }
907
908    fn ensure_open(&self) -> Result<()> {
909        if self.engine.is_closed() {
910            Err(Error::Closed)
911        } else {
912            Ok(())
913        }
914    }
915
916    fn ensure_writable(&self) -> Result<()> {
917        self.ensure_open()?;
918        if self.read_only {
919            Err(Error::ReadOnly)
920        } else {
921            Ok(())
922        }
923    }
924
925    fn validate_key_size(&self, key: &[u8]) -> Result<()> {
926        if key.len() <= self.max_key_size {
927            return Ok(());
928        }
929
930        Err(invalid_input_error(format!(
931            "key length {} exceeds configured max_key_size {}",
932            key.len(),
933            self.max_key_size
934        )))
935    }
936
937    fn validate_value_size(&self, value: &[u8]) -> Result<()> {
938        if value.len() <= self.max_value_size {
939            return Ok(());
940        }
941
942        Err(invalid_input_error(format!(
943            "value length {} exceeds configured max_value_size {}",
944            value.len(),
945            self.max_value_size
946        )))
947    }
948
949    fn validate_write_kv_sizes(&self, key: &[u8], value: &[u8]) -> Result<()> {
950        self.validate_key_size(key)?;
951        self.validate_value_size(value)
952    }
953
954    fn validate_prefixed_key_size(&self, prefixed_key: &[u8]) -> Result<()> {
955        let user_key_len = prefixed_key.len().saturating_sub(4);
956        if user_key_len <= self.max_key_size {
957            return Ok(());
958        }
959
960        Err(invalid_input_error(format!(
961            "key length {} exceeds configured max_key_size {}",
962            user_key_len, self.max_key_size
963        )))
964    }
965
966    fn validate_batch_sizes(&self, batch: &WriteBatch) -> Result<()> {
967        for op in &batch.ops {
968            match op {
969                WriteBatchOp::Put { key, value } => {
970                    self.validate_prefixed_key_size(key)?;
971                    self.validate_value_size(value)?;
972                }
973                WriteBatchOp::Delete { key } => self.validate_prefixed_key_size(key)?,
974                WriteBatchOp::DeleteRange { start, end } => {
975                    self.validate_prefixed_key_size(start)?;
976                    self.validate_prefixed_key_size(end)?;
977                }
978                WriteBatchOp::Merge { key, operand } => {
979                    self.validate_prefixed_key_size(key)?;
980                    self.validate_value_size(operand)?;
981                }
982            }
983        }
984        Ok(())
985    }
986
987    fn validate_cf_handle(&self, cf: &ColumnFamilyHandle) -> Result<()> {
988        if self.cfs.is_live_handle(cf) {
989            Ok(())
990        } else {
991            Err(invalid_cf_handle_error(cf))
992        }
993    }
994
995    fn is_live_cf_handle(&self, cf: &ColumnFamilyHandle) -> bool {
996        self.cfs.is_live_handle(cf)
997    }
998
999    fn validate_prefixed_cf_io(&self, prefixed_key: &[u8]) -> std::io::Result<()> {
1000        let cf_id = prefixed_cf_id(prefixed_key)?;
1001        if self.cfs.contains_id(cf_id) {
1002            Ok(())
1003        } else {
1004            Err(invalid_cf_id_io_error(cf_id))
1005        }
1006    }
1007
1008    fn validate_prefixed_cf(&self, prefixed_key: &[u8]) -> Result<()> {
1009        let cf_id = prefixed_cf_id(prefixed_key).map_err(Error::from)?;
1010        if self.cfs.contains_id(cf_id) {
1011            Ok(())
1012        } else {
1013            Err(invalid_cf_id_error(cf_id))
1014        }
1015    }
1016
1017    fn validate_batch_cf_liveness(&self, batch: &WriteBatch) -> Result<()> {
1018        for op in &batch.ops {
1019            match op {
1020                WriteBatchOp::Put { key, .. }
1021                | WriteBatchOp::Delete { key }
1022                | WriteBatchOp::Merge { key, .. } => self.validate_prefixed_cf(key)?,
1023                WriteBatchOp::DeleteRange { start, end } => {
1024                    self.validate_prefixed_cf(start)?;
1025                    self.validate_prefixed_cf(end)?;
1026                }
1027            }
1028        }
1029        Ok(())
1030    }
1031
1032    /// Create a point-in-time snapshot for consistent reads.
1033    ///
1034    /// Snapshots also pin the compaction GC horizon: as long as at
1035    /// least one `Snapshot` at seq `S` is alive, the compaction
1036    /// thread will not drop any version needed to read at seq `S`.
1037    /// Dropping the returned `Snapshot` releases the pin and may
1038    /// allow subsequent compactions to reclaim space.
1039    pub fn snapshot(&self) -> Snapshot {
1040        let seq = self.engine.register_snapshot_at_horizon();
1041        Snapshot {
1042            engine: Arc::clone(&self.engine),
1043            cfs: Arc::clone(&self.cfs),
1044            seq,
1045        }
1046    }
1047
1048    /// Scan a key range lazily, holding one entry rather than the range.
1049    ///
1050    /// The streaming counterpart to [`Db::scan`], and what a caller
1051    /// should reach for by default: a scan that stops early pays only for
1052    /// what it read, and memory does not grow with the size of the range.
1053    /// Values come back as [`DbSlice`], so no value bytes are copied.
1054    ///
1055    /// The scan is served from a snapshot pinned when it is created, so
1056    /// concurrent writes cannot shift the range underneath it.
1057    ///
1058    /// ```no_run
1059    /// # use regolith::{Db, Options};
1060    /// # let db = Db::open("/tmp/scan_stream_doc", Options::default()).unwrap();
1061    /// for (key, value) in db.scan_stream(Some(b"user:"), Some(b"user;"))? {
1062    ///     println!("{} is {} bytes", String::from_utf8_lossy(&key), value.len());
1063    /// }
1064    /// # Ok::<(), regolith::Error>(())
1065    /// ```
1066    pub fn scan_stream(&self, start: Option<&[u8]>, end: Option<&[u8]>) -> Result<ScanStream> {
1067        Ok(self.snapshot().into_scan_stream(start, end))
1068    }
1069
1070    /// Scan a key range in the default column family.
1071    ///
1072    /// Returns all key-value pairs where `start <= key < end`, with
1073    /// keys in their user-visible form (no CF prefix). This
1074    /// materializes the entire range into memory, so it is bounded by
1075    /// the size of the range rather than by the caller. Prefer
1076    /// [`Db::scan_stream`], or [`Db::scan_page`] when the caller wants an
1077    /// explicit page size.
1078    pub fn scan(
1079        &self,
1080        start: Option<&[u8]>,
1081        end: Option<&[u8]>,
1082    ) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
1083        let lo = match start {
1084            Some(s) => prefix_key(DEFAULT_CF_ID, s),
1085            None => cf_lower_bound(DEFAULT_CF_ID),
1086        };
1087        let hi = match end {
1088            Some(e) => prefix_key(DEFAULT_CF_ID, e),
1089            None => cf_upper_bound(DEFAULT_CF_ID),
1090        };
1091        // `new_iter_latest` loads the published view and *then* samples the
1092        // horizon. Sampling first and building the iterator after leaves a
1093        // window where a compaction no snapshot pins can drop the newest
1094        // version at or below the sampled sequence, after which the scan
1095        // finds only versions it must filter out and a key reads absent.
1096        let raw = collect_range(self.engine.new_iter_latest(), Some(&lo), Some(&hi))?;
1097        strip_cf_prefix_entries(raw)
1098    }
1099
1100    /// Scan a bounded page in the default column family.
1101    ///
1102    /// At most `limit` entries are materialized. When
1103    /// [`ScanPage::next_start`] is `Some`, pass that key back as
1104    /// `start` to continue the scan. Each `Db` call captures its own
1105    /// read snapshot; create a [`Snapshot`] and call
1106    /// [`Snapshot::scan_page`] for a stable multi-page walk while
1107    /// writes continue.
1108    pub fn scan_page(
1109        &self,
1110        start: Option<&[u8]>,
1111        end: Option<&[u8]>,
1112        limit: usize,
1113    ) -> Result<ScanPage> {
1114        let lo = match start {
1115            Some(s) => prefix_key(DEFAULT_CF_ID, s),
1116            None => cf_lower_bound(DEFAULT_CF_ID),
1117        };
1118        let hi = match end {
1119            Some(e) => prefix_key(DEFAULT_CF_ID, e),
1120            None => cf_upper_bound(DEFAULT_CF_ID),
1121        };
1122        collect_page(self.engine.new_iter_latest(), &lo, &hi, limit).and_then(strip_cf_prefix_page)
1123    }
1124
1125    /// Create a streaming iterator over the default column family.
1126    ///
1127    /// The iterator captures a consistent view at the moment it is
1128    /// created: later writes are invisible to this iterator, and
1129    /// concurrent background compaction cannot invalidate it. Keys from
1130    /// the iterator have the CF prefix stripped and appear exactly as
1131    /// the caller supplied them on put.
1132    ///
1133    /// A fresh iterator is not positioned; call one of
1134    /// [`CfIter::seek_to_first`], [`CfIter::seek`], or
1135    /// [`CfIter::seek_for_prev`] before reading.
1136    pub fn iter(&self) -> CfIter<'_> {
1137        let default = self.default_cf();
1138        self.iter_cf(&default)
1139    }
1140
1141    /// Create a raw streaming iterator over the entire engine
1142    /// keyspace, including the reserved metadata CF. Internal -
1143    /// used by [`Db::iter_cf`] via `CfIter`.
1144    fn raw_iter(&self) -> Iter<'_> {
1145        Iter::from_internal(self.engine.new_iter_latest()).with_stats(self.engine.statistics_arc())
1146    }
1147
1148    /// Delete all data in the database.
1149    pub fn drop_all(&self) -> Result<()> {
1150        self.ensure_writable()?;
1151        self.engine.drop_all().map_err(Error::from)
1152    }
1153
1154    /// Synchronously compact every SSTable overlapping the default
1155    /// column-family user-key range `[start, end)` down to the
1156    /// bottommost non-empty level.
1157    ///
1158    /// Passing `None` for either bound means "unbounded" on that side,
1159    /// so `compact_range(None, None)` compacts the entire default
1160    /// column family.
1161    ///
1162    /// Active memtable contents that fall in the range are flushed to
1163    /// L0 first. The call blocks until the requested compaction work
1164    /// is finished and is serialized with the background compaction
1165    /// scheduler so the two paths can't fight over the same inputs.
1166    pub fn compact_range(&self, start: Option<&[u8]>, end: Option<&[u8]>) -> Result<()> {
1167        self.ensure_writable()?;
1168        if let Some(start) = start {
1169            self.validate_key_size(start)?;
1170        }
1171        if let Some(end) = end {
1172            self.validate_key_size(end)?;
1173        }
1174        let lower = match start {
1175            Some(s) => prefix_key(DEFAULT_CF_ID, s),
1176            None => cf_lower_bound(DEFAULT_CF_ID),
1177        };
1178        let upper = match end {
1179            Some(e) => prefix_key(DEFAULT_CF_ID, e),
1180            None => cf_upper_bound(DEFAULT_CF_ID),
1181        };
1182        self.engine
1183            .compact_range(Some(&lower), Some(&upper))
1184            .map_err(Error::from)
1185    }
1186
1187    /// Rotate the active memtable and write it to a level-0 SSTable,
1188    /// on this thread, before returning.
1189    ///
1190    /// A no-op when the active memtable holds no entries and no range
1191    /// tombstones. This is the same flush a full memtable triggers on
1192    /// the write path, made explicit so a caller can decide when to
1193    /// pay for it. Memtable flushes are written by the calling thread
1194    /// in every mode, so this behaves identically with and without
1195    /// background compaction workers.
1196    ///
1197    /// Flushing does not compact: the new file lands in L0 and stays
1198    /// there until a compaction job merges it. See [`Db::compact_step`]
1199    /// and [`Db::compact_range`].
1200    pub fn flush(&self) -> Result<()> {
1201        self.ensure_writable()?;
1202        self.engine.flush_active_memtable().map_err(Error::from)
1203    }
1204
1205    /// Perform at most one pending compaction job on this thread.
1206    ///
1207    /// Returns `Ok(true)` when a job ran. Callers running with
1208    /// [`Options::max_background_compactions`] set to `0` use this to
1209    /// keep the level structure healthy outside the write path; a
1210    /// writer that would otherwise stall already performs the same job
1211    /// itself, so this is an optimization of write latency rather than
1212    /// a requirement for correctness.
1213    ///
1214    /// `Ok(false)` means this call did no work, for either of two
1215    /// reasons: nothing was over its compaction trigger, or another
1216    /// thread currently holds the files that would be compacted. A
1217    /// loop of the form `while db.compact_step()? {}` therefore
1218    /// terminates, and terminates having compacted everything this
1219    /// thread could reach. Every compaction style regolith offers reduces
1220    /// the file count it merges, so the loop cannot be fed forever by
1221    /// its own output.
1222    ///
1223    /// Safe to call with background workers running: the job is picked
1224    /// under the same engine-wide compaction lock and the same
1225    /// in-progress file set the workers use, so the two can never pick
1226    /// overlapping inputs.
1227    pub fn compact_step(&self) -> Result<bool> {
1228        self.ensure_writable()?;
1229        let outcome = self.engine.run_one_compaction_pass().map_err(Error::from)?;
1230        Ok(outcome == engine::compaction::CompactionOutcome::DidWork)
1231    }
1232
1233    /// Return the string value of a named property, or `None` if
1234    /// `name` isn't recognized. See the module-level docs for the
1235    /// full list of supported properties; the most useful ones
1236    /// are `"regolith.stats"`, `"regolith.sstables"`,
1237    /// `"regolith.levelstats"`, and `"regolith.options"`.
1238    pub fn get_property(&self, name: &str) -> Option<String> {
1239        match name {
1240            "regolith.stats" => Some(self.format_stats_property()),
1241            "regolith.sstables" => Some(self.format_sstables_property()),
1242            "regolith.levelstats" => Some(self.format_levelstats_property()),
1243            "regolith.options" => Some(format!("{:#?}", self.options_snapshot())),
1244            _ => {
1245                // Integer properties surfaced through the string
1246                // API too - every int property's string form is
1247                // just its decimal number.
1248                self.get_int_property(name).map(|v| v.to_string())
1249            }
1250        }
1251    }
1252
1253    /// What the environment this database was opened on can
1254    /// actually do.
1255    ///
1256    /// Read this when a durability or isolation guarantee matters.
1257    /// regolith keeps working on a host without directory fsync, without
1258    /// hard links, or without cross-process locking, but the
1259    /// guarantee is narrower there, and this is where it says so
1260    /// instead of the database quietly claiming more than it
1261    /// provides. On the default [`crate::env::StdEnv`] every flag is
1262    /// `true` on Linux, macOS, and Windows.
1263    pub fn capabilities(&self) -> env::Capabilities {
1264        self.engine.capabilities()
1265    }
1266
1267    /// Return the integer value of a named property, or `None` if
1268    /// `name` isn't recognized or doesn't have an integer form.
1269    pub fn get_int_property(&self, name: &str) -> Option<u64> {
1270        if let Some(level_str) = name.strip_prefix("regolith.num-files-at-level") {
1271            let level: usize = level_str.parse().ok()?;
1272            return Some(self.engine.num_files_at_level(level));
1273        }
1274        match name {
1275            "regolith.total-sst-files-size" => Some(self.engine.total_sst_size()),
1276            "regolith.cur-size-active-mem-table" => Some(self.engine.active_memtable_size()),
1277            "regolith.memtable-reserved-bytes" => Some(self.engine.memtables_reserved_size()),
1278            "regolith.arena-pool-bytes" => Some(self.engine.arena_pool_size()),
1279            "regolith.cur-size-all-mem-tables" => {
1280                Some(self.engine.active_memtable_size() + self.engine.frozen_memtables_size())
1281            }
1282            "regolith.num-entries-active-mem-table" => {
1283                // Approximate: the memtable exposes `approximate_size`
1284                // in bytes but no direct entry count. Estimate by
1285                // assuming a 48-byte average entry (internal key +
1286                // value). This is a rough indicator, not an exact
1287                // count.
1288                let bytes = self.engine.active_memtable_size();
1289                Some(bytes / 48)
1290            }
1291            "regolith.num-entries-imm-mem-tables" => {
1292                let bytes = self.engine.frozen_memtables_size();
1293                Some(bytes / 48)
1294            }
1295            "regolith.estimate-num-keys" => {
1296                // Lower-bound estimate: exact SST entry count plus
1297                // a rough guess for the memtable contribution.
1298                let sst = self.engine.total_sst_num_entries();
1299                let mem_bytes =
1300                    self.engine.active_memtable_size() + self.engine.frozen_memtables_size();
1301                Some(sst + mem_bytes / 48)
1302            }
1303            "regolith.estimate-live-data-size" => Some(self.engine.total_sst_size()),
1304            "regolith.num-snapshots" => Some(self.engine.live_snapshot_count()),
1305            "regolith.oldest-snapshot-time" => self.engine.oldest_snapshot_time_unix(),
1306            "regolith.block-cache-usage" => Some(self.engine.block_cache_usage() as u64),
1307            "regolith.block-cache-capacity" => Some(self.engine.block_cache_capacity() as u64),
1308            "regolith.pinned-metadata-bytes" => Some(self.engine.pinned_metadata_bytes() as u64),
1309            // Background errors are surfaced through the
1310            // `EventListener::on_background_error` callback today,
1311            // with no dedicated counter yet. Report `0` so any
1312            // monitoring layer consuming this property gets a
1313            // stable numeric value instead of `None`.
1314            "regolith.background-errors" => Some(0),
1315            _ => None,
1316        }
1317    }
1318
1319    /// Format the multi-line `regolith.stats` property: counters +
1320    /// histograms (when statistics are enabled) plus per-level
1321    /// file counts and compaction aggregates.
1322    fn format_stats_property(&self) -> String {
1323        let mut out = String::new();
1324        out.push_str("== regolith engine stats ==\n");
1325        out.push_str(&self.format_levelstats_property());
1326        if let Some(stats) = self.engine.statistics() {
1327            out.push('\n');
1328            out.push_str(&stats.dump());
1329        } else {
1330            out.push_str("\n(no Statistics object configured - see Options::statistics)\n");
1331        }
1332        out
1333    }
1334
1335    /// Format the `regolith.levelstats` property: one row per
1336    /// level with file count and total size in bytes.
1337    fn format_levelstats_property(&self) -> String {
1338        let version = self.engine.current_version();
1339        let mut out = String::from("Level  Files     Size(B)\n");
1340        for (lvl, files) in version.levels.iter().enumerate() {
1341            let count = files.len();
1342            let size: u64 = files.iter().map(|f| f.meta.file_size).sum();
1343            out.push_str(&format!("{lvl:5}  {count:5}  {size:10}\n"));
1344        }
1345        out
1346    }
1347
1348    /// Format the `regolith.sstables` property: one row per live
1349    /// SSTable with its level, file id, size, and key range.
1350    fn format_sstables_property(&self) -> String {
1351        let version = self.engine.current_version();
1352        let mut out =
1353            String::from("Level    FileID       Size(B)     Entries  SmallestKey..LargestKey\n");
1354        for (lvl, files) in version.levels.iter().enumerate() {
1355            for f in files {
1356                // Strip the CF prefix for display when the key
1357                // has room for it; otherwise show the raw bytes.
1358                let smallest = format_key_for_display(&f.meta.smallest_key);
1359                let largest = format_key_for_display(&f.meta.largest_key);
1360                out.push_str(&format!(
1361                    "{lvl:5}  {:8}  {:12}  {:10}  {}..{}\n",
1362                    f.meta.file_id, f.meta.file_size, f.meta.num_entries, smallest, largest
1363                ));
1364            }
1365        }
1366        out
1367    }
1368
1369    /// A minimal snapshot of the engine options. We deliberately
1370    /// do not carry the full `Options` struct around past
1371    /// construction, so this returns a small struct with just
1372    /// the observable knobs.
1373    fn options_snapshot(&self) -> OptionsSnapshot {
1374        OptionsSnapshot {
1375            durability: self.durability,
1376            default_cf: DEFAULT_CF_NAME,
1377            read_only: self.read_only,
1378            max_key_size: self.max_key_size,
1379            max_value_size: self.max_value_size,
1380            transaction_keys_inline: self.transaction_keys_inline,
1381        }
1382    }
1383
1384    /// Return the approximate on-disk bytes in each of the given
1385    /// ranges, in the same order as `ranges`. Each range is
1386    /// scoped to the default column family.
1387    ///
1388    /// Computed index-only: no data-block decompression happens,
1389    /// so the cost is sub-linear in the range size. Accuracy is
1390    /// bounded by one data-block worth of bytes per range
1391    /// boundary (partially-covered blocks at `start` and `end` are
1392    /// included whole). Active-memtable contents are **not**
1393    /// included - call [`Db::get_approximate_memtable_stats`] for
1394    /// those.
1395    pub fn get_approximate_sizes(&self, ranges: &[Range<'_>]) -> Vec<u64> {
1396        ranges
1397            .iter()
1398            .map(|r| self.approximate_size_in_range(&self.default_cf(), r))
1399            .collect()
1400    }
1401
1402    /// CF-scoped variant of [`Db::get_approximate_sizes`].
1403    pub fn get_approximate_sizes_cf(
1404        &self,
1405        cf: &ColumnFamilyHandle,
1406        ranges: &[Range<'_>],
1407    ) -> Vec<u64> {
1408        if !self.is_live_cf_handle(cf) {
1409            return vec![0; ranges.len()];
1410        }
1411        ranges
1412            .iter()
1413            .map(|r| self.approximate_size_in_range(cf, r))
1414            .collect()
1415    }
1416
1417    fn approximate_size_in_range(&self, cf: &ColumnFamilyHandle, r: &Range<'_>) -> u64 {
1418        if r.start >= r.end {
1419            return 0;
1420        }
1421        let lo = prefix_key(cf.id(), r.start);
1422        let hi = prefix_key(cf.id(), r.end);
1423        self.engine.approximate_size_in_range(&lo, &hi)
1424    }
1425
1426    /// Exact count + approximate size of entries in the active
1427    /// memtable whose user key falls in `range`, scoped to the
1428    /// default column family. Frozen memtables are not included.
1429    pub fn get_approximate_memtable_stats(&self, range: Range<'_>) -> MemTableStats {
1430        self.memtable_stats_in(&self.default_cf(), &range)
1431    }
1432
1433    /// CF-scoped variant of [`Db::get_approximate_memtable_stats`].
1434    pub fn get_approximate_memtable_stats_cf(
1435        &self,
1436        cf: &ColumnFamilyHandle,
1437        range: Range<'_>,
1438    ) -> MemTableStats {
1439        if !self.is_live_cf_handle(cf) {
1440            return MemTableStats::default();
1441        }
1442        self.memtable_stats_in(cf, &range)
1443    }
1444
1445    fn memtable_stats_in(&self, cf: &ColumnFamilyHandle, range: &Range<'_>) -> MemTableStats {
1446        if range.start >= range.end {
1447            return MemTableStats::default();
1448        }
1449        let lo = prefix_key(cf.id(), range.start);
1450        let hi = prefix_key(cf.id(), range.end);
1451        let (count, size) = self.engine.approximate_memtable_stats(&lo, &hi);
1452        MemTableStats { count, size }
1453    }
1454
1455    /// Bulk-ingest one or more externally-built SSTable files. Each
1456    /// file must have been produced by [`SstFileWriter`]; on success
1457    /// every ingested file is placed at the appropriate level and its
1458    /// keys become visible to new reads and iterators. See
1459    /// [`IngestOptions`] for the snapshot-consistency and placement
1460    /// rules.
1461    ///
1462    /// The source files are left untouched on disk - the engine
1463    /// re-emits each file into the database's own SSTable directory
1464    /// so it can rewrite entry sequence numbers. Callers may delete
1465    /// the source files or re-ingest them at any time.
1466    pub fn ingest_external_files(
1467        &self,
1468        files: &[std::path::PathBuf],
1469        opts: IngestOptions,
1470    ) -> Result<()> {
1471        self.ensure_writable()?;
1472        self.engine
1473            .ingest_external_files(files, &opts, |user_key| {
1474                self.validate_prefixed_cf_io(user_key)
1475            })
1476            .map_err(Error::from)
1477    }
1478
1479    /// Flush all data to disk and shut down background threads.
1480    ///
1481    /// After a successful close, result-returning operations on this
1482    /// handle fail with [`Error::Closed`]. Calling `close` more than
1483    /// once is allowed.
1484    pub fn close(&self) -> Result<()> {
1485        self.engine.close().map_err(Error::from)
1486    }
1487
1488    /// Block until no [`Snapshot`] and no snapshot-backed iterator is
1489    /// live, returning how many pins were still outstanding when
1490    /// `timeout` elapsed. `0` means the wait succeeded.
1491    ///
1492    /// A snapshot pins the SSTables it can see, so closing a database
1493    /// while one is outstanding leaves those files on disk and their
1494    /// readers open. An embedder that wants a clean shutdown has to wait
1495    /// for its readers to finish, and this is that wait: it blocks on
1496    /// the release itself rather than polling a counter, so it costs
1497    /// nothing while it waits and adds no latency once the last reader
1498    /// is done.
1499    ///
1500    /// Counts pins, not handles. An iterator taken from a snapshot pins
1501    /// it again for as long as the iterator lives, so a `Snapshot` that
1502    /// has already been dropped can still be counted here, which is
1503    /// exactly the case an embedder tracking its own transaction objects
1504    /// would miss.
1505    ///
1506    /// Returns a count rather than an error because whether outstanding
1507    /// readers are a failure is the caller's decision: [`Db::close`]
1508    /// does not require this wait, and a database closed with snapshots
1509    /// live is consistent, just not tidy.
1510    ///
1511    /// ```
1512    /// # use std::time::Duration;
1513    /// # use regolith::{Db, Options};
1514    /// # fn main() -> regolith::Result<()> {
1515    /// # let dir = tempfile::tempdir().unwrap();
1516    /// let db = Db::open(dir.path(), Options::default())?;
1517    /// db.put(b"k", b"v")?;
1518    ///
1519    /// let snapshot = db.snapshot();
1520    /// assert_eq!(db.wait_for_snapshots(Duration::from_millis(50)), 1);
1521    ///
1522    /// drop(snapshot);
1523    /// assert_eq!(db.wait_for_snapshots(Duration::from_secs(5)), 0);
1524    /// db.close()?;
1525    /// # Ok(())
1526    /// # }
1527    /// ```
1528    pub fn wait_for_snapshots(&self, timeout: std::time::Duration) -> u64 {
1529        self.engine.wait_for_snapshots(timeout)
1530    }
1531
1532    /// Test-only: number of SSTable files at `level`.
1533    #[cfg(test)]
1534    pub(crate) fn level_file_count(&self, level: usize) -> usize {
1535        self.engine.level_file_count(level)
1536    }
1537
1538    /// Create a hard-linked [`Checkpoint`] of the database.
1539    ///
1540    /// Equivalent to [`Checkpoint::new`] followed by
1541    /// [`Checkpoint::create`]. The call briefly flushes the active
1542    /// memtable and compacts the manifest before any files are
1543    /// linked; concurrent writers continue to make progress.
1544    pub fn checkpoint<P: AsRef<Path>>(&self, target_dir: P) -> Result<()> {
1545        self.ensure_writable()?;
1546        let cp = Checkpoint::new(self)?;
1547        cp.create(target_dir)
1548    }
1549
1550    // ── column families ─────────────────────────────────────────────────
1551
1552    /// Return a handle to the default column family. Always
1553    /// present - [`Db::open`] creates it if the database didn't
1554    /// already contain one.
1555    pub fn default_cf(&self) -> ColumnFamilyHandle {
1556        ColumnFamilyHandle {
1557            name: Arc::new(DEFAULT_CF_NAME.to_string()),
1558            id: DEFAULT_CF_ID,
1559        }
1560    }
1561
1562    /// Look up a column family by name. Returns `None` when no CF
1563    /// with that name has been created (or if the CF was dropped).
1564    pub fn column_family(&self, name: &str) -> Option<ColumnFamilyHandle> {
1565        self.cfs.get(name)
1566    }
1567
1568    /// Return the names of every live column family, including
1569    /// `"default"`. Order is unspecified.
1570    pub fn list_column_families(&self) -> Vec<String> {
1571        let mut names = self.cfs.names();
1572        names.sort();
1573        names
1574    }
1575
1576    /// Create a new column family with `name`. The name must be
1577    /// non-empty and unique; creating a CF with an existing name
1578    /// returns the existing handle (idempotent).
1579    ///
1580    /// The new CF is persisted to the on-disk metadata before this
1581    /// call returns, so it survives a crash and a reopen.
1582    pub fn create_column_family(&self, name: &str) -> Result<ColumnFamilyHandle> {
1583        self.ensure_writable()?;
1584        if name.is_empty() {
1585            return Err(Error::invalid_argument(
1586                "column family name must not be empty",
1587            ));
1588        }
1589        if let Some(existing) = self.cfs.get(name) {
1590            return Ok(existing);
1591        }
1592        self.validate_prefixed_key_size(&meta::name_key(name))?;
1593        let Some((handle, next_id)) = self.cfs.allocate(name) else {
1594            return Err(Error::invalid_argument(
1595                "the column-family id space is exhausted",
1596            ));
1597        };
1598        let mut batch = BTreeMap::new();
1599        batch.insert(
1600            meta::name_key(name),
1601            Some(handle.id().to_be_bytes().to_vec()),
1602        );
1603        batch.insert(meta::next_id_key(), Some(next_id.to_be_bytes().to_vec()));
1604        self.engine
1605            .apply_grouped_batch(batch, Vec::new(), Vec::new(), self.durability, false)
1606            .map(|_| ())
1607            .map_err(Error::from)?;
1608        Ok(handle)
1609    }
1610
1611    /// Drop a column family. Every key stored in the CF is removed
1612    /// via a single range tombstone (O(1) write work regardless of
1613    /// key count) and the CF name is unregistered so future
1614    /// lookups via [`Db::column_family`] return `None`. Space is
1615    /// physically reclaimed by the next compaction over the range.
1616    ///
1617    /// Dropping the default column family is not allowed and
1618    /// returns an error.
1619    pub fn drop_column_family(&self, cf: ColumnFamilyHandle) -> Result<()> {
1620        self.ensure_writable()?;
1621        if cf.id() == DEFAULT_CF_ID {
1622            return Err(Error::invalid_argument(
1623                "cannot drop the default column family",
1624            ));
1625        }
1626        if cf.id() == META_CF_ID {
1627            return Err(Error::invalid_argument(
1628                "cannot drop the reserved metadata column family",
1629            ));
1630        }
1631        self.validate_cf_handle(&cf)?;
1632        let lo = cf_lower_bound(cf.id());
1633        let hi = cf_upper_bound(cf.id());
1634        // Apply the data range-delete and the metadata entry
1635        // removal in a single atomic batch so a crash mid-drop
1636        // either leaves the CF fully present or fully removed.
1637        let mut point_ops = BTreeMap::new();
1638        point_ops.insert(meta::name_key(cf.name()), None);
1639        let range_deletes = vec![(lo, hi)];
1640        self.engine
1641            .apply_grouped_batch(point_ops, range_deletes, Vec::new(), self.durability, false)
1642            .map(|_| ())
1643            .map_err(Error::from)?;
1644        self.cfs.remove(cf.name());
1645        Ok(())
1646    }
1647
1648    /// Read `key` from column family `cf`. Same semantics as
1649    /// [`Db::get`] but scoped to the CF's keyspace.
1650    pub fn get_cf(&self, cf: &ColumnFamilyHandle, key: &[u8]) -> Result<Option<Vec<u8>>> {
1651        Ok(self.get_slice_cf(cf, key)?.map(DbSlice::into_vec))
1652    }
1653
1654    /// Batched point lookup across a single CF.
1655    pub fn multi_get_cf(
1656        &self,
1657        cf: &ColumnFamilyHandle,
1658        keys: &[&[u8]],
1659    ) -> Result<Vec<Option<Vec<u8>>>> {
1660        self.validate_cf_handle(cf)?;
1661        let owned: Vec<Vec<u8>> = keys.iter().map(|k| prefix_key(cf.id(), k)).collect();
1662        let refs: Vec<&[u8]> = owned.iter().map(|k| k.as_slice()).collect();
1663        self.engine.multi_get_latest(&refs).map_err(Error::from)
1664    }
1665
1666    /// Write `key → value` in column family `cf`.
1667    pub fn put_cf(&self, cf: &ColumnFamilyHandle, key: &[u8], value: &[u8]) -> Result<()> {
1668        self.ensure_writable()?;
1669        self.validate_cf_handle(cf)?;
1670        self.validate_write_kv_sizes(key, value)?;
1671        let mut batch = BTreeMap::new();
1672        batch.insert(prefix_key(cf.id(), key), Some(value.to_vec()));
1673        self.engine
1674            .apply_grouped_batch(batch, Vec::new(), Vec::new(), self.durability, false)
1675            .map(|_| ())
1676            .map_err(Error::from)
1677    }
1678
1679    /// Delete `key` in column family `cf`.
1680    pub fn delete_cf(&self, cf: &ColumnFamilyHandle, key: &[u8]) -> Result<()> {
1681        self.ensure_writable()?;
1682        self.validate_cf_handle(cf)?;
1683        self.validate_key_size(key)?;
1684        let mut batch = BTreeMap::new();
1685        batch.insert(prefix_key(cf.id(), key), None);
1686        self.engine
1687            .apply_grouped_batch(batch, Vec::new(), Vec::new(), self.durability, false)
1688            .map(|_| ())
1689            .map_err(Error::from)
1690    }
1691
1692    /// Delete every key in `[start, end)` in column family `cf`.
1693    ///
1694    /// An empty range is still a write: a read-only or closed handle
1695    /// rejects it with [`Error::ReadOnly`] or [`Error::Closed`] rather
1696    /// than returning `Ok`.
1697    pub fn delete_range_cf(&self, cf: &ColumnFamilyHandle, start: &[u8], end: &[u8]) -> Result<()> {
1698        self.ensure_writable()?;
1699        if start >= end {
1700            return Ok(());
1701        }
1702        self.validate_cf_handle(cf)?;
1703        self.validate_key_size(start)?;
1704        self.validate_key_size(end)?;
1705        self.engine
1706            .apply_grouped_batch(
1707                BTreeMap::new(),
1708                vec![(prefix_key(cf.id(), start), prefix_key(cf.id(), end))],
1709                Vec::new(),
1710                self.durability,
1711                false,
1712            )
1713            .map(|_| ())
1714            .map_err(Error::from)
1715    }
1716
1717    /// Layer a merge operand on top of `key` in column family `cf`.
1718    /// Requires [`Options::merge_operator`] to be set.
1719    pub fn merge_cf(&self, cf: &ColumnFamilyHandle, key: &[u8], operand: &[u8]) -> Result<()> {
1720        self.ensure_writable()?;
1721        self.validate_cf_handle(cf)?;
1722        self.validate_write_kv_sizes(key, operand)?;
1723        self.engine
1724            .apply_grouped_batch(
1725                BTreeMap::new(),
1726                Vec::new(),
1727                vec![(prefix_key(cf.id(), key), operand.to_vec())],
1728                self.durability,
1729                false,
1730            )
1731            .map(|_| ())
1732            .map_err(Error::from)
1733    }
1734
1735    /// Scan a key range inside column family `cf`.
1736    ///
1737    /// Returned keys have the CF prefix stripped - they appear
1738    /// exactly as the caller supplied them on put. This convenience
1739    /// method materializes the entire range into memory. Prefer
1740    /// [`Db::iter_cf`] for streaming scans or [`Db::scan_page_cf`]
1741    /// when the caller needs an explicit page size.
1742    pub fn scan_cf(
1743        &self,
1744        cf: &ColumnFamilyHandle,
1745        start: Option<&[u8]>,
1746        end: Option<&[u8]>,
1747    ) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
1748        self.validate_cf_handle(cf)?;
1749        let lo = match start {
1750            Some(s) => prefix_key(cf.id(), s),
1751            None => cf_lower_bound(cf.id()),
1752        };
1753        let hi = match end {
1754            Some(e) => prefix_key(cf.id(), e),
1755            None => cf_upper_bound(cf.id()),
1756        };
1757        // `new_iter_latest` loads the published view and *then* samples the
1758        // horizon. Sampling first and building the iterator after leaves a
1759        // window where a compaction no snapshot pins can drop the newest
1760        // version at or below the sampled sequence, after which the scan
1761        // finds only versions it must filter out and a key reads absent.
1762        let raw = collect_range(self.engine.new_iter_latest(), Some(&lo), Some(&hi))?;
1763        strip_cf_prefix_entries(raw)
1764    }
1765
1766    /// Scan a bounded page inside column family `cf`.
1767    ///
1768    /// At most `limit` entries are materialized, and returned keys have
1769    /// the CF prefix stripped. When [`ScanPage::next_start`] is `Some`,
1770    /// pass that key back as `start` to continue the scan.
1771    pub fn scan_page_cf(
1772        &self,
1773        cf: &ColumnFamilyHandle,
1774        start: Option<&[u8]>,
1775        end: Option<&[u8]>,
1776        limit: usize,
1777    ) -> Result<ScanPage> {
1778        self.validate_cf_handle(cf)?;
1779        let lo = match start {
1780            Some(s) => prefix_key(cf.id(), s),
1781            None => cf_lower_bound(cf.id()),
1782        };
1783        let hi = match end {
1784            Some(e) => prefix_key(cf.id(), e),
1785            None => cf_upper_bound(cf.id()),
1786        };
1787        collect_page(self.engine.new_iter_latest(), &lo, &hi, limit).and_then(strip_cf_prefix_page)
1788    }
1789
1790    /// Streaming iterator bounded to column family `cf`. The
1791    /// returned keys have the CF prefix stripped.
1792    pub fn iter_cf<'a>(&'a self, cf: &ColumnFamilyHandle) -> CfIter<'a> {
1793        let inner = self.raw_iter();
1794        if self.is_live_cf_handle(cf) {
1795            CfIter::new(inner, cf.id())
1796        } else {
1797            CfIter::invalid(inner, cf)
1798        }
1799    }
1800
1801    /// Create a forward-only [`TailingIter`] over the default
1802    /// column family. Unlike [`Db::iter`], a tailing iterator
1803    /// sees writes that arrive after it was created and does not
1804    /// pin the database at a point in time - see [`TailingIter`]
1805    /// for the ordering rules.
1806    pub fn iter_tailing(&self) -> TailingIter {
1807        tailing::new_default(Arc::clone(&self.engine))
1808    }
1809
1810    /// Create a forward-only [`TailingIter`] scoped to column
1811    /// family `cf`.
1812    pub fn iter_tailing_cf(&self, cf: &ColumnFamilyHandle) -> TailingIter {
1813        let engine = Arc::clone(&self.engine);
1814        if self.is_live_cf_handle(cf) {
1815            tailing::new_for_cf(engine, cf)
1816        } else {
1817            tailing::new_empty(engine, cf)
1818        }
1819    }
1820
1821    pub(crate) fn engine(&self) -> &RegolithEngine {
1822        &self.engine
1823    }
1824
1825    /// Clone the engine `Arc` - used by transaction facade types
1826    /// that need to carry an engine reference around independent
1827    /// of the owning `Db`'s lifetime. Internal-only.
1828    pub(crate) fn engine_arc(&self) -> Arc<RegolithEngine> {
1829        Arc::clone(&self.engine)
1830    }
1831
1832    /// Database-global durability mode. Used by transaction
1833    /// commit code to choose fsync semantics.
1834    pub(crate) fn durability(&self) -> engine::DurabilityMode {
1835        self.durability
1836    }
1837}
1838
1839/// Streaming iterator scoped to a single column family. Wraps a
1840/// regular [`Iter`] and bounds the scan to the CF's prefix range,
1841/// stripping the 4-byte CF prefix from every key before returning
1842/// it. Created by [`Db::iter_cf`] / [`Snapshot::iter_cf`].
1843pub struct CfIter<'a> {
1844    inner: Iter<'a>,
1845    cf_id: u32,
1846    upper_bound: Vec<u8>,
1847    valid_cf: bool,
1848    /// Why this iterator has no column family to read, when it has none.
1849    ///
1850    /// A handle that is not live is a detected error, and the rest of the CF
1851    /// read surface returns it. An iterator cannot, so it is held here and
1852    /// handed back by [`CfIter::status`]. `None` on every live iterator, so
1853    /// the path that works allocates nothing.
1854    invalid_cf: Option<Box<str>>,
1855}
1856
1857impl<'a> CfIter<'a> {
1858    fn new(inner: Iter<'a>, cf_id: u32) -> Self {
1859        Self {
1860            invalid_cf: None,
1861            inner,
1862            cf_id,
1863            upper_bound: cf_upper_bound(cf_id),
1864            valid_cf: true,
1865        }
1866    }
1867
1868    fn invalid(inner: Iter<'a>, cf: &ColumnFamilyHandle) -> Self {
1869        Self {
1870            inner,
1871            cf_id: DEFAULT_CF_ID,
1872            upper_bound: cf_upper_bound(DEFAULT_CF_ID),
1873            valid_cf: false,
1874            invalid_cf: Some(
1875                format!(
1876                    "column family handle '{}' with id {} is not live",
1877                    cf.name(),
1878                    cf.id()
1879                )
1880                .into_boxed_str(),
1881            ),
1882        }
1883    }
1884
1885    /// Position the cursor at the first key in the CF.
1886    pub fn seek_to_first(&mut self) {
1887        if !self.valid_cf {
1888            return;
1889        }
1890        let lo = self.cf_id.to_be_bytes();
1891        self.inner.seek(&lo);
1892    }
1893
1894    /// Position the cursor at the last key in the CF (or before
1895    /// the CF's upper bound if the CF is empty).
1896    pub fn seek_to_last(&mut self) {
1897        if !self.valid_cf {
1898            return;
1899        }
1900        self.inner.seek_to_last_before(&self.upper_bound);
1901    }
1902
1903    /// Position the cursor at the first key `>= target` in the CF.
1904    pub fn seek(&mut self, target: &[u8]) {
1905        if !self.valid_cf {
1906            return;
1907        }
1908        self.inner.seek(&prefix_key(self.cf_id, target));
1909    }
1910
1911    /// Position the cursor at the last key `<= target` in the CF.
1912    pub fn seek_for_prev(&mut self, target: &[u8]) {
1913        if !self.valid_cf {
1914            return;
1915        }
1916        self.inner.seek_for_prev(&prefix_key(self.cf_id, target));
1917    }
1918
1919    /// Position the cursor at the first key in this CF that starts
1920    /// with `prefix`, and bound subsequent forward iteration to
1921    /// that prefix. Delegates to the underlying [`Iter::seek_prefix`],
1922    /// with `prefix` first re-scoped to include the CF prefix.
1923    pub fn seek_prefix(&mut self, prefix: &[u8]) {
1924        if !self.valid_cf {
1925            return;
1926        }
1927        self.inner.seek_prefix(&prefix_key(self.cf_id, prefix));
1928    }
1929
1930    /// Advance the cursor forward. Invalidates the iterator if
1931    /// the next key crosses the CF's upper bound.
1932    pub fn next(&mut self) {
1933        if !self.valid_cf {
1934            return;
1935        }
1936        self.inner.next();
1937    }
1938
1939    /// Move the cursor backward. Invalidates the iterator if
1940    /// the previous key crosses the CF's lower bound.
1941    pub fn prev(&mut self) {
1942        if !self.valid_cf {
1943            return;
1944        }
1945        self.inner.prev();
1946    }
1947
1948    /// Whether the caller has positioned this cursor at all. See
1949    /// [`Iter::positioned`].
1950    pub fn positioned(&self) -> bool {
1951        self.inner.positioned()
1952    }
1953
1954    /// Whether the cursor is positioned on a visible key within
1955    /// the CF.
1956    pub fn valid(&self) -> bool {
1957        if !self.valid_cf {
1958            return false;
1959        }
1960        let Some(k) = self.inner.key() else {
1961            return false;
1962        };
1963        if k < self.cf_id.to_be_bytes().as_slice() {
1964            return false;
1965        }
1966        if k >= self.upper_bound.as_slice() {
1967            return false;
1968        }
1969        true
1970    }
1971
1972    /// Current key, with the CF prefix stripped.
1973    pub fn key(&self) -> Option<&[u8]> {
1974        if !self.valid() {
1975            return None;
1976        }
1977        self.inner.key().and_then(|k| k.get(4..))
1978    }
1979
1980    /// Current value.
1981    pub fn value(&self) -> Option<&[u8]> {
1982        if !self.valid() {
1983            return None;
1984        }
1985        self.inner.value()
1986    }
1987
1988    /// Current value as a [`DbSlice`], which outlives the cursor
1989    /// moving on. See [`Iter::value_slice`].
1990    pub fn value_slice(&self) -> Option<DbSlice> {
1991        if !self.valid() {
1992            return None;
1993        }
1994        self.inner.value_slice()
1995    }
1996
1997    /// Why the walk stopped, or why it never started.
1998    ///
1999    /// A handle that is not live is reported here rather than dropped. The
2000    /// rest of the CF read surface (`get_cf`, `scan_cf`, `scan_page_cf`,
2001    /// `multi_get_cf`) returns that as an `Err`; an iterator has no way to,
2002    /// so without this a dropped column family, or a handle belonging to a
2003    /// different `Db`, would read as an empty one and report success.
2004    pub fn status(&self) -> Result<()> {
2005        if let Some(reason) = &self.invalid_cf {
2006            return Err(Error::invalid_column_family(reason.to_string()));
2007        }
2008        self.inner.status()
2009    }
2010}
2011
2012/// Owned streaming iterator over a [`Snapshot`] in the default column
2013/// family. This is useful for adapters that need to return an owned
2014/// iterator object without tying the type to a borrowed snapshot
2015/// lifetime.
2016pub struct OwnedSnapshotIter {
2017    inner: CfIter<'static>,
2018    _snapshot: Snapshot,
2019}
2020
2021impl OwnedSnapshotIter {
2022    fn new(snapshot: Snapshot) -> Self {
2023        let inner = Iter::<'static>::from_internal(snapshot.engine.new_iter_at(snapshot.seq))
2024            .with_stats(snapshot.engine.statistics_arc());
2025        Self {
2026            inner: CfIter::new(inner, DEFAULT_CF_ID),
2027            _snapshot: snapshot,
2028        }
2029    }
2030
2031    /// Position the cursor at the first key in the default CF.
2032    pub fn seek_to_first(&mut self) {
2033        self.inner.seek_to_first();
2034    }
2035
2036    /// Position the cursor at the last key in the default CF.
2037    pub fn seek_to_last(&mut self) {
2038        self.inner.seek_to_last();
2039    }
2040
2041    /// Position the cursor at the first key `>= target`.
2042    pub fn seek(&mut self, target: &[u8]) {
2043        self.inner.seek(target);
2044    }
2045
2046    /// Position the cursor at the last key `<= target`.
2047    pub fn seek_for_prev(&mut self, target: &[u8]) {
2048        self.inner.seek_for_prev(target);
2049    }
2050
2051    /// Position the cursor at the first key with `prefix`.
2052    pub fn seek_prefix(&mut self, prefix: &[u8]) {
2053        self.inner.seek_prefix(prefix);
2054    }
2055
2056    /// Advance the cursor forward.
2057    pub fn next(&mut self) {
2058        self.inner.next();
2059    }
2060
2061    /// Move the cursor backward.
2062    pub fn prev(&mut self) {
2063        self.inner.prev();
2064    }
2065
2066    /// Whether the caller has positioned this cursor at all. See
2067    /// [`Iter::positioned`].
2068    pub fn positioned(&self) -> bool {
2069        self.inner.positioned()
2070    }
2071
2072    /// Whether the cursor is positioned on a visible key.
2073    pub fn valid(&self) -> bool {
2074        self.inner.valid()
2075    }
2076
2077    /// Current key.
2078    pub fn key(&self) -> Option<&[u8]> {
2079        self.inner.key()
2080    }
2081
2082    /// Current value.
2083    pub fn value(&self) -> Option<&[u8]> {
2084        self.inner.value()
2085    }
2086
2087    /// Current value as a [`DbSlice`], which outlives the cursor
2088    /// moving on. See [`Iter::value_slice`].
2089    pub fn value_slice(&self) -> Option<DbSlice> {
2090        self.inner.value_slice()
2091    }
2092
2093    /// Propagate any I/O error from the underlying iterator.
2094    pub fn status(&self) -> Result<()> {
2095        self.inner.status()
2096    }
2097}
2098
2099/// A lazy, bounded scan over a key range.
2100///
2101/// Returned by [`Db::scan_stream`] and [`Snapshot::scan_stream`]. Holds
2102/// one entry at a time rather than the range, so a caller that stops
2103/// early pays only for what it read: the opposite of [`Db::scan`], which
2104/// reads the whole range up front. The value is a [`DbSlice`], so no
2105/// value bytes are copied.
2106///
2107/// The scan runs against a pinned snapshot, so writes that land while it
2108/// is being drained are invisible to it and the range cannot shift
2109/// underneath the cursor.
2110pub struct ScanStream {
2111    entries: Entries<OwnedSnapshotIter>,
2112    /// Exclusive upper bound in user-visible form, or `None` for the end
2113    /// of the column family.
2114    end: Option<Vec<u8>>,
2115    done: bool,
2116}
2117
2118impl ScanStream {
2119    /// Why the scan stopped.
2120    ///
2121    /// `Ok(())` means the range ended. An error means it did not: what the
2122    /// stream yielded is a prefix of the range and the rest was never read.
2123    ///
2124    /// This matters because [`Iterator`] cannot carry a failure. A scan that
2125    /// dies on a corrupt block ends exactly like one that reached the end of
2126    /// its range, and a caller that only iterates cannot tell a short answer
2127    /// from a complete one. Check this after iterating whenever a missing row
2128    /// would be worse than an error.
2129    ///
2130    /// ```no_run
2131    /// # use regolith::{Db, Options};
2132    /// # let db = Db::open("/tmp/scan_status_doc", Options::default()).unwrap();
2133    /// let mut scan = db.scan_stream(None, None)?;
2134    /// let rows: Vec<_> = scan.by_ref().collect();
2135    /// scan.status()?;  // the rows above are the whole range only if this is Ok
2136    /// # Ok::<(), regolith::Error>(())
2137    /// ```
2138    pub fn status(&self) -> Result<()> {
2139        self.entries.status()
2140    }
2141}
2142
2143impl Iterator for ScanStream {
2144    type Item = (Vec<u8>, DbSlice);
2145
2146    fn next(&mut self) -> Option<Self::Item> {
2147        if self.done {
2148            return None;
2149        }
2150        let (key, value) = self.entries.next()?;
2151        if let Some(end) = &self.end
2152            && key.as_slice() >= end.as_slice()
2153        {
2154            // Keys come out ascending, so the first key at or past the
2155            // bound ends the scan; nothing after it can be in range.
2156            self.done = true;
2157            return None;
2158        }
2159        Some((key, value))
2160    }
2161}
2162
2163impl std::fmt::Debug for ScanStream {
2164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2165        f.debug_struct("ScanStream")
2166            .field("done", &self.done)
2167            .finish_non_exhaustive()
2168    }
2169}
2170
2171/// Ordered entries drained from a cursor, from its current position on.
2172///
2173/// Returned by the `IntoIterator` impls on the cursors and by
2174/// [`OwnedSnapshotIter::entries`] / [`OwnedSnapshotIter::entries_rev`].
2175/// The value is a [`DbSlice`], so iterating copies keys but never value
2176/// bytes: a key is reassembled from its prefix-compressed form into a
2177/// buffer the cursor owns and has to be copied out, while a value is
2178/// stored whole and can be handed over by reference.
2179///
2180/// This is the seam to build a `Stream` on. regolith's IO is synchronous
2181/// and it requires no async runtime, so wrapping a ready iterator with
2182/// `futures::stream::iter` belongs where the async context is rather than
2183/// here, where it would add a dependency and never yield.
2184pub struct Entries<C> {
2185    cursor: C,
2186    started: bool,
2187    reverse: bool,
2188}
2189
2190impl<C> Entries<C> {
2191    fn new(cursor: C, reverse: bool) -> Self {
2192        Self {
2193            cursor,
2194            started: false,
2195            reverse,
2196        }
2197    }
2198
2199    /// Give the cursor back, positioned wherever iteration stopped.
2200    pub fn into_cursor(self) -> C {
2201        self.cursor
2202    }
2203}
2204
2205/// A cursor already positioned by `seek` is left where it is on the first
2206/// step, so seeking and then iterating resumes from the seek instead of
2207/// restarting at the end of the range.
2208macro_rules! impl_entries {
2209    ($cursor:ty $(, $lt:lifetime)?) => {
2210        impl$(<$lt>)? Iterator for Entries<$cursor> {
2211            type Item = (Vec<u8>, DbSlice);
2212
2213            fn next(&mut self) -> Option<Self::Item> {
2214                if self.started {
2215                    if self.reverse {
2216                        self.cursor.prev();
2217                    } else {
2218                        self.cursor.next();
2219                    }
2220                } else {
2221                    self.started = true;
2222                    // `positioned`, not `valid`. A cursor the caller seeked
2223                    // past the end of the range is invalid but positioned,
2224                    // and seeking it again would hand back the very rows the
2225                    // caller seeked away from.
2226                    if !self.cursor.positioned() {
2227                        if self.reverse {
2228                            self.cursor.seek_to_last();
2229                        } else {
2230                            self.cursor.seek_to_first();
2231                        }
2232                    }
2233                }
2234                if !self.cursor.valid() {
2235                    // A cursor goes invalid for two reasons that look
2236                    // identical from here: the range ended, or the walk
2237                    // failed. `Iterator` has nowhere to put the difference,
2238                    // so say it out loud rather than let a failed scan read
2239                    // as a complete one. `Entries::status` returns it to a
2240                    // caller that checks.
2241                    if let Err(e) = self.cursor.status() {
2242                        tracing::error!(
2243                            error = %e,
2244                            "scan ended early: the iterator failed mid-range, \
2245                             so the rows returned are a prefix and not the range"
2246                        );
2247                    }
2248                    return None;
2249                }
2250                let key = self.cursor.key()?.to_vec();
2251                let value = self.cursor.value_slice()?;
2252                Some((key, value))
2253            }
2254        }
2255
2256        impl$(<$lt>)? Entries<$cursor> {
2257            /// Why the walk stopped.
2258            ///
2259            /// `Ok(())` means the range ended. An error means it did not:
2260            /// the entries handed out are a prefix of the range, and the
2261            /// rest was not read. Iterating alone cannot tell the two
2262            /// apart, so a caller that must not silently lose rows checks
2263            /// this once the iteration finishes.
2264            pub fn status(&self) -> Result<()> {
2265                self.cursor.status()
2266            }
2267        }
2268
2269        impl$(<$lt>)? IntoIterator for $cursor {
2270            type Item = (Vec<u8>, DbSlice);
2271            type IntoIter = Entries<$cursor>;
2272
2273            fn into_iter(self) -> Self::IntoIter {
2274                Entries::new(self, false)
2275            }
2276        }
2277    };
2278}
2279
2280impl_entries!(OwnedSnapshotIter);
2281impl_entries!(CfIter<'a>, 'a);
2282
2283impl OwnedSnapshotIter {
2284    /// Iterate forward from the cursor's current position.
2285    pub fn entries(self) -> Entries<Self> {
2286        Entries::new(self, false)
2287    }
2288
2289    /// Iterate backward from the cursor's current position.
2290    pub fn entries_rev(self) -> Entries<Self> {
2291        Entries::new(self, true)
2292    }
2293}
2294
2295impl<'a> CfIter<'a> {
2296    /// Iterate forward from the cursor's current position.
2297    pub fn entries(self) -> Entries<CfIter<'a>> {
2298        Entries::new(self, false)
2299    }
2300
2301    /// Iterate backward from the cursor's current position.
2302    pub fn entries_rev(self) -> Entries<CfIter<'a>> {
2303        Entries::new(self, true)
2304    }
2305}
2306
2307/// A point-in-time snapshot for consistent reads.
2308pub struct Snapshot {
2309    engine: Arc<RegolithEngine>,
2310    cfs: Arc<CfRegistry>,
2311    seq: u64,
2312}
2313
2314impl Drop for Snapshot {
2315    fn drop(&mut self) {
2316        // Release the pin this snapshot held in the engine's
2317        // compaction GC registry. Compaction is now free to drop any
2318        // version it was keeping alive for this snapshot's sake,
2319        // subject to other live snapshots that may still pin older
2320        // seqs.
2321        self.engine.release_snapshot(self.seq);
2322    }
2323}
2324
2325impl std::fmt::Debug for Snapshot {
2326    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2327        f.debug_struct("Snapshot")
2328            .field("seq", &self.seq)
2329            .finish_non_exhaustive()
2330    }
2331}
2332
2333impl Snapshot {
2334    fn clone_pin(&self) -> Self {
2335        self.engine.register_snapshot(self.seq);
2336        Self {
2337            engine: Arc::clone(&self.engine),
2338            cfs: Arc::clone(&self.cfs),
2339            seq: self.seq,
2340        }
2341    }
2342
2343    fn validate_cf_handle(&self, cf: &ColumnFamilyHandle) -> Result<()> {
2344        if self.cfs.is_live_handle(cf) {
2345            Ok(())
2346        } else {
2347            Err(invalid_cf_handle_error(cf))
2348        }
2349    }
2350
2351    fn is_live_cf_handle(&self, cf: &ColumnFamilyHandle) -> bool {
2352        self.cfs.is_live_handle(cf)
2353    }
2354
2355    /// The engine sequence this snapshot reads at.
2356    ///
2357    /// Every write with a sequence at or below this value is visible here, and
2358    /// every later write is not. Pairs with [`Db::write_sequenced`], which
2359    /// returns the sequence a batch committed at, so an upper layer can order
2360    /// its own versions against regolith's without serializing commits behind a
2361    /// lock of its own: the horizon publishes atomically inside the write, and
2362    /// a snapshot captures it atomically here.
2363    pub fn sequence(&self) -> u64 {
2364        self.seq
2365    }
2366
2367    /// Get the value for a key at this snapshot (default CF).
2368    pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
2369        Ok(self.get_slice(key)?.map(DbSlice::into_vec))
2370    }
2371
2372    /// Read a value at this snapshot without copying it. See
2373    /// [`Db::get_slice`] and [`DbSlice`].
2374    pub fn get_slice(&self, key: &[u8]) -> Result<Option<DbSlice>> {
2375        self.lookup_slice(&LookupKey::new(DEFAULT_CF_ID, key, self.seq))
2376    }
2377
2378    /// [`Snapshot::get_slice`] scoped to a column family.
2379    pub fn get_slice_cf(&self, cf: &ColumnFamilyHandle, key: &[u8]) -> Result<Option<DbSlice>> {
2380        self.validate_cf_handle(cf)?;
2381        self.lookup_slice(&LookupKey::new(cf.id(), key, self.seq))
2382    }
2383
2384    fn lookup_slice(&self, lk: &LookupKey) -> Result<Option<DbSlice>> {
2385        self.engine
2386            .get_slice(lk)
2387            .map_err(|err| map_point_read_error(err, lk.prefixed_user_key()))
2388    }
2389
2390    /// Whether a live value exists for `key` at this snapshot. See
2391    /// [`Db::has`] for what this does and does not avoid.
2392    pub fn has(&self, key: &[u8]) -> Result<bool> {
2393        Ok(self.get_size(key)?.is_some())
2394    }
2395
2396    /// [`Snapshot::has`] scoped to a column family.
2397    pub fn has_cf(&self, cf: &ColumnFamilyHandle, key: &[u8]) -> Result<bool> {
2398        Ok(self.get_size_cf(cf, key)?.is_some())
2399    }
2400
2401    /// Length in bytes of the live value for `key` at this snapshot,
2402    /// or `None` when there is none. See [`Db::get_size`].
2403    pub fn get_size(&self, key: &[u8]) -> Result<Option<usize>> {
2404        self.lookup_size(&LookupKey::new(DEFAULT_CF_ID, key, self.seq))
2405    }
2406
2407    /// [`Snapshot::get_size`] scoped to a column family.
2408    pub fn get_size_cf(&self, cf: &ColumnFamilyHandle, key: &[u8]) -> Result<Option<usize>> {
2409        self.validate_cf_handle(cf)?;
2410        self.lookup_size(&LookupKey::new(cf.id(), key, self.seq))
2411    }
2412
2413    fn lookup_size(&self, lk: &LookupKey) -> Result<Option<usize>> {
2414        self.engine
2415            .get_size(lk)
2416            .map_err(|err| map_point_read_error(err, lk.prefixed_user_key()))
2417    }
2418
2419    /// Batched point lookup anchored at this snapshot (default CF).
2420    pub fn multi_get(&self, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>> {
2421        let owned: Vec<Vec<u8>> = keys.iter().map(|k| prefix_key(DEFAULT_CF_ID, k)).collect();
2422        let refs: Vec<&[u8]> = owned.iter().map(|k| k.as_slice()).collect();
2423        self.engine
2424            .multi_get_at(&refs, self.seq)
2425            .map_err(Error::from)
2426    }
2427
2428    /// Scan a key range at this snapshot (default CF).
2429    ///
2430    /// This convenience method materializes the entire range into
2431    /// memory. Prefer [`Snapshot::iter`] for streaming scans or
2432    /// [`Snapshot::scan_page`] when the caller needs an explicit page
2433    /// size.
2434    pub fn scan(
2435        &self,
2436        start: Option<&[u8]>,
2437        end: Option<&[u8]>,
2438    ) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
2439        let lo = match start {
2440            Some(s) => prefix_key(DEFAULT_CF_ID, s),
2441            None => cf_lower_bound(DEFAULT_CF_ID),
2442        };
2443        let hi = match end {
2444            Some(e) => prefix_key(DEFAULT_CF_ID, e),
2445            None => cf_upper_bound(DEFAULT_CF_ID),
2446        };
2447        let raw = collect_range(self.engine.new_iter_at(self.seq), Some(&lo), Some(&hi))?;
2448        strip_cf_prefix_entries(raw)
2449    }
2450
2451    /// Scan a bounded page at this snapshot (default CF).
2452    ///
2453    /// At most `limit` entries are materialized. When
2454    /// [`ScanPage::next_start`] is `Some`, pass that key back as
2455    /// `start` to continue the same snapshot-consistent range.
2456    pub fn scan_page(
2457        &self,
2458        start: Option<&[u8]>,
2459        end: Option<&[u8]>,
2460        limit: usize,
2461    ) -> Result<ScanPage> {
2462        let lo = match start {
2463            Some(s) => prefix_key(DEFAULT_CF_ID, s),
2464            None => cf_lower_bound(DEFAULT_CF_ID),
2465        };
2466        let hi = match end {
2467            Some(e) => prefix_key(DEFAULT_CF_ID, e),
2468            None => cf_upper_bound(DEFAULT_CF_ID),
2469        };
2470        collect_page(self.engine.new_iter_at(self.seq), &lo, &hi, limit)
2471            .and_then(strip_cf_prefix_page)
2472    }
2473
2474    /// Create a streaming iterator anchored at this snapshot
2475    /// (default CF). Keys returned have the CF prefix stripped.
2476    pub fn iter(&self) -> CfIter<'_> {
2477        CfIter::new(
2478            Iter::from_internal(self.engine.new_iter_at(self.seq))
2479                .with_stats(self.engine.statistics_arc()),
2480            DEFAULT_CF_ID,
2481        )
2482    }
2483
2484    /// Scan a key range lazily against this snapshot.
2485    ///
2486    /// The streaming counterpart to [`Snapshot::scan`]. The whole walk
2487    /// sees one point in time, because the snapshot stays pinned for as
2488    /// long as the stream lives.
2489    pub fn scan_stream(&self, start: Option<&[u8]>, end: Option<&[u8]>) -> ScanStream {
2490        self.clone_pin().into_scan_stream(start, end)
2491    }
2492
2493    /// [`Snapshot::scan_stream`], consuming the snapshot instead of
2494    /// pinning a second handle on it.
2495    pub fn into_scan_stream(self, start: Option<&[u8]>, end: Option<&[u8]>) -> ScanStream {
2496        let end = end.map(<[u8]>::to_vec);
2497        let mut cursor = self.into_owned_iter();
2498        match start {
2499            Some(start) => cursor.seek(start),
2500            None => cursor.seek_to_first(),
2501        }
2502        ScanStream {
2503            entries: Entries::new(cursor, false),
2504            end,
2505            done: false,
2506        }
2507    }
2508
2509    /// Create an owned streaming iterator at this snapshot's sequence
2510    /// (default CF). The iterator takes its own snapshot pin, so the
2511    /// original snapshot remains usable for later reads.
2512    pub fn owned_iter(&self) -> OwnedSnapshotIter {
2513        OwnedSnapshotIter::new(self.clone_pin())
2514    }
2515
2516    /// Consume this snapshot and create an owned streaming iterator
2517    /// (default CF). The iterator keeps the snapshot pin alive for its
2518    /// own lifetime, so it can be stored in trait objects that cannot
2519    /// express a borrow from `Snapshot`.
2520    pub fn into_owned_iter(self) -> OwnedSnapshotIter {
2521        OwnedSnapshotIter::new(self)
2522    }
2523
2524    /// CF-scoped get at this snapshot.
2525    pub fn get_cf(&self, cf: &ColumnFamilyHandle, key: &[u8]) -> Result<Option<Vec<u8>>> {
2526        Ok(self.get_slice_cf(cf, key)?.map(DbSlice::into_vec))
2527    }
2528
2529    /// CF-scoped multi_get at this snapshot.
2530    pub fn multi_get_cf(
2531        &self,
2532        cf: &ColumnFamilyHandle,
2533        keys: &[&[u8]],
2534    ) -> Result<Vec<Option<Vec<u8>>>> {
2535        self.validate_cf_handle(cf)?;
2536        let owned: Vec<Vec<u8>> = keys.iter().map(|k| prefix_key(cf.id(), k)).collect();
2537        let refs: Vec<&[u8]> = owned.iter().map(|k| k.as_slice()).collect();
2538        self.engine
2539            .multi_get_at(&refs, self.seq)
2540            .map_err(Error::from)
2541    }
2542
2543    /// CF-scoped scan at this snapshot.
2544    ///
2545    /// Returned keys have the CF prefix stripped. This convenience
2546    /// method materializes the entire range into memory. Prefer
2547    /// [`Snapshot::iter_cf`] for streaming scans or
2548    /// [`Snapshot::scan_page_cf`] when the caller needs an explicit
2549    /// page size.
2550    pub fn scan_cf(
2551        &self,
2552        cf: &ColumnFamilyHandle,
2553        start: Option<&[u8]>,
2554        end: Option<&[u8]>,
2555    ) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
2556        self.validate_cf_handle(cf)?;
2557        let lo = match start {
2558            Some(s) => prefix_key(cf.id(), s),
2559            None => cf_lower_bound(cf.id()),
2560        };
2561        let hi = match end {
2562            Some(e) => prefix_key(cf.id(), e),
2563            None => cf_upper_bound(cf.id()),
2564        };
2565        let raw = collect_range(self.engine.new_iter_at(self.seq), Some(&lo), Some(&hi))?;
2566        strip_cf_prefix_entries(raw)
2567    }
2568
2569    /// CF-scoped bounded page at this snapshot.
2570    ///
2571    /// At most `limit` entries are materialized, and returned keys have
2572    /// the CF prefix stripped. When [`ScanPage::next_start`] is `Some`,
2573    /// pass that key back as `start` to continue the same
2574    /// snapshot-consistent range.
2575    pub fn scan_page_cf(
2576        &self,
2577        cf: &ColumnFamilyHandle,
2578        start: Option<&[u8]>,
2579        end: Option<&[u8]>,
2580        limit: usize,
2581    ) -> Result<ScanPage> {
2582        self.validate_cf_handle(cf)?;
2583        let lo = match start {
2584            Some(s) => prefix_key(cf.id(), s),
2585            None => cf_lower_bound(cf.id()),
2586        };
2587        let hi = match end {
2588            Some(e) => prefix_key(cf.id(), e),
2589            None => cf_upper_bound(cf.id()),
2590        };
2591        collect_page(self.engine.new_iter_at(self.seq), &lo, &hi, limit)
2592            .and_then(strip_cf_prefix_page)
2593    }
2594
2595    /// CF-scoped streaming iterator at this snapshot.
2596    pub fn iter_cf<'a>(&'a self, cf: &ColumnFamilyHandle) -> CfIter<'a> {
2597        let inner = Iter::from_internal(self.engine.new_iter_at(self.seq))
2598            .with_stats(self.engine.statistics_arc());
2599        if self.is_live_cf_handle(cf) {
2600            CfIter::new(inner, cf.id())
2601        } else {
2602            CfIter::invalid(inner, cf)
2603        }
2604    }
2605}
2606
2607/// Collect a bounded range of `(user_key, value)` pairs via the streaming
2608/// iterator. This is the engine of `Db::scan` / `Snapshot::scan`; the
2609/// dedicated method exists so both callers share one merge implementation.
2610fn collect_range(
2611    mut iter: crate::engine::iterator::RegolithIterator,
2612    start: Option<&[u8]>,
2613    end: Option<&[u8]>,
2614) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
2615    match start {
2616        Some(s) => iter.seek(s),
2617        None => iter.seek_to_first(),
2618    }
2619    iter.status().map_err(Error::from)?;
2620
2621    let mut out = Vec::new();
2622    while iter.valid() {
2623        let (Some(k), Some(v)) = (iter.key(), iter.value()) else {
2624            break;
2625        };
2626        if let Some(e) = end
2627            && k >= e
2628        {
2629            break;
2630        }
2631        out.push((k.to_vec(), v.to_vec()));
2632        iter.next();
2633    }
2634    iter.status().map_err(Error::from)?;
2635    Ok(out)
2636}
2637
2638fn collect_page(
2639    mut iter: crate::engine::iterator::RegolithIterator,
2640    start: &[u8],
2641    end: &[u8],
2642    limit: usize,
2643) -> Result<ScanPage> {
2644    if limit == 0 {
2645        return Err(invalid_input_error(
2646            "scan page limit must be greater than zero",
2647        ));
2648    }
2649
2650    iter.seek(start);
2651    iter.status().map_err(Error::from)?;
2652
2653    let mut entries = Vec::new();
2654    let mut next_start = None;
2655    while iter.valid() {
2656        let (Some(k), Some(v)) = (iter.key(), iter.value()) else {
2657            break;
2658        };
2659        if k >= end {
2660            break;
2661        }
2662        if entries.len() == limit {
2663            next_start = Some(k.to_vec());
2664            break;
2665        }
2666        entries.push((k.to_vec(), v.to_vec()));
2667        iter.next();
2668    }
2669    iter.status().map_err(Error::from)?;
2670    Ok(ScanPage {
2671        entries,
2672        next_start,
2673    })
2674}
2675
2676fn strip_cf_prefix_page(page: ScanPage) -> Result<ScanPage> {
2677    let entries = strip_cf_prefix_entries(page.entries)?;
2678    let next_start = page
2679        .next_start
2680        .map(|k| strip_cf_prefix_key(&k))
2681        .transpose()?;
2682    Ok(ScanPage {
2683        entries,
2684        next_start,
2685    })
2686}
2687
2688/// One ordered operation in a [`WriteBatch`].
2689#[derive(Debug)]
2690pub(crate) enum WriteBatchOp {
2691    /// Write a value for `key`.
2692    Put { key: Vec<u8>, value: Vec<u8> },
2693    /// Delete the point value for `key`.
2694    Delete { key: Vec<u8> },
2695    /// Delete every key in `[start, end)`.
2696    DeleteRange { start: Vec<u8>, end: Vec<u8> },
2697    /// Add one merge operand for `key`.
2698    Merge { key: Vec<u8>, operand: Vec<u8> },
2699}
2700
2701impl WriteBatchOp {
2702    /// Heap bytes this op holds. Drives the streaming writer's flush
2703    /// budget, so it counts what is buffered now, not what the op will
2704    /// encode to later.
2705    pub(crate) fn buffered_bytes(&self) -> usize {
2706        match self {
2707            Self::Put { key, value } => key.len() + value.len(),
2708            Self::Delete { key } => key.len(),
2709            Self::DeleteRange { start, end } => start.len() + end.len(),
2710            Self::Merge { key, operand } => key.len() + operand.len(),
2711        }
2712    }
2713}
2714
2715/// A batch of write operations to apply atomically.
2716#[derive(Debug, Default)]
2717pub struct WriteBatch {
2718    ops: Vec<WriteBatchOp>,
2719}
2720
2721impl WriteBatch {
2722    /// Create an empty write batch.
2723    pub fn new() -> Self {
2724        Self::default()
2725    }
2726
2727    /// Add a put operation to the batch (default column family).
2728    pub fn put(&mut self, key: &[u8], value: &[u8]) {
2729        self.put_owned(key, value.to_vec());
2730    }
2731
2732    /// [`WriteBatch::put`] for a value the caller already owns.
2733    ///
2734    /// Takes the buffer instead of copying it, which is what a producer
2735    /// that just built the bytes wants. The key is still copied because
2736    /// it is rewritten with a column-family prefix, so there is nothing
2737    /// to hand over.
2738    pub fn put_owned(&mut self, key: &[u8], value: Vec<u8>) {
2739        self.ops.push(WriteBatchOp::Put {
2740            key: prefix_key(DEFAULT_CF_ID, key),
2741            value,
2742        });
2743    }
2744
2745    /// Bytes this batch is holding: the key and value of every buffered
2746    /// operation. What a caller bounds when it decides to flush.
2747    pub fn buffered_bytes(&self) -> usize {
2748        self.ops.iter().map(WriteBatchOp::buffered_bytes).sum()
2749    }
2750
2751    /// Add a delete operation to the batch (default column family).
2752    pub fn delete(&mut self, key: &[u8]) {
2753        self.ops.push(WriteBatchOp::Delete {
2754            key: prefix_key(DEFAULT_CF_ID, key),
2755        });
2756    }
2757
2758    /// Delete every key in the half-open range `[start, end)` in
2759    /// the default column family.
2760    ///
2761    /// When the batch is applied, the range delete is ordered with
2762    /// the other batch operations, so later puts inside the range
2763    /// remain live while earlier puts are shadowed. Calls with
2764    /// `start >= end` are ignored.
2765    pub fn delete_range(&mut self, start: &[u8], end: &[u8]) {
2766        if start >= end {
2767            return;
2768        }
2769        self.ops.push(WriteBatchOp::DeleteRange {
2770            start: prefix_key(DEFAULT_CF_ID, start),
2771            end: prefix_key(DEFAULT_CF_ID, end),
2772        });
2773    }
2774
2775    /// Add a merge operand for `key` in the default column family.
2776    /// Requires the database to be configured with a
2777    /// [`MergeOperator`]; the operand is layered on top of any
2778    /// existing value or merge chain and collapsed at read time.
2779    /// Multiple merges on the same key in a single batch are
2780    /// allowed and applied in insertion order.
2781    pub fn merge(&mut self, key: &[u8], operand: &[u8]) {
2782        self.ops.push(WriteBatchOp::Merge {
2783            key: prefix_key(DEFAULT_CF_ID, key),
2784            operand: operand.to_vec(),
2785        });
2786    }
2787
2788    /// Add a put scoped to column family `cf`.
2789    pub fn put_cf(&mut self, cf: &ColumnFamilyHandle, key: &[u8], value: &[u8]) {
2790        self.ops.push(WriteBatchOp::Put {
2791            key: prefix_key(cf.id(), key),
2792            value: value.to_vec(),
2793        });
2794    }
2795
2796    /// Add a delete scoped to column family `cf`.
2797    pub fn delete_cf(&mut self, cf: &ColumnFamilyHandle, key: &[u8]) {
2798        self.ops.push(WriteBatchOp::Delete {
2799            key: prefix_key(cf.id(), key),
2800        });
2801    }
2802
2803    /// Add a range delete scoped to column family `cf`.
2804    pub fn delete_range_cf(&mut self, cf: &ColumnFamilyHandle, start: &[u8], end: &[u8]) {
2805        if start >= end {
2806            return;
2807        }
2808        self.ops.push(WriteBatchOp::DeleteRange {
2809            start: prefix_key(cf.id(), start),
2810            end: prefix_key(cf.id(), end),
2811        });
2812    }
2813
2814    /// Add a merge operand scoped to column family `cf`.
2815    pub fn merge_cf(&mut self, cf: &ColumnFamilyHandle, key: &[u8], operand: &[u8]) {
2816        self.ops.push(WriteBatchOp::Merge {
2817            key: prefix_key(cf.id(), key),
2818            operand: operand.to_vec(),
2819        });
2820    }
2821
2822    /// Insert an already-prefixed put (internal use by wrappers
2823    /// like `DbWithTtl` that iterate a source batch's raw entries
2824    /// and rebuild a new batch without re-applying the CF prefix).
2825    pub(crate) fn insert_raw_put(&mut self, prefixed_key: Vec<u8>, value: Vec<u8>) {
2826        self.ops.push(WriteBatchOp::Put {
2827            key: prefixed_key,
2828            value,
2829        });
2830    }
2831
2832    /// Insert an already-prefixed delete.
2833    pub(crate) fn insert_raw_delete(&mut self, prefixed_key: Vec<u8>) {
2834        self.ops.push(WriteBatchOp::Delete { key: prefixed_key });
2835    }
2836
2837    /// Insert an already-prefixed range delete.
2838    pub(crate) fn insert_raw_range_delete(
2839        &mut self,
2840        prefixed_start: Vec<u8>,
2841        prefixed_end: Vec<u8>,
2842    ) {
2843        self.ops.push(WriteBatchOp::DeleteRange {
2844            start: prefixed_start,
2845            end: prefixed_end,
2846        });
2847    }
2848
2849    /// Insert an already-prefixed merge operand.
2850    pub(crate) fn insert_raw_merge(&mut self, prefixed_key: Vec<u8>, operand: Vec<u8>) {
2851        self.ops.push(WriteBatchOp::Merge {
2852            key: prefixed_key,
2853            operand,
2854        });
2855    }
2856
2857    /// Number of point operations in the batch. Repeated operations
2858    /// on the same key are counted separately. Range deletes and
2859    /// merges are counted separately via
2860    /// [`WriteBatch::range_delete_count`] and
2861    /// [`WriteBatch::merge_count`].
2862    pub fn len(&self) -> usize {
2863        self.ops
2864            .iter()
2865            .filter(|op| matches!(op, WriteBatchOp::Put { .. } | WriteBatchOp::Delete { .. }))
2866            .count()
2867    }
2868
2869    /// Number of range-delete operations in the batch.
2870    pub fn range_delete_count(&self) -> usize {
2871        self.ops
2872            .iter()
2873            .filter(|op| matches!(op, WriteBatchOp::DeleteRange { .. }))
2874            .count()
2875    }
2876
2877    /// Number of merge operations in the batch.
2878    pub fn merge_count(&self) -> usize {
2879        self.ops
2880            .iter()
2881            .filter(|op| matches!(op, WriteBatchOp::Merge { .. }))
2882            .count()
2883    }
2884
2885    /// Whether the batch contains no operations of any kind.
2886    pub fn is_empty(&self) -> bool {
2887        self.ops.is_empty()
2888    }
2889}
2890
2891#[cfg(test)]
2892mod tests {
2893    use super::*;
2894    use std::path::PathBuf;
2895    use tempfile::TempDir;
2896
2897    fn open_tmp() -> (Db, TempDir) {
2898        let dir = TempDir::new().unwrap();
2899        let db = Db::open(dir.path(), Options::default()).unwrap();
2900        (db, dir)
2901    }
2902
2903    fn first_wal_path(dir: &TempDir) -> PathBuf {
2904        let mut entries: Vec<_> = std::fs::read_dir(dir.path().join("wal"))
2905            .unwrap()
2906            .filter_map(|entry| entry.ok())
2907            .filter(|entry| entry.path().extension().and_then(|ext| ext.to_str()) == Some("log"))
2908            .collect();
2909        entries.sort_by_key(|entry| entry.path());
2910        entries.into_iter().next().unwrap().path()
2911    }
2912
2913    #[test]
2914    fn test_db_open_rejects_second_writer_on_same_directory() {
2915        let dir = TempDir::new().unwrap();
2916        let db = Db::open(dir.path(), Options::default()).unwrap();
2917
2918        let err = Db::open(dir.path(), Options::default()).unwrap_err();
2919        match err {
2920            Error::Io(io) => {
2921                assert_eq!(io.kind(), std::io::ErrorKind::AlreadyExists);
2922                assert!(io.to_string().contains("already locked"));
2923            }
2924            other => panic!("expected lock I/O error, got {other:?}"),
2925        }
2926
2927        db.put(b"k", b"v").unwrap();
2928        drop(db);
2929
2930        let reopened = Db::open(dir.path(), Options::default()).unwrap();
2931        assert_eq!(reopened.get(b"k").unwrap(), Some(b"v".to_vec()));
2932    }
2933
2934    #[test]
2935    fn test_db_open_rejects_invalid_options() {
2936        let dir = TempDir::new().unwrap();
2937        let err = Db::open(
2938            dir.path(),
2939            Options {
2940                write_buffer_size: 0,
2941                ..Options::default()
2942            },
2943        )
2944        .unwrap_err();
2945        match err {
2946            Error::InvalidArgument(message) => assert!(message.contains("write_buffer_size")),
2947            other => panic!("expected invalid argument, got {other:?}"),
2948        }
2949    }
2950
2951    #[test]
2952    fn test_open_read_only_replays_wal_without_mutating_files() {
2953        let dir = TempDir::new().unwrap();
2954        {
2955            let opts = Options {
2956                durability: DurabilityMode::Immediate,
2957                ..Options::default()
2958            };
2959            let db = Db::open(dir.path(), opts).unwrap();
2960            db.put(b"wal_only", b"value").unwrap();
2961        }
2962
2963        let manifest_len = std::fs::metadata(dir.path().join("MANIFEST"))
2964            .unwrap()
2965            .len();
2966        let mut wal_names_before: Vec<_> = std::fs::read_dir(dir.path().join("wal"))
2967            .unwrap()
2968            .map(|entry| entry.unwrap().file_name())
2969            .collect();
2970        wal_names_before.sort();
2971
2972        let ro = Db::open_read_only(dir.path(), Options::default()).unwrap();
2973        assert_eq!(ro.get(b"wal_only").unwrap(), Some(b"value".to_vec()));
2974
2975        let err = ro.put(b"blocked", b"write").unwrap_err();
2976        match err {
2977            Error::ReadOnly => {}
2978            other => panic!("expected read-only error, got {other:?}"),
2979        }
2980
2981        let writer_err = Db::open(dir.path(), Options::default()).unwrap_err();
2982        match writer_err {
2983            Error::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::AlreadyExists),
2984            other => panic!("expected lock conflict, got {other:?}"),
2985        }
2986        ro.close().unwrap();
2987        drop(ro);
2988
2989        assert_eq!(
2990            std::fs::metadata(dir.path().join("MANIFEST"))
2991                .unwrap()
2992                .len(),
2993            manifest_len
2994        );
2995        let mut wal_names_after: Vec<_> = std::fs::read_dir(dir.path().join("wal"))
2996            .unwrap()
2997            .map(|entry| entry.unwrap().file_name())
2998            .collect();
2999        wal_names_after.sort();
3000        assert_eq!(wal_names_after, wal_names_before);
3001    }
3002
3003    #[test]
3004    fn test_open_read_only_missing_db_errors() {
3005        let dir = TempDir::new().unwrap();
3006        let err = Db::open_read_only(dir.path(), Options::default()).unwrap_err();
3007        match err {
3008            Error::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::NotFound),
3009            other => panic!("expected missing DB error, got {other:?}"),
3010        }
3011    }
3012
3013    #[test]
3014    fn test_close_transitions_handle_to_closed_state() {
3015        let (db, _dir) = open_tmp();
3016        db.put(b"k", b"v").unwrap();
3017        let snap = db.snapshot();
3018
3019        db.close().unwrap();
3020        db.close().unwrap();
3021
3022        match db.get(b"k").unwrap_err() {
3023            Error::Closed => {}
3024            other => panic!("expected closed error from get, got {other:?}"),
3025        }
3026        match db.put(b"after", b"close").unwrap_err() {
3027            Error::Closed => {}
3028            other => panic!("expected closed error from put, got {other:?}"),
3029        }
3030        match db.write(WriteBatch::new()).unwrap_err() {
3031            Error::Closed => {}
3032            other => panic!("expected closed error from empty write, got {other:?}"),
3033        }
3034        match db.delete_range(b"z", b"a").unwrap_err() {
3035            Error::Closed => {}
3036            other => panic!("expected closed error from no-op range delete, got {other:?}"),
3037        }
3038        let default_cf = db.default_cf();
3039        match db.delete_range_cf(&default_cf, b"z", b"a").unwrap_err() {
3040            Error::Closed => {}
3041            other => panic!("expected closed error from no-op cf range delete, got {other:?}"),
3042        }
3043        match snap.get(b"k").unwrap_err() {
3044            Error::Closed => {}
3045            other => panic!("expected closed error from snapshot get, got {other:?}"),
3046        }
3047
3048        let mut iter = db.iter();
3049        iter.seek_to_first();
3050        match iter.status().unwrap_err() {
3051            Error::Closed => {}
3052            other => panic!("expected closed error from iterator, got {other:?}"),
3053        }
3054    }
3055
3056    #[test]
3057    fn test_failed_close_can_be_retried() {
3058        let dir = TempDir::new().unwrap();
3059        let db = Db::open(dir.path(), Options::default()).unwrap();
3060        db.put(b"k", b"v").unwrap();
3061
3062        let blocked_wal_path = dir.path().join("wal").join("wal_000002.log");
3063        std::fs::create_dir(&blocked_wal_path).unwrap();
3064
3065        match db.close().unwrap_err() {
3066            Error::Io(_) => {}
3067            other => panic!("expected close I/O error, got {other:?}"),
3068        }
3069
3070        assert_eq!(db.get(b"k").unwrap(), Some(b"v".to_vec()));
3071
3072        std::fs::remove_dir(&blocked_wal_path).unwrap();
3073        db.close().unwrap();
3074        db.close().unwrap();
3075
3076        let sst_count = std::fs::read_dir(dir.path().join("sst"))
3077            .unwrap()
3078            .filter_map(|entry| entry.ok())
3079            .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "sst"))
3080            .count();
3081        assert!(sst_count > 0);
3082
3083        drop(db);
3084        let reopened = Db::open(dir.path(), Options::default()).unwrap();
3085        assert_eq!(reopened.get(b"k").unwrap(), Some(b"v".to_vec()));
3086    }
3087
3088    #[test]
3089    fn test_configured_key_value_size_limits_are_enforced() {
3090        let dir = TempDir::new().unwrap();
3091        let opts = Options {
3092            max_key_size: 8,
3093            max_value_size: 4,
3094            ..Options::default()
3095        };
3096        let db = Db::open(dir.path(), opts).unwrap();
3097
3098        db.put(b"abc", b"1234").unwrap();
3099        assert!(db.put(b"toolongky", b"1").is_err());
3100        assert!(db.put(b"a", b"12345").is_err());
3101        assert!(db.delete(b"toolongky").is_err());
3102        assert!(db.merge(b"toolongky", b"1").is_err());
3103        assert!(db.merge(b"a", b"12345").is_err());
3104
3105        let mut batch = WriteBatch::new();
3106        batch.put(b"ok", b"1");
3107        batch.put(b"toolong", b"2");
3108        db.write(batch).unwrap();
3109        assert_eq!(db.get(b"ok").unwrap(), Some(b"1".to_vec()));
3110
3111        let mut batch = WriteBatch::new();
3112        batch.put(b"ok2", b"1");
3113        batch.put(b"toolongky", b"2");
3114        assert!(db.write(batch).is_err());
3115        assert_eq!(db.get(b"ok2").unwrap(), None);
3116
3117        let cf = db.create_column_family("cf").unwrap();
3118        assert!(db.put_cf(&cf, b"toolongky", b"1").is_err());
3119    }
3120
3121    /// Options that force flushes early so tests can exercise the SSTable path.
3122    fn tiny_flush_opts() -> Options {
3123        Options {
3124            write_buffer_size: 4 * 1024,
3125            ..Options::default()
3126        }
3127    }
3128
3129    /// Write enough filler bytes to push the active memtable past
3130    /// `write_buffer_size`, forcing a flush to L0.
3131    fn force_flush(db: &Db, tag: &str) {
3132        let payload = vec![0u8; 512];
3133        for i in 0..32 {
3134            let key = format!("__flush_{}_{:04}", tag, i);
3135            db.put(key.as_bytes(), &payload).unwrap();
3136        }
3137    }
3138
3139    fn force_flush_with_prefix(db: &Db, prefix: &str) {
3140        let payload = vec![0u8; 512];
3141        for i in 0..32 {
3142            let key = format!("{prefix}_{i:04}");
3143            db.put(key.as_bytes(), &payload).unwrap();
3144        }
3145    }
3146
3147    #[test]
3148    fn test_basic_crud() {
3149        let (db, _dir) = open_tmp();
3150
3151        db.put(b"key1", b"value1").unwrap();
3152        assert_eq!(db.get(b"key1").unwrap(), Some(b"value1".to_vec()));
3153
3154        db.put(b"key1", b"value2").unwrap();
3155        assert_eq!(db.get(b"key1").unwrap(), Some(b"value2".to_vec()));
3156
3157        db.delete(b"key1").unwrap();
3158        assert_eq!(db.get(b"key1").unwrap(), None);
3159
3160        assert_eq!(db.get(b"nonexistent").unwrap(), None);
3161    }
3162
3163    #[test]
3164    fn test_write_batch() {
3165        let (db, _dir) = open_tmp();
3166
3167        let mut batch = WriteBatch::new();
3168        batch.put(b"a", b"1");
3169        batch.put(b"b", b"2");
3170        batch.put(b"c", b"3");
3171        db.write(batch).unwrap();
3172
3173        assert_eq!(db.get(b"a").unwrap(), Some(b"1".to_vec()));
3174        assert_eq!(db.get(b"b").unwrap(), Some(b"2".to_vec()));
3175        assert_eq!(db.get(b"c").unwrap(), Some(b"3".to_vec()));
3176    }
3177
3178    #[test]
3179    fn test_write_batch_uses_single_wal_batch_record() {
3180        let (db, dir) = open_tmp();
3181
3182        let mut batch = WriteBatch::new();
3183        batch.put(b"a", b"1");
3184        batch.put(b"b", b"2");
3185        db.write(batch).unwrap();
3186
3187        // Records begin after the file stamp; the type byte is the fifth
3188        // byte of the first record.
3189        let stamp = crate::engine::wal::WAL_STAMP_LEN;
3190        let wal = std::fs::read(first_wal_path(&dir)).unwrap();
3191        assert!(wal.len() >= stamp + 5);
3192        assert_eq!(&wal[0..4], b"REGO", "the log must carry its stamp");
3193        assert_eq!(
3194            wal[stamp + 4],
3195            0x05,
3196            "multi-op WriteBatch must use RECORD_BATCH"
3197        );
3198    }
3199
3200    #[test]
3201    fn test_snapshot_isolation() {
3202        let (db, _dir) = open_tmp();
3203
3204        db.put(b"key", b"v1").unwrap();
3205        let snap = db.snapshot();
3206
3207        db.put(b"key", b"v2").unwrap();
3208
3209        assert_eq!(snap.get(b"key").unwrap(), Some(b"v1".to_vec()));
3210        assert_eq!(db.get(b"key").unwrap(), Some(b"v2".to_vec()));
3211    }
3212
3213    #[test]
3214    fn test_scan() {
3215        let (db, _dir) = open_tmp();
3216
3217        db.put(b"a", b"1").unwrap();
3218        db.put(b"b", b"2").unwrap();
3219        db.put(b"c", b"3").unwrap();
3220        db.put(b"d", b"4").unwrap();
3221
3222        let results = db.scan(Some(b"b"), Some(b"d")).unwrap();
3223        assert_eq!(results.len(), 2);
3224        assert_eq!(results[0], (b"b".to_vec(), b"2".to_vec()));
3225        assert_eq!(results[1], (b"c".to_vec(), b"3".to_vec()));
3226    }
3227
3228    #[test]
3229    fn test_scan_page_limits_and_resumes() {
3230        let (db, _dir) = open_tmp();
3231
3232        for c in b'a'..=b'e' {
3233            db.put(&[c], &[c]).unwrap();
3234        }
3235
3236        let page = db.scan_page(Some(b"b"), Some(b"e"), 2).unwrap();
3237        assert_eq!(
3238            page,
3239            ScanPage {
3240                entries: vec![
3241                    (b"b".to_vec(), b"b".to_vec()),
3242                    (b"c".to_vec(), b"c".to_vec()),
3243                ],
3244                next_start: Some(b"d".to_vec()),
3245            }
3246        );
3247
3248        let next = db
3249            .scan_page(page.next_start.as_deref(), Some(b"e"), 2)
3250            .unwrap();
3251        assert_eq!(
3252            next,
3253            ScanPage {
3254                entries: vec![(b"d".to_vec(), b"d".to_vec())],
3255                next_start: None,
3256            }
3257        );
3258
3259        let exact_end = db.scan_page(Some(b"b"), Some(b"d"), 2).unwrap();
3260        assert_eq!(
3261            exact_end,
3262            ScanPage {
3263                entries: vec![
3264                    (b"b".to_vec(), b"b".to_vec()),
3265                    (b"c".to_vec(), b"c".to_vec()),
3266                ],
3267                next_start: None,
3268            }
3269        );
3270    }
3271
3272    #[test]
3273    fn test_scan_page_rejects_zero_limit() {
3274        let (db, _dir) = open_tmp();
3275
3276        let err = db.scan_page(None, None, 0).unwrap_err();
3277        match err {
3278            Error::InvalidArgument(message) => assert!(message.contains("greater than zero")),
3279            other => panic!("expected invalid argument error, got {other:?}"),
3280        }
3281    }
3282
3283    #[test]
3284    fn test_strip_cf_prefix_page_rejects_short_internal_key() {
3285        let page = ScanPage {
3286            entries: vec![(vec![0, 1, 2], b"value".to_vec())],
3287            next_start: None,
3288        };
3289
3290        match strip_cf_prefix_page(page).unwrap_err() {
3291            Error::Corruption(source) => assert_eq!(source.kind(), std::io::ErrorKind::InvalidData),
3292            other => panic!("expected corruption error, got {other:?}"),
3293        }
3294    }
3295
3296    #[test]
3297    fn test_snapshot_scan_page_is_stable() {
3298        let (db, _dir) = open_tmp();
3299
3300        db.put(b"a", b"1").unwrap();
3301        db.put(b"b", b"2").unwrap();
3302        let snap = db.snapshot();
3303
3304        db.put(b"a", b"new").unwrap();
3305        db.delete(b"b").unwrap();
3306        db.put(b"c", b"3").unwrap();
3307
3308        let first = snap.scan_page(None, None, 1).unwrap();
3309        assert_eq!(
3310            first,
3311            ScanPage {
3312                entries: vec![(b"a".to_vec(), b"1".to_vec())],
3313                next_start: Some(b"b".to_vec()),
3314            }
3315        );
3316
3317        let second = snap
3318            .scan_page(first.next_start.as_deref(), None, 2)
3319            .unwrap();
3320        assert_eq!(
3321            second,
3322            ScanPage {
3323                entries: vec![(b"b".to_vec(), b"2".to_vec())],
3324                next_start: None,
3325            }
3326        );
3327    }
3328
3329    #[test]
3330    fn test_drop_all() {
3331        let (db, _dir) = open_tmp();
3332
3333        db.put(b"key1", b"val1").unwrap();
3334        db.put(b"key2", b"val2").unwrap();
3335        db.drop_all().unwrap();
3336
3337        assert_eq!(db.get(b"key1").unwrap(), None);
3338        assert_eq!(db.get(b"key2").unwrap(), None);
3339
3340        db.put(b"key3", b"val3").unwrap();
3341        assert_eq!(db.get(b"key3").unwrap(), Some(b"val3".to_vec()));
3342    }
3343
3344    #[test]
3345    fn test_drop_all_reopen_ignores_leftover_old_wal() {
3346        let dir = TempDir::new().unwrap();
3347        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3348
3349        db.put(b"flushed", b"old").unwrap();
3350        force_flush(&db, "drop-all-wal-floor");
3351        db.put(b"unflushed", b"old").unwrap();
3352        db.drop_all().unwrap();
3353        drop(db);
3354
3355        let stale_wal_path = dir.path().join("wal").join("wal_000001.log");
3356        let mut stale_wal = engine::wal::Wal::create(&stale_wal_path).unwrap();
3357        stale_wal
3358            .append_put(&prefix_key(DEFAULT_CF_ID, b"resurrect"), b"bad", 99)
3359            .unwrap();
3360        stale_wal.sync_data().unwrap();
3361
3362        let db = Db::open(dir.path(), Options::default()).unwrap();
3363        assert_eq!(db.get(b"flushed").unwrap(), None);
3364        assert_eq!(db.get(b"unflushed").unwrap(), None);
3365        assert_eq!(db.get(b"resurrect").unwrap(), None);
3366        assert!(db.scan(None, None).unwrap().is_empty());
3367    }
3368
3369    #[test]
3370    fn test_snapshot_isolation_across_flush() {
3371        let dir = TempDir::new().unwrap();
3372        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3373
3374        db.put(b"key", b"v1").unwrap();
3375        let snap = db.snapshot();
3376
3377        db.put(b"key", b"v2").unwrap();
3378        force_flush(&db, "snap");
3379
3380        assert_eq!(snap.get(b"key").unwrap(), Some(b"v1".to_vec()));
3381        assert_eq!(db.get(b"key").unwrap(), Some(b"v2".to_vec()));
3382    }
3383
3384    #[test]
3385    fn test_delete_persists_across_flush() {
3386        let dir = TempDir::new().unwrap();
3387        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3388
3389        db.put(b"key", b"v1").unwrap();
3390        force_flush(&db, "a");
3391
3392        db.delete(b"key").unwrap();
3393        force_flush(&db, "b");
3394
3395        assert_eq!(db.get(b"key").unwrap(), None);
3396    }
3397
3398    #[test]
3399    fn test_crash_recovery_without_close() {
3400        let dir = TempDir::new().unwrap();
3401
3402        {
3403            let db = Db::open(dir.path(), Options::default()).unwrap();
3404            db.put(b"a", b"1").unwrap();
3405            db.put(b"b", b"2").unwrap();
3406            db.delete(b"a").unwrap();
3407            db.put(b"c", b"3").unwrap();
3408            // Drop without close() - simulates a crash; the WAL must be replayed.
3409        }
3410
3411        let db = Db::open(dir.path(), Options::default()).unwrap();
3412        assert_eq!(db.get(b"a").unwrap(), None);
3413        assert_eq!(db.get(b"b").unwrap(), Some(b"2".to_vec()));
3414        assert_eq!(db.get(b"c").unwrap(), Some(b"3".to_vec()));
3415    }
3416
3417    #[test]
3418    fn test_recovered_wal_survives_second_crash_before_flush() {
3419        let dir = TempDir::new().unwrap();
3420
3421        {
3422            let db = Db::open(dir.path(), Options::default()).unwrap();
3423            db.put(b"a", b"1").unwrap();
3424            db.put(b"b", b"2").unwrap();
3425            db.put(b"d", b"4").unwrap();
3426            db.delete(b"a").unwrap();
3427            db.delete_range(b"d", b"f").unwrap();
3428            // Drop without close so the first reopen must recover from WAL.
3429        }
3430
3431        {
3432            let db = Db::open(dir.path(), Options::default()).unwrap();
3433            assert_eq!(db.get(b"a").unwrap(), None);
3434            assert_eq!(db.get(b"b").unwrap(), Some(b"2".to_vec()));
3435            assert_eq!(db.get(b"d").unwrap(), None);
3436            // Drop again before the recovered memtable can flush. The
3437            // recovered state must have been rewritten to the active WAL.
3438        }
3439
3440        let db = Db::open(dir.path(), Options::default()).unwrap();
3441        assert_eq!(db.get(b"a").unwrap(), None);
3442        assert_eq!(db.get(b"b").unwrap(), Some(b"2".to_vec()));
3443        assert_eq!(db.get(b"d").unwrap(), None);
3444    }
3445
3446    // ─── Streaming iterator tests ────────────────────────────────────────
3447
3448    fn collect_iter(db: &Db) -> Vec<(Vec<u8>, Vec<u8>)> {
3449        let mut it = db.iter();
3450        it.seek_to_first();
3451        let mut out = Vec::new();
3452        while it.valid() {
3453            out.push((it.key().unwrap().to_vec(), it.value().unwrap().to_vec()));
3454            it.next();
3455        }
3456        it.status().unwrap();
3457        out
3458    }
3459
3460    #[test]
3461    fn test_iter_empty_db() {
3462        let (db, _dir) = open_tmp();
3463        let mut it = db.iter();
3464        it.seek_to_first();
3465        assert!(!it.valid());
3466        it.seek(b"anything");
3467        assert!(!it.valid());
3468        assert!(it.status().is_ok());
3469    }
3470
3471    #[test]
3472    fn test_iter_basic_forward() {
3473        let (db, _dir) = open_tmp();
3474        for i in 0..10 {
3475            let k = format!("k{:02}", i);
3476            let v = format!("v{}", i);
3477            db.put(k.as_bytes(), v.as_bytes()).unwrap();
3478        }
3479        let items = collect_iter(&db);
3480        assert_eq!(items.len(), 10);
3481        for (i, (k, v)) in items.iter().enumerate() {
3482            assert_eq!(k, format!("k{:02}", i).as_bytes());
3483            assert_eq!(v, format!("v{}", i).as_bytes());
3484        }
3485    }
3486
3487    #[test]
3488    fn test_owned_snapshot_iter_streams_from_snapshot() {
3489        let (db, _dir) = open_tmp();
3490        db.put(b"a", b"1").unwrap();
3491        db.put(b"b", b"2").unwrap();
3492        let snapshot = db.snapshot();
3493        db.put(b"c", b"3").unwrap();
3494
3495        let mut it = snapshot.owned_iter();
3496        assert_eq!(snapshot.get(b"b").unwrap(), Some(b"2".to_vec()));
3497
3498        it.seek_to_first();
3499        assert!(it.valid());
3500        assert_eq!(it.key(), Some(b"a".as_ref()));
3501        assert_eq!(it.value(), Some(b"1".as_ref()));
3502        it.next();
3503        assert_eq!(it.key(), Some(b"b".as_ref()));
3504        it.next();
3505        assert!(!it.valid());
3506
3507        it.seek_to_last();
3508        assert_eq!(it.key(), Some(b"b".as_ref()));
3509        it.prev();
3510        assert_eq!(it.key(), Some(b"a".as_ref()));
3511        it.status().unwrap();
3512    }
3513
3514    #[test]
3515    fn test_iter_seek_exact_and_between() {
3516        let (db, _dir) = open_tmp();
3517        db.put(b"a", b"1").unwrap();
3518        db.put(b"c", b"3").unwrap();
3519        db.put(b"e", b"5").unwrap();
3520
3521        let mut it = db.iter();
3522
3523        it.seek(b"a");
3524        assert!(it.valid());
3525        assert_eq!(it.key(), Some(b"a".as_ref()));
3526
3527        it.seek(b"b");
3528        assert_eq!(it.key(), Some(b"c".as_ref()));
3529
3530        it.seek(b"c");
3531        assert_eq!(it.key(), Some(b"c".as_ref()));
3532
3533        it.seek(b"f");
3534        assert!(!it.valid());
3535    }
3536
3537    #[test]
3538    fn test_iter_seek_for_prev() {
3539        let (db, _dir) = open_tmp();
3540        db.put(b"a", b"1").unwrap();
3541        db.put(b"c", b"3").unwrap();
3542        db.put(b"e", b"5").unwrap();
3543
3544        let mut it = db.iter();
3545
3546        it.seek_for_prev(b"e");
3547        assert_eq!(it.key(), Some(b"e".as_ref()));
3548
3549        it.seek_for_prev(b"d");
3550        assert_eq!(it.key(), Some(b"c".as_ref()));
3551
3552        it.seek_for_prev(b"a");
3553        assert_eq!(it.key(), Some(b"a".as_ref()));
3554
3555        it.seek_for_prev(b"0");
3556        assert!(!it.valid());
3557    }
3558
3559    #[test]
3560    fn test_iter_continues_after_seek() {
3561        let (db, _dir) = open_tmp();
3562        for c in b'a'..=b'j' {
3563            db.put(&[c], &[c]).unwrap();
3564        }
3565
3566        let mut it = db.iter();
3567        it.seek(b"d");
3568        let mut keys = Vec::new();
3569        while it.valid() {
3570            keys.push(it.key().unwrap().to_vec());
3571            it.next();
3572        }
3573        assert_eq!(
3574            keys,
3575            vec![
3576                b"d".to_vec(),
3577                b"e".to_vec(),
3578                b"f".to_vec(),
3579                b"g".to_vec(),
3580                b"h".to_vec(),
3581                b"i".to_vec(),
3582                b"j".to_vec(),
3583            ]
3584        );
3585    }
3586
3587    #[test]
3588    fn test_iter_across_memtable_and_l0() {
3589        let dir = TempDir::new().unwrap();
3590        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3591
3592        for i in 0..10 {
3593            let k = format!("old{:02}", i);
3594            db.put(k.as_bytes(), b"old").unwrap();
3595        }
3596        force_flush(&db, "to-l0");
3597
3598        for i in 0..5 {
3599            let k = format!("new{:02}", i);
3600            db.put(k.as_bytes(), b"new").unwrap();
3601        }
3602
3603        let items = collect_iter(&db);
3604        let olds = items.iter().filter(|(k, _)| k.starts_with(b"old")).count();
3605        let news = items.iter().filter(|(k, _)| k.starts_with(b"new")).count();
3606        assert_eq!(olds, 10);
3607        assert_eq!(news, 5);
3608
3609        let sorted: Vec<_> = items.iter().map(|(k, _)| k.clone()).collect();
3610        let mut expected = sorted.clone();
3611        expected.sort();
3612        assert_eq!(sorted, expected);
3613    }
3614
3615    #[test]
3616    fn test_iter_tombstone_hides_older_level_entry() {
3617        let dir = TempDir::new().unwrap();
3618        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3619
3620        db.put(b"kept", b"v1").unwrap();
3621        db.put(b"gone", b"v1").unwrap();
3622        force_flush(&db, "a");
3623
3624        db.delete(b"gone").unwrap();
3625
3626        let items = collect_iter(&db);
3627        let keys: Vec<_> = items.iter().map(|(k, _)| k.clone()).collect();
3628        assert!(keys.contains(&b"kept".to_vec()));
3629        assert!(!keys.contains(&b"gone".to_vec()));
3630    }
3631
3632    #[test]
3633    fn test_iter_latest_version_wins_across_levels() {
3634        let dir = TempDir::new().unwrap();
3635        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3636
3637        db.put(b"k", b"v1").unwrap();
3638        force_flush(&db, "a");
3639        db.put(b"k", b"v2").unwrap();
3640
3641        let mut it = db.iter();
3642        it.seek(b"k");
3643        assert_eq!(it.key(), Some(b"k".as_ref()));
3644        assert_eq!(it.value(), Some(b"v2".as_ref()));
3645    }
3646
3647    #[test]
3648    fn test_iter_honors_snapshot_isolation() {
3649        let (db, _dir) = open_tmp();
3650        db.put(b"k", b"v1").unwrap();
3651        let snap = db.snapshot();
3652        db.put(b"k", b"v2").unwrap();
3653
3654        let mut it = snap.iter();
3655        it.seek(b"k");
3656        assert_eq!(it.value(), Some(b"v1".as_ref()));
3657    }
3658
3659    #[test]
3660    fn test_iter_snapshot_ignores_tombstone_newer_than_snap() {
3661        let (db, _dir) = open_tmp();
3662        db.put(b"k", b"v1").unwrap();
3663        let snap = db.snapshot();
3664        db.delete(b"k").unwrap();
3665
3666        let mut it = snap.iter();
3667        it.seek(b"k");
3668        assert_eq!(it.value(), Some(b"v1".as_ref()));
3669    }
3670
3671    #[test]
3672    fn test_iter_consistency_with_scan() {
3673        let (db, _dir) = open_tmp();
3674        for i in 0..100 {
3675            let k = format!("k{:03}", i);
3676            let v = format!("v{}", i);
3677            db.put(k.as_bytes(), v.as_bytes()).unwrap();
3678        }
3679
3680        let scan = db.scan(Some(b"k020"), Some(b"k050")).unwrap();
3681
3682        let mut it = db.iter();
3683        it.seek(b"k020");
3684        let mut from_iter = Vec::new();
3685        while it.valid() {
3686            let k = it.key().unwrap();
3687            if k >= b"k050".as_ref() {
3688                break;
3689            }
3690            from_iter.push((k.to_vec(), it.value().unwrap().to_vec()));
3691            it.next();
3692        }
3693
3694        assert_eq!(scan, from_iter);
3695    }
3696
3697    #[test]
3698    fn test_iter_large_scan_10k_keys_after_flush() {
3699        let dir = TempDir::new().unwrap();
3700        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3701
3702        const N: usize = 10_000;
3703        for i in 0..N {
3704            let k = format!("key_{:06}", i);
3705            db.put(k.as_bytes(), b"v").unwrap();
3706        }
3707
3708        let mut it = db.iter();
3709        it.seek(b"key_");
3710        let mut count = 0;
3711        while it.valid() {
3712            let k = it.key().unwrap();
3713            if !k.starts_with(b"key_") {
3714                it.next();
3715                continue;
3716            }
3717            count += 1;
3718            it.next();
3719        }
3720        assert_eq!(count, N);
3721    }
3722
3723    // ─── Snapshot-pinning GC tests ──────────────────────────────────────
3724
3725    /// Thin wrapper around the engine's test-only persisted-versions
3726    /// accessor. Returns `(seq, value_type)` for every copy of
3727    /// `user_key` currently sitting in an SSTable at any level.
3728    fn all_versions_of(db: &Db, user_key: &[u8]) -> Vec<(u64, u8)> {
3729        // The helper walks raw engine keys, so re-apply the
3730        // default-CF prefix before querying.
3731        let prefixed = prefix_key(DEFAULT_CF_ID, user_key);
3732        db.engine.all_persisted_versions_of(&prefixed).unwrap()
3733    }
3734
3735    #[test]
3736    fn test_gc_drops_old_versions_without_snapshot() {
3737        // With no live snapshot, compact_range(None, None) should
3738        // leave only the newest version of each user key on disk.
3739        let dir = TempDir::new().unwrap();
3740        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3741
3742        for v in 0..10 {
3743            db.put(b"k", format!("v{}", v).as_bytes()).unwrap();
3744        }
3745
3746        db.compact_range(None, None).unwrap();
3747
3748        let versions = all_versions_of(&db, b"k");
3749        assert_eq!(
3750            versions.len(),
3751            1,
3752            "expected a single surviving version, found {:?}",
3753            versions
3754        );
3755        assert_eq!(db.get(b"k").unwrap(), Some(b"v9".to_vec()));
3756    }
3757
3758    #[test]
3759    fn test_gc_preserves_versions_pinned_by_snapshot() {
3760        // Take a snapshot at seq 5, then write more versions. After
3761        // compaction the snapshot must still read its view, which
3762        // requires preserving the version it pinned.
3763        let dir = TempDir::new().unwrap();
3764        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3765
3766        db.put(b"k", b"v1").unwrap();
3767        db.put(b"k", b"v2").unwrap();
3768        db.put(b"k", b"v3").unwrap();
3769        let snap = db.snapshot();
3770        // `snap` now pins seq=3 - the snapshot sees v3.
3771
3772        for v in 4..10 {
3773            db.put(b"k", format!("v{}", v).as_bytes()).unwrap();
3774        }
3775
3776        db.compact_range(None, None).unwrap();
3777
3778        assert_eq!(snap.get(b"k").unwrap(), Some(b"v3".to_vec()));
3779        assert_eq!(db.get(b"k").unwrap(), Some(b"v9".to_vec()));
3780    }
3781
3782    #[test]
3783    fn test_gc_releases_pin_when_snapshot_drops() {
3784        // Pinning a snapshot and then dropping it should fully
3785        // release the horizon so the next compaction can collapse
3786        // the key to a single surviving version.
3787        let dir = TempDir::new().unwrap();
3788        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3789
3790        for v in 0..5 {
3791            db.put(b"k", format!("v{}", v).as_bytes()).unwrap();
3792        }
3793
3794        {
3795            let _snap = db.snapshot();
3796            assert_eq!(db.engine.oldest_live_seq(), 5);
3797        }
3798        // Pin released.
3799        assert_eq!(db.engine.oldest_live_seq(), u64::MAX);
3800
3801        for v in 5..10 {
3802            db.put(b"k", format!("v{}", v).as_bytes()).unwrap();
3803        }
3804
3805        db.compact_range(None, None).unwrap();
3806
3807        let versions = all_versions_of(&db, b"k");
3808        assert_eq!(versions.len(), 1);
3809        assert_eq!(db.get(b"k").unwrap(), Some(b"v9".to_vec()));
3810    }
3811
3812    #[test]
3813    fn test_gc_with_multiple_live_snapshots_uses_oldest() {
3814        // When two snapshots are live, the older one's seq is the
3815        // GC horizon. Every version newer than (or at) the older
3816        // snapshot's seq must be preserved so the newer snapshot
3817        // can still read its own view too.
3818        let dir = TempDir::new().unwrap();
3819        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3820
3821        db.put(b"k", b"v1").unwrap();
3822        db.put(b"k", b"v2").unwrap();
3823        let old_snap = db.snapshot(); // pins seq 2
3824        db.put(b"k", b"v3").unwrap();
3825        db.put(b"k", b"v4").unwrap();
3826        let new_snap = db.snapshot(); // pins seq 4
3827        db.put(b"k", b"v5").unwrap();
3828        db.put(b"k", b"v6").unwrap();
3829
3830        db.compact_range(None, None).unwrap();
3831
3832        // Both snapshots must still return their respective versions.
3833        assert_eq!(old_snap.get(b"k").unwrap(), Some(b"v2".to_vec()));
3834        assert_eq!(new_snap.get(b"k").unwrap(), Some(b"v4".to_vec()));
3835        assert_eq!(db.get(b"k").unwrap(), Some(b"v6".to_vec()));
3836    }
3837
3838    #[test]
3839    fn test_gc_preserves_tombstone_hiding_older_entries() {
3840        // A tombstone newer than any live snapshot still needs to
3841        // survive compaction - it's the newest version and reads
3842        // must resolve to "deleted".
3843        let dir = TempDir::new().unwrap();
3844        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3845
3846        for v in 0..5 {
3847            db.put(b"k", format!("v{}", v).as_bytes()).unwrap();
3848        }
3849        db.delete(b"k").unwrap();
3850
3851        db.compact_range(None, None).unwrap();
3852
3853        assert_eq!(db.get(b"k").unwrap(), None);
3854
3855        // The newest surviving version is a tombstone - look for it
3856        // on disk.
3857        let versions = all_versions_of(&db, b"k");
3858        assert!(!versions.is_empty());
3859        // Highest seq is the tombstone.
3860        let (_, vt) = *versions.iter().max_by_key(|(seq, _)| *seq).unwrap();
3861        const VALUE_TYPE_DELETION: u8 = 0;
3862        assert_eq!(vt, VALUE_TYPE_DELETION);
3863    }
3864
3865    #[test]
3866    fn test_range_tombstone_pruning_preserves_snapshot_visible_value() {
3867        let dir = TempDir::new().unwrap();
3868        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3869
3870        db.put(b"k", b"old").unwrap();
3871        let snap = db.snapshot();
3872        db.delete_range(b"a", b"z").unwrap();
3873
3874        db.compact_range(None, None).unwrap();
3875
3876        assert_eq!(snap.get(b"k").unwrap(), Some(b"old".to_vec()));
3877        assert_eq!(db.get(b"k").unwrap(), None);
3878    }
3879
3880    #[test]
3881    fn test_gc_across_many_user_keys() {
3882        // Stress the multi-group path: many distinct user keys each
3883        // with several versions. No snapshot is live so each key
3884        // should collapse to exactly one surviving version.
3885        let dir = TempDir::new().unwrap();
3886        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3887
3888        for i in 0..200 {
3889            for v in 0..3 {
3890                db.put(
3891                    format!("k{:03}", i).as_bytes(),
3892                    format!("v{}_{}", i, v).as_bytes(),
3893                )
3894                .unwrap();
3895            }
3896        }
3897
3898        db.compact_range(None, None).unwrap();
3899
3900        for i in 0..200 {
3901            let k = format!("k{:03}", i);
3902            let versions = all_versions_of(&db, k.as_bytes());
3903            assert_eq!(versions.len(), 1, "key {} survived with {:?}", k, versions);
3904            assert_eq!(
3905                db.get(k.as_bytes()).unwrap(),
3906                Some(format!("v{}_2", i).into_bytes())
3907            );
3908        }
3909    }
3910
3911    // ─── compact_range tests ────────────────────────────────────────────
3912
3913    fn level_file_count(db: &Db, level: usize) -> usize {
3914        db.engine.level_file_count(level)
3915    }
3916
3917    fn total_file_count(db: &Db) -> usize {
3918        db.engine.total_file_count()
3919    }
3920
3921    #[test]
3922    fn test_compact_range_empty_db() {
3923        let (db, _dir) = open_tmp();
3924        // No data, no files. compact_range is a no-op and must succeed.
3925        db.compact_range(None, None).unwrap();
3926        assert_eq!(total_file_count(&db), 0);
3927    }
3928
3929    #[test]
3930    fn test_compact_range_full_preserves_reads() {
3931        let dir = TempDir::new().unwrap();
3932        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3933
3934        for i in 0..500 {
3935            let k = format!("k{:04}", i);
3936            db.put(k.as_bytes(), format!("v{}", i).as_bytes()).unwrap();
3937        }
3938
3939        db.compact_range(None, None).unwrap();
3940
3941        // Every key is still readable after the compaction.
3942        for i in 0..500 {
3943            let k = format!("k{:04}", i);
3944            assert_eq!(
3945                db.get(k.as_bytes()).unwrap(),
3946                Some(format!("v{}", i).into_bytes())
3947            );
3948        }
3949    }
3950
3951    #[test]
3952    fn test_compact_range_flushes_active_memtable() {
3953        // Writes that are still in the memtable when compact_range is
3954        // called must be flushed to L0 before the walk, so the active
3955        // memtable is empty afterwards.
3956        let (db, _dir) = open_tmp();
3957        for i in 0..10 {
3958            let k = format!("m{:02}", i);
3959            db.put(k.as_bytes(), b"v").unwrap();
3960        }
3961        assert!(!db.engine.active_memtable_is_empty());
3962
3963        db.compact_range(None, None).unwrap();
3964
3965        assert!(db.engine.active_memtable_is_empty());
3966        // And data is still readable through the SSTable path.
3967        for i in 0..10 {
3968            let k = format!("m{:02}", i);
3969            assert_eq!(db.get(k.as_bytes()).unwrap(), Some(b"v".to_vec()));
3970        }
3971    }
3972
3973    #[test]
3974    fn test_compact_range_drains_l0() {
3975        // After a full compact_range, nothing should remain at L0 -
3976        // every file must have been pushed down to L1+.
3977        let dir = TempDir::new().unwrap();
3978        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3979
3980        for i in 0..200 {
3981            let k = format!("k{:04}", i);
3982            db.put(k.as_bytes(), b"v").unwrap();
3983        }
3984
3985        db.compact_range(None, None).unwrap();
3986
3987        assert_eq!(level_file_count(&db, 0), 0);
3988        // Some higher level must hold the data.
3989        assert!(total_file_count(&db) > 0);
3990    }
3991
3992    #[test]
3993    fn test_compact_range_bounded_preserves_all_data() {
3994        let dir = TempDir::new().unwrap();
3995        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
3996
3997        // Three disjoint ranges: low (a*), mid (m*), high (z*).
3998        for i in 0..100 {
3999            db.put(format!("a{:03}", i).as_bytes(), b"a").unwrap();
4000        }
4001        for i in 0..100 {
4002            db.put(format!("m{:03}", i).as_bytes(), b"m").unwrap();
4003        }
4004        for i in 0..100 {
4005            db.put(format!("z{:03}", i).as_bytes(), b"z").unwrap();
4006        }
4007
4008        // Only compact the mid range.
4009        db.compact_range(Some(b"m"), Some(b"n")).unwrap();
4010
4011        // Every key must still be readable regardless of the range.
4012        for i in 0..100 {
4013            assert_eq!(
4014                db.get(format!("a{:03}", i).as_bytes()).unwrap(),
4015                Some(b"a".to_vec())
4016            );
4017            assert_eq!(
4018                db.get(format!("m{:03}", i).as_bytes()).unwrap(),
4019                Some(b"m".to_vec())
4020            );
4021            assert_eq!(
4022                db.get(format!("z{:03}", i).as_bytes()).unwrap(),
4023                Some(b"z".to_vec())
4024            );
4025        }
4026    }
4027
4028    #[test]
4029    fn test_compact_range_bounded_compacts_default_cf_files() {
4030        let dir = TempDir::new().unwrap();
4031        let opts = Options {
4032            write_buffer_size: 4 * 1024,
4033            l0_compaction_trigger: 1_000,
4034            ..Options::default()
4035        };
4036        let db = Db::open(dir.path(), opts).unwrap();
4037        let payload = vec![0u8; 512];
4038
4039        for i in 0..32 {
4040            db.put(format!("m{i:04}").as_bytes(), &payload).unwrap();
4041        }
4042        force_flush_with_prefix(&db, "m_flush");
4043
4044        let l0_before = level_file_count(&db, 0);
4045        assert!(l0_before > 0);
4046
4047        db.compact_range(Some(b"m"), Some(b"n")).unwrap();
4048
4049        assert_eq!(level_file_count(&db, 0), 0);
4050        assert!(total_file_count(&db) > 0);
4051        assert_eq!(db.get(b"m0000").unwrap(), Some(payload));
4052    }
4053
4054    #[test]
4055    fn test_compact_range_reclaims_space_after_overwrite() {
4056        // Write N keys, overwrite them, force flush, then compact_range.
4057        // The number of distinct entries after compaction should be N
4058        // (one per user key) - the old overwritten versions got merged
4059        // away by deduplication during compaction.
4060        let dir = TempDir::new().unwrap();
4061        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4062
4063        for i in 0..200 {
4064            let k = format!("k{:03}", i);
4065            db.put(k.as_bytes(), b"v1").unwrap();
4066        }
4067        for i in 0..200 {
4068            let k = format!("k{:03}", i);
4069            db.put(k.as_bytes(), b"v2").unwrap();
4070        }
4071
4072        db.compact_range(None, None).unwrap();
4073
4074        for i in 0..200 {
4075            let k = format!("k{:03}", i);
4076            assert_eq!(db.get(k.as_bytes()).unwrap(), Some(b"v2".to_vec()));
4077        }
4078    }
4079
4080    #[test]
4081    fn test_compact_range_runs_alongside_background_compaction() {
4082        // Write enough to trigger background compactions, then while
4083        // the engine is still churning, fire a foreground compact_range.
4084        // Both must complete without corruption.
4085        let dir = TempDir::new().unwrap();
4086        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4087
4088        const N: usize = 2_000;
4089        for i in 0..N {
4090            let k = format!("key_{:05}", i);
4091            db.put(k.as_bytes(), b"v").unwrap();
4092        }
4093
4094        db.compact_range(None, None).unwrap();
4095
4096        // After the foreground compaction, every key is still there.
4097        for i in 0..N {
4098            let k = format!("key_{:05}", i);
4099            assert_eq!(db.get(k.as_bytes()).unwrap(), Some(b"v".to_vec()));
4100        }
4101    }
4102
4103    #[test]
4104    fn test_compact_range_iterator_still_correct() {
4105        // compact_range shouldn't perturb an iterator built after it.
4106        let dir = TempDir::new().unwrap();
4107        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4108
4109        for i in 0..300 {
4110            let k = format!("k{:04}", i);
4111            db.put(k.as_bytes(), b"v").unwrap();
4112        }
4113
4114        db.compact_range(None, None).unwrap();
4115
4116        let mut it = db.iter();
4117        it.seek_to_first();
4118        let mut count = 0;
4119        while it.valid() {
4120            if it.key().unwrap().starts_with(b"k") {
4121                count += 1;
4122            }
4123            it.next();
4124        }
4125        assert_eq!(count, 300);
4126    }
4127
4128    #[test]
4129    fn test_compact_range_tombstones_are_preserved() {
4130        // Tombstones must survive compaction until the bottommost level
4131        // drops them - for now compaction preserves all versions, so a
4132        // deleted key is still absent to reads.
4133        let dir = TempDir::new().unwrap();
4134        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4135
4136        for i in 0..50 {
4137            let k = format!("k{:02}", i);
4138            db.put(k.as_bytes(), b"v").unwrap();
4139        }
4140        // Delete half of them.
4141        for i in (0..50).step_by(2) {
4142            let k = format!("k{:02}", i);
4143            db.delete(k.as_bytes()).unwrap();
4144        }
4145
4146        db.compact_range(None, None).unwrap();
4147
4148        for i in 0..50 {
4149            let k = format!("k{:02}", i);
4150            let expected = if i % 2 == 0 {
4151                None
4152            } else {
4153                Some(b"v".to_vec())
4154            };
4155            assert_eq!(db.get(k.as_bytes()).unwrap(), expected);
4156        }
4157    }
4158
4159    #[test]
4160    fn test_compaction_range_tombstone_bounds_cover_point_keys_in_other_files() {
4161        let dir = TempDir::new().unwrap();
4162        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4163
4164        db.put(b"b", b"old").unwrap();
4165        force_flush_with_prefix(&db, "__old_flush");
4166        db.compact_range(None, None).unwrap();
4167
4168        db.delete_range(b"a", b"z").unwrap();
4169        db.put(b"m", b"new").unwrap();
4170        force_flush_with_prefix(&db, "zz_new_flush");
4171        db.compact_range(None, None).unwrap();
4172
4173        assert_eq!(db.get(b"b").unwrap(), None);
4174        assert_eq!(db.get(b"m").unwrap(), Some(b"new".to_vec()));
4175    }
4176
4177    #[test]
4178    fn test_compaction_splits_range_tombstones_around_point_outputs() {
4179        let dir = TempDir::new().unwrap();
4180        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4181
4182        db.put(b"b", b"old-left").unwrap();
4183        db.put(b"y", b"old-right").unwrap();
4184        db.compact_range(None, None).unwrap();
4185
4186        db.delete_range(b"a", b"z").unwrap();
4187        db.put(b"m", b"new").unwrap();
4188        db.compact_range(None, None).unwrap();
4189
4190        assert_eq!(db.get(b"b").unwrap(), None);
4191        assert_eq!(db.get(b"m").unwrap(), Some(b"new".to_vec()));
4192        assert_eq!(db.get(b"y").unwrap(), None);
4193
4194        let version = db.engine.current_version();
4195        let rt_only_files = version
4196            .levels
4197            .iter()
4198            .flatten()
4199            .filter(|file| file.meta.num_entries == 0)
4200            .count();
4201        assert!(
4202            rt_only_files >= 2,
4203            "range tombstone gaps should be emitted separately, got {rt_only_files}"
4204        );
4205    }
4206
4207    // ─── MultiGet tests ─────────────────────────────────────────────────
4208
4209    #[test]
4210    fn test_multi_get_empty_batch() {
4211        let (db, _dir) = open_tmp();
4212        db.put(b"x", b"y").unwrap();
4213        let results = db.multi_get(&[]).unwrap();
4214        assert!(results.is_empty());
4215    }
4216
4217    #[test]
4218    fn test_multi_get_all_hit() {
4219        let (db, _dir) = open_tmp();
4220        db.put(b"a", b"1").unwrap();
4221        db.put(b"b", b"2").unwrap();
4222        db.put(b"c", b"3").unwrap();
4223
4224        let keys: &[&[u8]] = &[b"a", b"b", b"c"];
4225        let results = db.multi_get(keys).unwrap();
4226        assert_eq!(
4227            results,
4228            vec![
4229                Some(b"1".to_vec()),
4230                Some(b"2".to_vec()),
4231                Some(b"3".to_vec())
4232            ]
4233        );
4234    }
4235
4236    #[test]
4237    fn test_multi_get_all_miss() {
4238        let (db, _dir) = open_tmp();
4239        db.put(b"a", b"1").unwrap();
4240
4241        let keys: &[&[u8]] = &[b"x", b"y", b"z"];
4242        let results = db.multi_get(keys).unwrap();
4243        assert_eq!(results, vec![None, None, None]);
4244    }
4245
4246    #[test]
4247    fn test_multi_get_mixed_hit_miss() {
4248        let (db, _dir) = open_tmp();
4249        db.put(b"a", b"1").unwrap();
4250        db.put(b"c", b"3").unwrap();
4251
4252        let keys: &[&[u8]] = &[b"a", b"b", b"c", b"d"];
4253        let results = db.multi_get(keys).unwrap();
4254        assert_eq!(
4255            results,
4256            vec![Some(b"1".to_vec()), None, Some(b"3".to_vec()), None]
4257        );
4258    }
4259
4260    #[test]
4261    fn test_multi_get_preserves_input_order() {
4262        let (db, _dir) = open_tmp();
4263        db.put(b"a", b"1").unwrap();
4264        db.put(b"b", b"2").unwrap();
4265        db.put(b"c", b"3").unwrap();
4266
4267        // Reverse order input.
4268        let keys: &[&[u8]] = &[b"c", b"a", b"b"];
4269        let results = db.multi_get(keys).unwrap();
4270        assert_eq!(
4271            results,
4272            vec![
4273                Some(b"3".to_vec()),
4274                Some(b"1".to_vec()),
4275                Some(b"2".to_vec())
4276            ]
4277        );
4278    }
4279
4280    #[test]
4281    fn test_multi_get_duplicates_in_input() {
4282        let (db, _dir) = open_tmp();
4283        db.put(b"a", b"1").unwrap();
4284        db.put(b"b", b"2").unwrap();
4285
4286        let keys: &[&[u8]] = &[b"a", b"b", b"a", b"missing", b"a"];
4287        let results = db.multi_get(keys).unwrap();
4288        assert_eq!(
4289            results,
4290            vec![
4291                Some(b"1".to_vec()),
4292                Some(b"2".to_vec()),
4293                Some(b"1".to_vec()),
4294                None,
4295                Some(b"1".to_vec()),
4296            ]
4297        );
4298    }
4299
4300    #[test]
4301    fn test_multi_get_honors_tombstones() {
4302        let (db, _dir) = open_tmp();
4303        db.put(b"a", b"1").unwrap();
4304        db.put(b"b", b"2").unwrap();
4305        db.put(b"c", b"3").unwrap();
4306        db.delete(b"b").unwrap();
4307
4308        let keys: &[&[u8]] = &[b"a", b"b", b"c"];
4309        let results = db.multi_get(keys).unwrap();
4310        assert_eq!(
4311            results,
4312            vec![Some(b"1".to_vec()), None, Some(b"3".to_vec())]
4313        );
4314    }
4315
4316    #[test]
4317    fn test_multi_get_tombstone_hides_older_level_entry() {
4318        let dir = TempDir::new().unwrap();
4319        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4320
4321        db.put(b"keep", b"v").unwrap();
4322        db.put(b"gone", b"v").unwrap();
4323        force_flush(&db, "x");
4324        db.delete(b"gone").unwrap();
4325
4326        let keys: &[&[u8]] = &[b"keep", b"gone"];
4327        let results = db.multi_get(keys).unwrap();
4328        assert_eq!(results, vec![Some(b"v".to_vec()), None]);
4329    }
4330
4331    #[test]
4332    fn test_multi_get_spans_memtable_and_l0() {
4333        let dir = TempDir::new().unwrap();
4334        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4335
4336        db.put(b"from_l0_1", b"v1").unwrap();
4337        db.put(b"from_l0_2", b"v2").unwrap();
4338        force_flush(&db, "x");
4339
4340        db.put(b"from_mem_1", b"v3").unwrap();
4341        db.put(b"from_mem_2", b"v4").unwrap();
4342
4343        let keys: &[&[u8]] = &[b"from_mem_1", b"from_l0_1", b"from_mem_2", b"from_l0_2"];
4344        let results = db.multi_get(keys).unwrap();
4345        assert_eq!(
4346            results,
4347            vec![
4348                Some(b"v3".to_vec()),
4349                Some(b"v1".to_vec()),
4350                Some(b"v4".to_vec()),
4351                Some(b"v2".to_vec())
4352            ]
4353        );
4354    }
4355
4356    #[test]
4357    fn test_multi_get_snapshot_isolation() {
4358        let (db, _dir) = open_tmp();
4359        db.put(b"a", b"a1").unwrap();
4360        db.put(b"b", b"b1").unwrap();
4361
4362        let snap = db.snapshot();
4363
4364        db.put(b"a", b"a2").unwrap();
4365        db.put(b"c", b"c1").unwrap();
4366        db.delete(b"b").unwrap();
4367
4368        let keys: &[&[u8]] = &[b"a", b"b", b"c"];
4369        let results = snap.multi_get(keys).unwrap();
4370        assert_eq!(
4371            results,
4372            vec![Some(b"a1".to_vec()), Some(b"b1".to_vec()), None],
4373        );
4374    }
4375
4376    #[test]
4377    fn test_multi_get_consistency_with_get() {
4378        // For any batch, multi_get must return the same results as a
4379        // loop of individual get calls at the same snapshot.
4380        let dir = TempDir::new().unwrap();
4381        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4382
4383        for i in 0..500 {
4384            let k = format!("k{:04}", i);
4385            db.put(k.as_bytes(), format!("v{}", i).as_bytes()).unwrap();
4386        }
4387        // Delete some.
4388        for i in (0..500).step_by(7) {
4389            let k = format!("k{:04}", i);
4390            db.delete(k.as_bytes()).unwrap();
4391        }
4392
4393        // Snapshot so individual gets and multi_get see the same thing.
4394        let snap = db.snapshot();
4395
4396        let keys_owned: Vec<String> = (0..500)
4397            .step_by(3)
4398            .map(|i| format!("k{:04}", i))
4399            .chain(std::iter::once("missing_key".to_string()))
4400            .collect();
4401        let keys: Vec<&[u8]> = keys_owned.iter().map(|s| s.as_bytes()).collect();
4402
4403        let individual: Vec<_> = keys.iter().map(|k| snap.get(k).unwrap()).collect();
4404        let batched = snap.multi_get(&keys).unwrap();
4405
4406        assert_eq!(individual, batched);
4407        assert_eq!(individual.len(), keys.len());
4408    }
4409
4410    #[test]
4411    fn test_multi_get_large_batch_after_flush() {
4412        let dir = TempDir::new().unwrap();
4413        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4414
4415        const N: usize = 2_000;
4416        for i in 0..N {
4417            let k = format!("key_{:05}", i);
4418            db.put(k.as_bytes(), b"v").unwrap();
4419        }
4420
4421        let keys_owned: Vec<String> = (0..N).map(|i| format!("key_{:05}", i)).collect();
4422        let keys: Vec<&[u8]> = keys_owned.iter().map(|s| s.as_bytes()).collect();
4423        let results = db.multi_get(&keys).unwrap();
4424        assert_eq!(results.len(), N);
4425        for r in &results {
4426            assert_eq!(r.as_deref(), Some(b"v".as_ref()));
4427        }
4428    }
4429
4430    #[test]
4431    fn test_multi_get_compacted_level_range_tombstones() {
4432        let dir = TempDir::new().unwrap();
4433        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4434
4435        for i in 0..30 {
4436            let key = format!("k{:02}", i);
4437            let value = format!("v{:02}", i);
4438            db.put(key.as_bytes(), value.as_bytes()).unwrap();
4439        }
4440        force_flush(&db, "base");
4441        db.delete_range(b"k10", b"k20").unwrap();
4442        db.compact_range(None, None).unwrap();
4443
4444        let version = db.engine.current_version();
4445        assert!(
4446            version.levels.iter().skip(1).any(|level| !level.is_empty()),
4447            "test must exercise L1+ files"
4448        );
4449
4450        let keys_owned = ["k09", "k10", "k15", "k20", "k15", "k09", "missing", "k29"];
4451        let keys: Vec<&[u8]> = keys_owned.iter().map(|key| key.as_bytes()).collect();
4452        let individual: Vec<_> = keys.iter().map(|key| db.get(key).unwrap()).collect();
4453        let batched = db.multi_get(&keys).unwrap();
4454
4455        assert_eq!(batched, individual);
4456        assert_eq!(batched[0], Some(b"v09".to_vec()));
4457        assert_eq!(batched[1], None);
4458        assert_eq!(batched[2], None);
4459        assert_eq!(batched[3], Some(b"v20".to_vec()));
4460        assert_eq!(batched[4], None);
4461        assert_eq!(batched[5], Some(b"v09".to_vec()));
4462        assert_eq!(batched[6], None);
4463        assert_eq!(batched[7], Some(b"v29".to_vec()));
4464    }
4465
4466    // ─── Reverse iteration tests ─────────────────────────────────────────
4467
4468    fn collect_reverse(db: &Db) -> Vec<(Vec<u8>, Vec<u8>)> {
4469        let mut it = db.iter();
4470        it.seek_to_last();
4471        let mut out = Vec::new();
4472        while it.valid() {
4473            out.push((it.key().unwrap().to_vec(), it.value().unwrap().to_vec()));
4474            it.prev();
4475        }
4476        it.status().unwrap();
4477        out
4478    }
4479
4480    #[test]
4481    fn test_iter_seek_to_last_empty() {
4482        let (db, _dir) = open_tmp();
4483        let mut it = db.iter();
4484        it.seek_to_last();
4485        assert!(!it.valid());
4486    }
4487
4488    #[test]
4489    fn test_iter_reverse_walk_basic() {
4490        let (db, _dir) = open_tmp();
4491        for i in 0..10 {
4492            let k = format!("k{:02}", i);
4493            db.put(k.as_bytes(), b"v").unwrap();
4494        }
4495        let items = collect_reverse(&db);
4496        assert_eq!(items.len(), 10);
4497        for (i, (k, _)) in items.iter().enumerate() {
4498            assert_eq!(k, format!("k{:02}", 9 - i).as_bytes());
4499        }
4500    }
4501
4502    #[test]
4503    fn test_iter_prev_latest_version() {
4504        let (db, _dir) = open_tmp();
4505        db.put(b"a", b"a1").unwrap();
4506        db.put(b"b", b"b1").unwrap();
4507        db.put(b"b", b"b2").unwrap();
4508        db.put(b"c", b"c1").unwrap();
4509
4510        let mut it = db.iter();
4511        it.seek_to_last();
4512        assert_eq!(it.key(), Some(b"c".as_ref()));
4513        it.prev();
4514        assert_eq!(it.key(), Some(b"b".as_ref()));
4515        assert_eq!(it.value(), Some(b"b2".as_ref()));
4516        it.prev();
4517        assert_eq!(it.key(), Some(b"a".as_ref()));
4518        it.prev();
4519        assert!(!it.valid());
4520    }
4521
4522    #[test]
4523    fn test_iter_seek_for_prev_then_prev() {
4524        let (db, _dir) = open_tmp();
4525        db.put(b"a", b"1").unwrap();
4526        db.put(b"c", b"3").unwrap();
4527        db.put(b"e", b"5").unwrap();
4528        db.put(b"g", b"7").unwrap();
4529
4530        let mut it = db.iter();
4531        it.seek_for_prev(b"f");
4532        assert_eq!(it.key(), Some(b"e".as_ref()));
4533        it.prev();
4534        assert_eq!(it.key(), Some(b"c".as_ref()));
4535        it.prev();
4536        assert_eq!(it.key(), Some(b"a".as_ref()));
4537        it.prev();
4538        assert!(!it.valid());
4539    }
4540
4541    #[test]
4542    fn test_iter_reverse_across_flush_levels() {
4543        let dir = TempDir::new().unwrap();
4544        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4545
4546        for i in 0..20 {
4547            let k = format!("k{:02}", i);
4548            db.put(k.as_bytes(), b"v").unwrap();
4549        }
4550        force_flush(&db, "a");
4551        for i in 20..30 {
4552            let k = format!("k{:02}", i);
4553            db.put(k.as_bytes(), b"v").unwrap();
4554        }
4555
4556        let items = collect_reverse(&db);
4557        let k_count = items.iter().filter(|(k, _)| k.starts_with(b"k")).count();
4558        assert_eq!(k_count, 30);
4559        let mut prev_k: Option<Vec<u8>> = None;
4560        for (k, _) in items.iter().filter(|(k, _)| k.starts_with(b"k")) {
4561            if let Some(p) = &prev_k {
4562                assert!(k < p, "not descending: {:?} after {:?}", k, p);
4563            }
4564            prev_k = Some(k.clone());
4565        }
4566    }
4567
4568    #[test]
4569    fn test_iter_reverse_hides_tombstoned_user_key() {
4570        let dir = TempDir::new().unwrap();
4571        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4572
4573        db.put(b"keep", b"v").unwrap();
4574        db.put(b"gone", b"v").unwrap();
4575        force_flush(&db, "a");
4576        db.delete(b"gone").unwrap();
4577
4578        let items = collect_reverse(&db);
4579        let keys: Vec<_> = items.iter().map(|(k, _)| k.clone()).collect();
4580        assert!(keys.contains(&b"keep".to_vec()));
4581        assert!(!keys.contains(&b"gone".to_vec()));
4582    }
4583
4584    #[test]
4585    fn test_iter_reverse_honors_snapshot_isolation() {
4586        let (db, _dir) = open_tmp();
4587        db.put(b"k", b"v1").unwrap();
4588        let snap = db.snapshot();
4589        db.put(b"k", b"v2").unwrap();
4590
4591        let mut it = snap.iter();
4592        it.seek_to_last();
4593        assert_eq!(it.key(), Some(b"k".as_ref()));
4594        assert_eq!(it.value(), Some(b"v1".as_ref()));
4595    }
4596
4597    #[test]
4598    fn test_iter_direction_flip_forward_to_reverse() {
4599        let (db, _dir) = open_tmp();
4600        for c in b'a'..=b'e' {
4601            db.put(&[c], &[c]).unwrap();
4602        }
4603
4604        let mut it = db.iter();
4605        it.seek_to_first();
4606        assert_eq!(it.key(), Some(b"a".as_ref()));
4607        it.next();
4608        assert_eq!(it.key(), Some(b"b".as_ref()));
4609        it.next();
4610        assert_eq!(it.key(), Some(b"c".as_ref()));
4611
4612        it.prev();
4613        assert_eq!(it.key(), Some(b"b".as_ref()));
4614        it.prev();
4615        assert_eq!(it.key(), Some(b"a".as_ref()));
4616        it.prev();
4617        assert!(!it.valid());
4618    }
4619
4620    #[test]
4621    fn test_iter_direction_flip_reverse_to_forward() {
4622        let (db, _dir) = open_tmp();
4623        for c in b'a'..=b'e' {
4624            db.put(&[c], &[c]).unwrap();
4625        }
4626
4627        let mut it = db.iter();
4628        it.seek_to_last();
4629        assert_eq!(it.key(), Some(b"e".as_ref()));
4630        it.prev();
4631        assert_eq!(it.key(), Some(b"d".as_ref()));
4632        it.prev();
4633        assert_eq!(it.key(), Some(b"c".as_ref()));
4634
4635        it.next();
4636        assert_eq!(it.key(), Some(b"d".as_ref()));
4637        it.next();
4638        assert_eq!(it.key(), Some(b"e".as_ref()));
4639        it.next();
4640        assert!(!it.valid());
4641    }
4642
4643    #[test]
4644    fn test_iter_reverse_scan_10k_keys_after_flush() {
4645        let dir = TempDir::new().unwrap();
4646        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4647
4648        const N: usize = 10_000;
4649        for i in 0..N {
4650            let k = format!("key_{:06}", i);
4651            db.put(k.as_bytes(), b"v").unwrap();
4652        }
4653
4654        let mut it = db.iter();
4655        it.seek_for_prev(b"key_~"); // '~' sorts after digits
4656        let mut count = 0;
4657        let mut prev: Option<Vec<u8>> = None;
4658        while it.valid() {
4659            let k = it.key().unwrap().to_vec();
4660            if !k.starts_with(b"key_") {
4661                it.prev();
4662                continue;
4663            }
4664            if let Some(p) = &prev {
4665                assert!(k < *p, "not descending: {:?} after {:?}", k, p);
4666            }
4667            prev = Some(k);
4668            count += 1;
4669            it.prev();
4670        }
4671        assert_eq!(count, N);
4672        assert!(it.status().is_ok());
4673    }
4674
4675    #[test]
4676    fn test_iter_reverse_seek_past_end_of_multi_block_sst() {
4677        // Regression: SsTableLevelIter::seek_for_prev used to fall back
4678        // to block 0 when the target exceeded every entry in the SST.
4679        // The correct fallback is the *last* block, so reverse walks
4680        // that start past the end actually visit every user key.
4681        //
4682        // Forces a multi-block SSTable with a small `block_size`, flushes
4683        // to L0 via `close()` so the data is guaranteed to be on disk,
4684        // then reopens and runs `seek_for_prev` with a target larger
4685        // than every key.
4686        let dir = TempDir::new().unwrap();
4687        let opts = Options {
4688            block_size: 128,
4689            write_buffer_size: 64 * 1024,
4690            ..Options::default()
4691        };
4692        {
4693            let db = Db::open(dir.path(), opts.clone()).unwrap();
4694            for i in 0..60u32 {
4695                let k = format!("k{:03}", i);
4696                db.put(k.as_bytes(), b"v").unwrap();
4697            }
4698            db.close().unwrap();
4699        }
4700
4701        let db = Db::open(dir.path(), opts).unwrap();
4702        let mut it = db.iter();
4703        it.seek_for_prev(b"~"); // '~' sorts after 'k'
4704
4705        let mut seen = Vec::new();
4706        while it.valid() {
4707            seen.push(it.key().unwrap().to_vec());
4708            it.prev();
4709        }
4710        assert_eq!(seen.len(), 60);
4711        assert_eq!(seen.first().map(|k| k.as_slice()), Some(&b"k059"[..]));
4712        assert_eq!(seen.last().map(|k| k.as_slice()), Some(&b"k000"[..]));
4713    }
4714
4715    #[test]
4716    fn test_iter_seek_for_prev_on_tombstoned_key() {
4717        let dir = TempDir::new().unwrap();
4718        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4719
4720        db.put(b"a", b"a1").unwrap();
4721        db.put(b"b", b"b1").unwrap();
4722        db.put(b"c", b"c1").unwrap();
4723        force_flush(&db, "x");
4724        db.delete(b"b").unwrap();
4725
4726        let mut it = db.iter();
4727        it.seek_for_prev(b"b");
4728        // `b` is tombstoned, so reverse-seek to `b` should skip past it
4729        // and land on `a`.
4730        assert_eq!(it.key(), Some(b"a".as_ref()));
4731    }
4732
4733    #[test]
4734    fn test_iter_survives_drop_all() {
4735        // drop_all unlinks every SSTable file. An iterator captured before
4736        // drop_all holds its own Arc<SsTableReader>s (each with an open
4737        // File), so OS fd refcounting keeps the bytes alive and the
4738        // iterator continues to produce its original view.
4739        let dir = TempDir::new().unwrap();
4740        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4741
4742        for i in 0..20 {
4743            let k = format!("pin{:03}", i);
4744            db.put(k.as_bytes(), b"v").unwrap();
4745        }
4746        force_flush(&db, "pinned");
4747
4748        let mut it = db.iter();
4749        it.seek_to_first();
4750
4751        db.drop_all().unwrap();
4752
4753        let mut seen_pinned = 0;
4754        while it.valid() {
4755            if it.key().unwrap().starts_with(b"pin") {
4756                seen_pinned += 1;
4757            }
4758            it.next();
4759        }
4760        assert_eq!(seen_pinned, 20);
4761        assert!(it.status().is_ok());
4762    }
4763
4764    #[test]
4765    fn test_persistence() {
4766        let dir = TempDir::new().unwrap();
4767
4768        {
4769            let db = Db::open(dir.path(), Options::default()).unwrap();
4770            db.put(b"persist", b"data").unwrap();
4771            db.close().unwrap();
4772        }
4773
4774        {
4775            let db = Db::open(dir.path(), Options::default()).unwrap();
4776            assert_eq!(db.get(b"persist").unwrap(), Some(b"data".to_vec()));
4777        }
4778    }
4779
4780    // ── delete_range ────────────────────────────────────────────────────────
4781
4782    #[test]
4783    fn test_delete_range_basic() {
4784        let (db, _dir) = open_tmp();
4785        for c in b'a'..=b'j' {
4786            db.put(&[c], &[c]).unwrap();
4787        }
4788        db.delete_range(b"c", b"g").unwrap();
4789
4790        assert_eq!(db.get(b"a").unwrap(), Some(b"a".to_vec()));
4791        assert_eq!(db.get(b"b").unwrap(), Some(b"b".to_vec()));
4792        assert_eq!(db.get(b"c").unwrap(), None);
4793        assert_eq!(db.get(b"d").unwrap(), None);
4794        assert_eq!(db.get(b"e").unwrap(), None);
4795        assert_eq!(db.get(b"f").unwrap(), None);
4796        assert_eq!(db.get(b"g").unwrap(), Some(b"g".to_vec())); // end exclusive
4797        assert_eq!(db.get(b"j").unwrap(), Some(b"j".to_vec()));
4798    }
4799
4800    #[test]
4801    fn test_delete_range_no_op_for_empty_or_inverted() {
4802        let (db, _dir) = open_tmp();
4803        db.put(b"a", b"1").unwrap();
4804        // Inverted range should be a silent no-op.
4805        db.delete_range(b"z", b"a").unwrap();
4806        // Equal bounds should also be a no-op (half-open empty range).
4807        db.delete_range(b"a", b"a").unwrap();
4808        assert_eq!(db.get(b"a").unwrap(), Some(b"1".to_vec()));
4809    }
4810
4811    #[test]
4812    fn test_delete_range_then_put_inside_range() {
4813        let (db, _dir) = open_tmp();
4814        db.put(b"k", b"old").unwrap();
4815        db.delete_range(b"a", b"z").unwrap();
4816        // A put after the range delete must win - it has a higher seq.
4817        db.put(b"k", b"new").unwrap();
4818        assert_eq!(db.get(b"k").unwrap(), Some(b"new".to_vec()));
4819    }
4820
4821    #[test]
4822    fn test_delete_range_put_then_range_delete_then_overwrite() {
4823        let (db, _dir) = open_tmp();
4824        db.put(b"k", b"v1").unwrap();
4825        db.delete_range(b"a", b"z").unwrap();
4826        assert_eq!(db.get(b"k").unwrap(), None);
4827        db.put(b"k", b"v2").unwrap();
4828        assert_eq!(db.get(b"k").unwrap(), Some(b"v2".to_vec()));
4829    }
4830
4831    #[test]
4832    fn test_delete_range_snapshot_isolation() {
4833        let (db, _dir) = open_tmp();
4834        db.put(b"k", b"v1").unwrap();
4835        let snap = db.snapshot();
4836        db.delete_range(b"a", b"z").unwrap();
4837        assert_eq!(db.get(b"k").unwrap(), None);
4838        // Snapshot is anchored before the range delete.
4839        assert_eq!(snap.get(b"k").unwrap(), Some(b"v1".to_vec()));
4840    }
4841
4842    #[test]
4843    fn test_delete_range_survives_flush() {
4844        let dir = TempDir::new().unwrap();
4845        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4846        for i in 0..20 {
4847            db.put(format!("key_{:02}", i).as_bytes(), b"v").unwrap();
4848        }
4849        db.delete_range(b"key_05", b"key_15").unwrap();
4850        force_flush(&db, "rt");
4851        for i in 0..20 {
4852            let key = format!("key_{:02}", i);
4853            let got = db.get(key.as_bytes()).unwrap();
4854            if (5..15).contains(&i) {
4855                assert_eq!(got, None, "key {} should be deleted", key);
4856            } else {
4857                assert_eq!(got, Some(b"v".to_vec()), "key {} should survive", key);
4858            }
4859        }
4860    }
4861
4862    #[test]
4863    fn test_delete_range_survives_compaction() {
4864        let dir = TempDir::new().unwrap();
4865        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
4866        for i in 0..30 {
4867            db.put(format!("key_{:02}", i).as_bytes(), b"v").unwrap();
4868        }
4869        db.delete_range(b"key_10", b"key_20").unwrap();
4870        // Force several flushes + a manual compaction down to L1+.
4871        for tag in 0..6 {
4872            force_flush(&db, &format!("c{}", tag));
4873        }
4874        db.compact_range(None, None).unwrap();
4875
4876        for i in 0..30 {
4877            let key = format!("key_{:02}", i);
4878            let got = db.get(key.as_bytes()).unwrap();
4879            if (10..20).contains(&i) {
4880                assert_eq!(got, None, "key {} should be deleted post-compact", key);
4881            } else {
4882                assert_eq!(
4883                    got,
4884                    Some(b"v".to_vec()),
4885                    "key {} should survive compact",
4886                    key
4887                );
4888            }
4889        }
4890    }
4891
4892    #[test]
4893    fn test_delete_range_iterator_skips_deleted() {
4894        let (db, _dir) = open_tmp();
4895        for c in b'a'..=b'h' {
4896            db.put(&[c], &[c]).unwrap();
4897        }
4898        db.delete_range(b"c", b"f").unwrap();
4899
4900        let results = db.scan(None, None).unwrap();
4901        let keys: Vec<u8> = results.iter().map(|(k, _)| k[0]).collect();
4902        assert_eq!(keys, vec![b'a', b'b', b'f', b'g', b'h']);
4903    }
4904
4905    #[test]
4906    fn test_delete_range_reverse_iterator_skips_deleted() {
4907        let (db, _dir) = open_tmp();
4908        for c in b'a'..=b'h' {
4909            db.put(&[c], &[c]).unwrap();
4910        }
4911        db.delete_range(b"c", b"f").unwrap();
4912
4913        let mut iter = db.iter();
4914        iter.seek_to_last();
4915        let mut keys = Vec::new();
4916        while iter.valid() {
4917            keys.push(iter.key().unwrap()[0]);
4918            iter.prev();
4919        }
4920        assert_eq!(keys, vec![b'h', b'g', b'f', b'b', b'a']);
4921    }
4922
4923    #[test]
4924    fn test_delete_range_multi_get_honors_rt() {
4925        let (db, _dir) = open_tmp();
4926        for c in b'a'..=b'f' {
4927            db.put(&[c], &[c]).unwrap();
4928        }
4929        db.delete_range(b"b", b"e").unwrap();
4930
4931        let keys: Vec<&[u8]> = vec![b"a", b"b", b"c", b"d", b"e", b"f"];
4932        let got = db.multi_get(&keys).unwrap();
4933        assert_eq!(got[0], Some(b"a".to_vec()));
4934        assert_eq!(got[1], None);
4935        assert_eq!(got[2], None);
4936        assert_eq!(got[3], None);
4937        assert_eq!(got[4], Some(b"e".to_vec()));
4938        assert_eq!(got[5], Some(b"f".to_vec()));
4939    }
4940
4941    #[test]
4942    fn test_delete_range_crash_recovery() {
4943        let dir = TempDir::new().unwrap();
4944        {
4945            let db = Db::open(dir.path(), Options::default()).unwrap();
4946            for c in b'a'..=b'e' {
4947                db.put(&[c], &[c]).unwrap();
4948            }
4949            db.delete_range(b"b", b"d").unwrap();
4950            // Drop without close - only the WAL has the range delete.
4951        }
4952        let db = Db::open(dir.path(), Options::default()).unwrap();
4953        assert_eq!(db.get(b"a").unwrap(), Some(b"a".to_vec()));
4954        assert_eq!(db.get(b"b").unwrap(), None);
4955        assert_eq!(db.get(b"c").unwrap(), None);
4956        assert_eq!(db.get(b"d").unwrap(), Some(b"d".to_vec()));
4957        assert_eq!(db.get(b"e").unwrap(), Some(b"e".to_vec()));
4958    }
4959
4960    #[test]
4961    fn test_delete_range_in_write_batch() {
4962        let (db, _dir) = open_tmp();
4963        for c in b'a'..=b'f' {
4964            db.put(&[c], &[c]).unwrap();
4965        }
4966        let mut batch = WriteBatch::new();
4967        batch.put(b"x", b"x");
4968        batch.delete_range(b"b", b"e");
4969        batch.put(b"y", b"y");
4970        db.write(batch).unwrap();
4971
4972        assert_eq!(db.get(b"a").unwrap(), Some(b"a".to_vec()));
4973        assert_eq!(db.get(b"b").unwrap(), None);
4974        assert_eq!(db.get(b"c").unwrap(), None);
4975        assert_eq!(db.get(b"d").unwrap(), None);
4976        assert_eq!(db.get(b"e").unwrap(), Some(b"e".to_vec()));
4977        assert_eq!(db.get(b"x").unwrap(), Some(b"x".to_vec()));
4978        assert_eq!(db.get(b"y").unwrap(), Some(b"y".to_vec()));
4979    }
4980
4981    #[test]
4982    fn test_write_batch_delete_range_then_put_inside_range_keeps_put() {
4983        let (db, _dir) = open_tmp();
4984        db.put(b"k", b"old").unwrap();
4985
4986        let mut batch = WriteBatch::new();
4987        batch.delete_range(b"a", b"z");
4988        batch.put(b"k", b"new");
4989        db.write(batch).unwrap();
4990
4991        assert_eq!(db.get(b"k").unwrap(), Some(b"new".to_vec()));
4992    }
4993
4994    #[test]
4995    fn test_write_batch_put_then_delete_range_inside_range_deletes_put() {
4996        let (db, _dir) = open_tmp();
4997
4998        let mut batch = WriteBatch::new();
4999        batch.put(b"k", b"new");
5000        batch.delete_range(b"a", b"z");
5001        db.write(batch).unwrap();
5002
5003        assert_eq!(db.get(b"k").unwrap(), None);
5004    }
5005
5006    #[test]
5007    fn test_write_batch_order_survives_wal_replay() {
5008        let dir = TempDir::new().unwrap();
5009        {
5010            let db = Db::open(dir.path(), Options::default()).unwrap();
5011            db.put(b"k", b"old").unwrap();
5012
5013            let mut batch = WriteBatch::new();
5014            batch.delete_range(b"a", b"z");
5015            batch.put(b"k", b"new");
5016            db.write(batch).unwrap();
5017            // Drop without an explicit close so reopen must recover
5018            // the ordered batch from the WAL.
5019        }
5020
5021        let db = Db::open(dir.path(), Options::default()).unwrap();
5022        assert_eq!(db.get(b"k").unwrap(), Some(b"new".to_vec()));
5023    }
5024
5025    #[test]
5026    fn test_delete_range_overlapping_ranges() {
5027        let (db, _dir) = open_tmp();
5028        for c in b'a'..=b'j' {
5029            db.put(&[c], &[c]).unwrap();
5030        }
5031        db.delete_range(b"b", b"e").unwrap();
5032        db.delete_range(b"d", b"h").unwrap();
5033
5034        assert_eq!(db.get(b"a").unwrap(), Some(b"a".to_vec()));
5035        for c in b'b'..=b'g' {
5036            assert_eq!(db.get(&[c]).unwrap(), None, "key {} deleted", c as char);
5037        }
5038        assert_eq!(db.get(b"h").unwrap(), Some(b"h".to_vec()));
5039    }
5040
5041    // ── compression codecs ──────────────────────────────────────────────────
5042
5043    fn compression_opts(codec: CompressionType) -> Options {
5044        Options {
5045            write_buffer_size: 4 * 1024,
5046            compression: codec,
5047            ..Options::default()
5048        }
5049    }
5050
5051    fn write_and_read_back(opts: Options) {
5052        let dir = TempDir::new().unwrap();
5053        let payload: Vec<u8> = (0..256).map(|i| (i % 31) as u8).collect();
5054        {
5055            let db = Db::open(dir.path(), opts.clone()).unwrap();
5056            for i in 0..200 {
5057                let key = format!("key_{:04}", i);
5058                db.put(key.as_bytes(), &payload).unwrap();
5059            }
5060            // Force a flush so reads must go through the SSTable codec path.
5061            force_flush(&db, "comp");
5062            for i in 0..200 {
5063                let key = format!("key_{:04}", i);
5064                assert_eq!(
5065                    db.get(key.as_bytes()).unwrap().as_deref(),
5066                    Some(payload.as_slice()),
5067                    "round-trip failed for {key}"
5068                );
5069            }
5070            db.close().unwrap();
5071        }
5072        // Reopen to verify the on-disk codec is decoded correctly by a
5073        // fresh reader.
5074        let db = Db::open(dir.path(), opts).unwrap();
5075        for i in 0..200 {
5076            let key = format!("key_{:04}", i);
5077            assert_eq!(
5078                db.get(key.as_bytes()).unwrap().as_deref(),
5079                Some(payload.as_slice())
5080            );
5081        }
5082    }
5083
5084    #[test]
5085    fn test_compression_none_roundtrip() {
5086        write_and_read_back(compression_opts(CompressionType::None));
5087    }
5088
5089    #[test]
5090    fn test_compression_lz4_roundtrip() {
5091        write_and_read_back(compression_opts(CompressionType::Lz4));
5092    }
5093
5094    #[test]
5095    fn test_compression_snappy_roundtrip() {
5096        write_and_read_back(compression_opts(CompressionType::Snappy));
5097    }
5098
5099    #[test]
5100    fn test_compression_per_level_mixed_codecs() {
5101        // L0 = Snappy, L1+ = Lz4. After a flush + manual compaction the
5102        // database must hold blocks compressed with both codecs and
5103        // still read back correctly.
5104        let dir = TempDir::new().unwrap();
5105        let opts = Options {
5106            write_buffer_size: 4 * 1024,
5107            compression: CompressionType::Lz4,
5108            compression_per_level: Some(vec![
5109                CompressionType::Snappy, // L0
5110                CompressionType::Lz4,    // L1
5111                CompressionType::None,   // L2 (unused here, just to exercise the slot)
5112            ]),
5113            ..Options::default()
5114        };
5115        let payload: Vec<u8> = (0..256).map(|i| (i % 17) as u8).collect();
5116        {
5117            let db = Db::open(dir.path(), opts.clone()).unwrap();
5118            for i in 0..300 {
5119                let key = format!("k_{:04}", i);
5120                db.put(key.as_bytes(), &payload).unwrap();
5121            }
5122            force_flush(&db, "mix");
5123            // Push everything down to L1 with the manual compaction path.
5124            db.compact_range(None, None).unwrap();
5125            for i in 0..300 {
5126                let key = format!("k_{:04}", i);
5127                assert_eq!(
5128                    db.get(key.as_bytes()).unwrap().as_deref(),
5129                    Some(payload.as_slice())
5130                );
5131            }
5132            db.close().unwrap();
5133        }
5134        // Reopen and re-read so the test exercises a fresh reader
5135        // hitting both codecs through the level layout we just built.
5136        let db = Db::open(dir.path(), opts).unwrap();
5137        for i in 0..300 {
5138            let key = format!("k_{:04}", i);
5139            assert_eq!(
5140                db.get(key.as_bytes()).unwrap().as_deref(),
5141                Some(payload.as_slice())
5142            );
5143        }
5144    }
5145
5146    #[test]
5147    fn test_compression_per_level_falls_back_to_default() {
5148        // Override only L0; L1+ should fall back to `compression`.
5149        let dir = TempDir::new().unwrap();
5150        let opts = Options {
5151            write_buffer_size: 4 * 1024,
5152            compression: CompressionType::Snappy,
5153            compression_per_level: Some(vec![CompressionType::None]),
5154            ..Options::default()
5155        };
5156        let db = Db::open(dir.path(), opts).unwrap();
5157        for i in 0..50 {
5158            db.put(format!("k_{i:03}").as_bytes(), b"v").unwrap();
5159        }
5160        force_flush(&db, "fb");
5161        db.compact_range(None, None).unwrap();
5162        for i in 0..50 {
5163            assert_eq!(
5164                db.get(format!("k_{i:03}").as_bytes()).unwrap(),
5165                Some(b"v".to_vec())
5166            );
5167        }
5168    }
5169
5170    // ── compaction filter ───────────────────────────────────────────────────
5171
5172    use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
5173
5174    /// Test filter that drops every entry whose user key ends in an
5175    /// odd ASCII digit. Also counts invocations so tests can verify
5176    /// the filter actually ran.
5177    struct DropOddKeysFilter {
5178        calls: AtomicUsize,
5179    }
5180
5181    impl CompactionFilter for DropOddKeysFilter {
5182        fn filter(&self, _level: usize, key: &[u8], _value: &[u8]) -> CompactionDecision {
5183            self.calls.fetch_add(1, AtomicOrdering::Relaxed);
5184            match key.last() {
5185                Some(b) if b.is_ascii_digit() && (b - b'0') % 2 == 1 => CompactionDecision::Remove,
5186                _ => CompactionDecision::Keep,
5187            }
5188        }
5189        fn name(&self) -> &'static str {
5190            "drop_odd_keys"
5191        }
5192    }
5193
5194    /// Test filter that uppercases every ASCII-lowercase byte in the
5195    /// value. Exercises `Change`.
5196    struct UppercaseValuesFilter;
5197
5198    impl CompactionFilter for UppercaseValuesFilter {
5199        fn filter(&self, _level: usize, _key: &[u8], value: &[u8]) -> CompactionDecision {
5200            let up: Vec<u8> = value.iter().map(|b| b.to_ascii_uppercase()).collect();
5201            if up == value {
5202                CompactionDecision::Keep
5203            } else {
5204                CompactionDecision::Change(up)
5205            }
5206        }
5207        fn name(&self) -> &'static str {
5208            "uppercase_values"
5209        }
5210    }
5211
5212    /// Filter that drops every range tombstone it sees.
5213    struct DropRangeTombstonesFilter;
5214
5215    impl CompactionFilter for DropRangeTombstonesFilter {
5216        fn filter(&self, _level: usize, _key: &[u8], _value: &[u8]) -> CompactionDecision {
5217            CompactionDecision::Keep
5218        }
5219        fn filter_range_delete(
5220            &self,
5221            _level: usize,
5222            _start: &[u8],
5223            _end: &[u8],
5224        ) -> CompactionDecision {
5225            CompactionDecision::Remove
5226        }
5227        fn name(&self) -> &'static str {
5228            "drop_range_tombstones"
5229        }
5230    }
5231
5232    #[test]
5233    fn test_compaction_filter_removes_matching_entries() {
5234        let dir = TempDir::new().unwrap();
5235        let filter = Arc::new(DropOddKeysFilter {
5236            calls: AtomicUsize::new(0),
5237        });
5238        let opts = Options {
5239            write_buffer_size: 4 * 1024,
5240            compaction_filter: Some(filter.clone()),
5241            ..Options::default()
5242        };
5243        let db = Db::open(dir.path(), opts).unwrap();
5244        // 20 keys: k0..k9 written twice so compaction has work. Use
5245        // longer payloads so the tiny write buffer triggers flushes.
5246        let payload = vec![b'v'; 512];
5247        for _round in 0..4 {
5248            for i in 0..10 {
5249                db.put(format!("k{i}").as_bytes(), &payload).unwrap();
5250            }
5251        }
5252        db.compact_range(None, None).unwrap();
5253
5254        // After compaction, odd-suffix keys are gone.
5255        for i in 0..10 {
5256            let got = db.get(format!("k{i}").as_bytes()).unwrap();
5257            if i % 2 == 1 {
5258                assert_eq!(got, None, "k{i} should be filtered");
5259            } else {
5260                assert_eq!(got, Some(payload.clone()), "k{i} should survive");
5261            }
5262        }
5263        assert!(
5264            filter.calls.load(AtomicOrdering::Relaxed) > 0,
5265            "filter should have been invoked"
5266        );
5267    }
5268
5269    #[test]
5270    fn test_compaction_filter_rewrites_values() {
5271        let dir = TempDir::new().unwrap();
5272        let opts = Options {
5273            write_buffer_size: 4 * 1024,
5274            compaction_filter: Some(Arc::new(UppercaseValuesFilter)),
5275            ..Options::default()
5276        };
5277        let db = Db::open(dir.path(), opts).unwrap();
5278        for i in 0..20 {
5279            db.put(format!("k{i:02}").as_bytes(), b"hello world")
5280                .unwrap();
5281        }
5282        // Force enough flushes + manual compaction to run the filter.
5283        force_flush(&db, "filter");
5284        db.compact_range(None, None).unwrap();
5285
5286        for i in 0..20 {
5287            assert_eq!(
5288                db.get(format!("k{i:02}").as_bytes()).unwrap(),
5289                Some(b"HELLO WORLD".to_vec())
5290            );
5291        }
5292    }
5293
5294    #[test]
5295    fn test_compaction_filter_skipped_while_snapshot_alive() {
5296        let dir = TempDir::new().unwrap();
5297        let opts = Options {
5298            write_buffer_size: 4 * 1024,
5299            compaction_filter: Some(Arc::new(UppercaseValuesFilter)),
5300            ..Options::default()
5301        };
5302        let db = Db::open(dir.path(), opts).unwrap();
5303        for i in 0..20 {
5304            db.put(format!("k{i:02}").as_bytes(), b"hello").unwrap();
5305        }
5306        // Hold a snapshot so the compaction filter is skipped entirely.
5307        let snap = db.snapshot();
5308        force_flush(&db, "snap_filter");
5309        db.compact_range(None, None).unwrap();
5310
5311        // The snapshot still observes the pre-filter value because
5312        // the filter was suppressed while it was alive. The live db
5313        // reads also see the unmodified value since compaction left
5314        // it intact.
5315        for i in 0..20 {
5316            assert_eq!(
5317                snap.get(format!("k{i:02}").as_bytes()).unwrap(),
5318                Some(b"hello".to_vec())
5319            );
5320            assert_eq!(
5321                db.get(format!("k{i:02}").as_bytes()).unwrap(),
5322                Some(b"hello".to_vec())
5323            );
5324        }
5325    }
5326
5327    #[test]
5328    fn test_compaction_filter_drops_range_tombstones() {
5329        let dir = TempDir::new().unwrap();
5330        let opts = Options {
5331            write_buffer_size: 4 * 1024,
5332            compaction_filter: Some(Arc::new(DropRangeTombstonesFilter)),
5333            ..Options::default()
5334        };
5335        let db = Db::open(dir.path(), opts).unwrap();
5336        for c in b'a'..=b'f' {
5337            db.put(&[c], &[c]).unwrap();
5338        }
5339        db.delete_range(b"b", b"e").unwrap();
5340        // Before compaction, the range-delete is honored - no snapshot
5341        // pinning, so the read path sees the memtable RT directly.
5342        for c in b'b'..=b'd' {
5343            assert_eq!(db.get(&[c]).unwrap(), None);
5344        }
5345        force_flush(&db, "drop_rt");
5346        db.compact_range(None, None).unwrap();
5347
5348        // After compaction the filter dropped the RT, so the original
5349        // values come back (they were never actually overwritten).
5350        for c in b'a'..=b'f' {
5351            assert_eq!(
5352                db.get(&[c]).unwrap(),
5353                Some(vec![c]),
5354                "key {} restored",
5355                c as char
5356            );
5357        }
5358    }
5359
5360    fn prefix_opts() -> Options {
5361        Options {
5362            write_buffer_size: 4 * 1024,
5363            prefix_extractor: Some(std::sync::Arc::new(FixedLengthPrefix(10))),
5364            ..Options::default()
5365        }
5366    }
5367
5368    #[test]
5369    fn test_seek_prefix_basic() {
5370        let (db, _dir) = open_tmp();
5371        db.put(b"tenant_001:k1", b"1").unwrap();
5372        db.put(b"tenant_001:k2", b"2").unwrap();
5373        db.put(b"tenant_002:k1", b"3").unwrap();
5374        db.put(b"tenant_010:k1", b"4").unwrap();
5375
5376        let mut it = db.iter();
5377        it.seek_prefix(b"tenant_001");
5378        let mut got: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
5379        while it.valid() {
5380            got.push((it.key().unwrap().to_vec(), it.value().unwrap().to_vec()));
5381            it.next();
5382        }
5383        assert_eq!(
5384            got,
5385            vec![
5386                (b"tenant_001:k1".to_vec(), b"1".to_vec()),
5387                (b"tenant_001:k2".to_vec(), b"2".to_vec()),
5388            ]
5389        );
5390    }
5391
5392    #[test]
5393    fn test_seek_prefix_absent_returns_empty() {
5394        let dir = TempDir::new().unwrap();
5395        let db = Db::open(dir.path(), prefix_opts()).unwrap();
5396        for i in 0..200 {
5397            let key = format!("tenant_001:k{:04}", i);
5398            db.put(key.as_bytes(), b"v").unwrap();
5399        }
5400        force_flush(&db, "p");
5401
5402        let mut it = db.iter();
5403        it.seek_prefix(b"tenant_999");
5404        assert!(!it.valid(), "expected no keys under an absent prefix");
5405    }
5406
5407    #[test]
5408    fn test_seek_prefix_uses_extracted_bloom_probe_for_longer_prefix() {
5409        let stats = Arc::new(Statistics::new());
5410        let dir = TempDir::new().unwrap();
5411        let db = Db::open(
5412            dir.path(),
5413            Options {
5414                write_buffer_size: 4 * 1024,
5415                l0_compaction_trigger: 1_000,
5416                bloom_bits_per_key: 64,
5417                prefix_extractor: Some(Arc::new(FixedLengthPrefix(8))),
5418                statistics: Some(stats.clone()),
5419                ..Options::default()
5420            },
5421        )
5422        .unwrap();
5423
5424        db.put(b"aaaa:item", b"v").unwrap();
5425        db.put(b"zzzz:item", b"v").unwrap();
5426        force_flush(&db, "long_prefix_probe");
5427
5428        stats.reset();
5429        let mut it = db.iter();
5430        it.seek_prefix(b"bbbb:item");
5431        assert!(!it.valid(), "absent long prefix should return empty");
5432        it.status().unwrap();
5433        assert_eq!(
5434            stats.get_ticker(Ticker::BlockCacheMiss),
5435            0,
5436            "prefix bloom should skip the SSTable before loading a data block"
5437        );
5438    }
5439
5440    #[test]
5441    fn test_seek_prefix_across_flush_boundary() {
5442        let dir = TempDir::new().unwrap();
5443        let db = Db::open(dir.path(), prefix_opts()).unwrap();
5444
5445        // First generation → flushed to L0.
5446        db.put(b"tenant_001:a", b"1a").unwrap();
5447        db.put(b"tenant_002:a", b"2a").unwrap();
5448        force_flush(&db, "p1");
5449
5450        // Second generation → stays in memtable at iteration time.
5451        db.put(b"tenant_001:b", b"1b").unwrap();
5452        db.put(b"tenant_002:b", b"2b").unwrap();
5453
5454        let mut it = db.iter();
5455        it.seek_prefix(b"tenant_001");
5456        let mut keys: Vec<Vec<u8>> = Vec::new();
5457        while it.valid() {
5458            keys.push(it.key().unwrap().to_vec());
5459            it.next();
5460        }
5461        assert_eq!(
5462            keys,
5463            vec![b"tenant_001:a".to_vec(), b"tenant_001:b".to_vec()]
5464        );
5465    }
5466
5467    #[test]
5468    fn test_seek_prefix_after_compact_range() {
5469        let dir = TempDir::new().unwrap();
5470        let db = Db::open(dir.path(), prefix_opts()).unwrap();
5471
5472        for i in 0..50 {
5473            db.put(format!("tenant_001:{:04}", i).as_bytes(), b"v")
5474                .unwrap();
5475            db.put(format!("tenant_002:{:04}", i).as_bytes(), b"v")
5476                .unwrap();
5477        }
5478        force_flush(&db, "c1");
5479        db.compact_range(None, None).unwrap();
5480
5481        let mut it = db.iter();
5482        it.seek_prefix(b"tenant_002");
5483        let mut count = 0;
5484        while it.valid() {
5485            let k = it.key().unwrap();
5486            assert!(
5487                k.starts_with(b"tenant_002"),
5488                "got unexpected key {:?}",
5489                std::str::from_utf8(k).unwrap_or("<non-utf8>")
5490            );
5491            count += 1;
5492            it.next();
5493        }
5494        assert_eq!(count, 50);
5495    }
5496
5497    #[test]
5498    fn test_seek_prefix_mixed_with_without_extractor() {
5499        // Open with no extractor, flush some data (file A has no prefix
5500        // bloom), then reopen with an extractor and write new data
5501        // (file B has a prefix bloom). Reads through the extractor-
5502        // configured DB must still return correct results across both
5503        // files.
5504        let dir = TempDir::new().unwrap();
5505        {
5506            let db = Db::open(
5507                dir.path(),
5508                Options {
5509                    write_buffer_size: 4 * 1024,
5510                    ..Options::default()
5511                },
5512            )
5513            .unwrap();
5514            db.put(b"tenant_001:old", b"old").unwrap();
5515            force_flush(&db, "a");
5516        }
5517
5518        let db = Db::open(dir.path(), prefix_opts()).unwrap();
5519        db.put(b"tenant_001:new", b"new").unwrap();
5520        db.put(b"tenant_002:new", b"new").unwrap();
5521        force_flush(&db, "b");
5522
5523        let mut it = db.iter();
5524        it.seek_prefix(b"tenant_001");
5525        let mut keys: Vec<Vec<u8>> = Vec::new();
5526        while it.valid() {
5527            keys.push(it.key().unwrap().to_vec());
5528            it.next();
5529        }
5530        assert_eq!(
5531            keys,
5532            vec![b"tenant_001:new".to_vec(), b"tenant_001:old".to_vec()]
5533        );
5534    }
5535
5536    #[test]
5537    fn test_compaction_filter_none_is_noop() {
5538        let (db, _dir) = open_tmp();
5539        for i in 0..10 {
5540            db.put(format!("k{i}").as_bytes(), b"v").unwrap();
5541        }
5542        db.compact_range(None, None).unwrap();
5543        for i in 0..10 {
5544            assert_eq!(
5545                db.get(format!("k{i}").as_bytes()).unwrap(),
5546                Some(b"v".to_vec())
5547            );
5548        }
5549    }
5550
5551    // ── per-write WriteOptions ──────────────────────────────────────────────
5552
5553    #[test]
5554    fn test_write_options_defaults_unchanged() {
5555        // `put_opt` with a default-constructed WriteOptions must
5556        // behave identically to `put`.
5557        let (db, _dir) = open_tmp();
5558        db.put_opt(&WriteOptions::default(), b"a", b"1").unwrap();
5559        assert_eq!(db.get(b"a").unwrap(), Some(b"1".to_vec()));
5560    }
5561
5562    #[test]
5563    fn test_write_options_sync_override_persists_across_reopen() {
5564        // With Eventual default, a sync write should still land on
5565        // disk such that a reopen recovers it. (Eventual alone
5566        // already survives a clean close - this test's real content
5567        // is that the sync flag doesn't break the normal code path.)
5568        let dir = TempDir::new().unwrap();
5569        let opts = Options {
5570            durability: DurabilityMode::Eventual,
5571            ..Options::default()
5572        };
5573        {
5574            let db = Db::open(dir.path(), opts.clone()).unwrap();
5575            db.put_opt(&WriteOptions::sync(), b"critical", b"payload")
5576                .unwrap();
5577            // Deliberately skip close() - sync must have forced the
5578            // WAL to durable storage already.
5579        }
5580        let db = Db::open(dir.path(), opts).unwrap();
5581        assert_eq!(db.get(b"critical").unwrap(), Some(b"payload".to_vec()));
5582    }
5583
5584    #[test]
5585    fn test_write_options_disable_wal_loses_data_on_drop_without_flush() {
5586        // disable_wal skips the WAL append entirely. Without a clean
5587        // close(), a reopen cannot recover the write because neither
5588        // the WAL nor an SSTable has it.
5589        let dir = TempDir::new().unwrap();
5590        let opts = Options::default();
5591        {
5592            let db = Db::open(dir.path(), opts.clone()).unwrap();
5593            db.put_opt(&WriteOptions::disable_wal(), b"ephemeral", b"ghost")
5594                .unwrap();
5595            // No close() - simulate a crash. The memtable holds the
5596            // write but nothing on disk does.
5597        }
5598        let db = Db::open(dir.path(), opts).unwrap();
5599        assert_eq!(db.get(b"ephemeral").unwrap(), None);
5600    }
5601
5602    #[test]
5603    fn test_write_options_disable_wal_visible_within_session() {
5604        // Within the same process, a disable_wal write is visible
5605        // to subsequent reads via the memtable - only a crash
5606        // erases it.
5607        let (db, _dir) = open_tmp();
5608        db.put_opt(&WriteOptions::disable_wal(), b"k", b"v")
5609            .unwrap();
5610        assert_eq!(db.get(b"k").unwrap(), Some(b"v".to_vec()));
5611    }
5612
5613    #[test]
5614    fn test_write_options_disable_wal_survives_clean_close() {
5615        // A clean close() flushes the memtable to an SSTable before
5616        // shutting down. A disable_wal write still made it into the
5617        // memtable, so close() + reopen recovers it via the SSTable
5618        // (not the WAL).
5619        let dir = TempDir::new().unwrap();
5620        let opts = Options::default();
5621        {
5622            let db = Db::open(dir.path(), opts.clone()).unwrap();
5623            db.put_opt(&WriteOptions::disable_wal(), b"bulk", b"loaded")
5624                .unwrap();
5625            db.close().unwrap();
5626        }
5627        let db = Db::open(dir.path(), opts).unwrap();
5628        assert_eq!(db.get(b"bulk").unwrap(), Some(b"loaded".to_vec()));
5629    }
5630
5631    #[test]
5632    fn test_close_flushes_range_tombstone_only_memtable() {
5633        // A disable_wal range delete lives only in the active memtable
5634        // until close. Clean close must flush it even though there are
5635        // no point entries in that memtable.
5636        let dir = TempDir::new().unwrap();
5637        let opts = Options::default();
5638
5639        {
5640            let db = Db::open(dir.path(), opts.clone()).unwrap();
5641            db.put(b"k", b"v").unwrap();
5642            db.close().unwrap();
5643        }
5644
5645        {
5646            let db = Db::open(dir.path(), opts.clone()).unwrap();
5647            assert_eq!(db.get(b"k").unwrap(), Some(b"v".to_vec()));
5648            db.delete_range_opt(&WriteOptions::disable_wal(), b"a", b"z")
5649                .unwrap();
5650            assert_eq!(db.get(b"k").unwrap(), None);
5651            db.close().unwrap();
5652        }
5653
5654        let db = Db::open(dir.path(), opts).unwrap();
5655        assert_eq!(db.get(b"k").unwrap(), None);
5656    }
5657
5658    #[test]
5659    fn test_write_options_batch_overrides() {
5660        let (db, _dir) = open_tmp();
5661        let mut batch = WriteBatch::new();
5662        batch.put(b"a", b"1");
5663        batch.put(b"b", b"2");
5664        batch.delete(b"ghost");
5665        db.write_opt(&WriteOptions::sync(), batch).unwrap();
5666        assert_eq!(db.get(b"a").unwrap(), Some(b"1".to_vec()));
5667        assert_eq!(db.get(b"b").unwrap(), Some(b"2".to_vec()));
5668    }
5669
5670    #[test]
5671    fn test_write_options_delete_and_delete_range_opts() {
5672        let (db, _dir) = open_tmp();
5673        for c in b'a'..=b'f' {
5674            db.put(&[c], &[c]).unwrap();
5675        }
5676        db.delete_opt(&WriteOptions::sync(), b"c").unwrap();
5677        db.delete_range_opt(&WriteOptions::sync(), b"d", b"f")
5678            .unwrap();
5679        assert_eq!(db.get(b"a").unwrap(), Some(b"a".to_vec()));
5680        assert_eq!(db.get(b"c").unwrap(), None);
5681        assert_eq!(db.get(b"d").unwrap(), None);
5682        assert_eq!(db.get(b"e").unwrap(), None);
5683        assert_eq!(db.get(b"f").unwrap(), Some(b"f".to_vec()));
5684    }
5685
5686    #[test]
5687    fn test_write_options_low_pri_and_no_slowdown_pass_through_when_not_stalling() {
5688        // `low_pri` is accepted and ignored. `no_slowdown` only bites
5689        // while the engine is stalling, and an idle engine is not.
5690        let (db, _dir) = open_tmp();
5691        let opts = WriteOptions {
5692            low_pri: true,
5693            no_slowdown: true,
5694            ..WriteOptions::default()
5695        };
5696        db.put_opt(&opts, b"k", b"v").unwrap();
5697        assert_eq!(db.get(b"k").unwrap(), Some(b"v".to_vec()));
5698    }
5699
5700    // ── merge operator ──────────────────────────────────────────────────────
5701
5702    /// Integer-counter merge operator: every operand is the 8-byte
5703    /// big-endian i64 delta to add. `full_merge` sums them (starting
5704    /// from `base` if present) and emits the new counter value.
5705    /// `partial_merge` folds two deltas by adding them.
5706    struct CounterMerge;
5707
5708    impl MergeOperator for CounterMerge {
5709        fn full_merge(
5710            &self,
5711            _key: &[u8],
5712            base: Option<&[u8]>,
5713            operands: &[&[u8]],
5714        ) -> Option<Vec<u8>> {
5715            let mut total: i64 = match base {
5716                Some(b) if b.len() == 8 => i64::from_be_bytes(b.try_into().unwrap()),
5717                Some(_) => return None,
5718                None => 0,
5719            };
5720            for op in operands {
5721                if op.len() != 8 {
5722                    return None;
5723                }
5724                total = total.wrapping_add(i64::from_be_bytes((*op).try_into().unwrap()));
5725            }
5726            Some(total.to_be_bytes().to_vec())
5727        }
5728
5729        fn partial_merge(&self, _key: &[u8], left: &[u8], right: &[u8]) -> Option<Vec<u8>> {
5730            if left.len() != 8 || right.len() != 8 {
5731                return None;
5732            }
5733            let l = i64::from_be_bytes(left.try_into().unwrap());
5734            let r = i64::from_be_bytes(right.try_into().unwrap());
5735            Some(l.wrapping_add(r).to_be_bytes().to_vec())
5736        }
5737
5738        fn name(&self) -> &'static str {
5739            "CounterMerge"
5740        }
5741    }
5742
5743    /// String-append merge operator: every operand is raw bytes;
5744    /// `full_merge` concatenates the base (if any) with every
5745    /// operand in oldest-first order.
5746    struct AppendMerge;
5747
5748    impl MergeOperator for AppendMerge {
5749        fn full_merge(
5750            &self,
5751            _key: &[u8],
5752            base: Option<&[u8]>,
5753            operands: &[&[u8]],
5754        ) -> Option<Vec<u8>> {
5755            let mut out: Vec<u8> = base.map(|b| b.to_vec()).unwrap_or_default();
5756            for op in operands {
5757                out.extend_from_slice(op);
5758            }
5759            Some(out)
5760        }
5761
5762        fn name(&self) -> &'static str {
5763            "AppendMerge"
5764        }
5765    }
5766
5767    fn counter_opts() -> Options {
5768        Options {
5769            write_buffer_size: 4 * 1024,
5770            merge_operator: Some(Arc::new(CounterMerge)),
5771            ..Options::default()
5772        }
5773    }
5774
5775    fn encode_i64(n: i64) -> Vec<u8> {
5776        n.to_be_bytes().to_vec()
5777    }
5778
5779    #[test]
5780    fn test_merge_counter_basic_chain_of_one() {
5781        let dir = TempDir::new().unwrap();
5782        let db = Db::open(dir.path(), counter_opts()).unwrap();
5783        db.merge(b"counter", &encode_i64(5)).unwrap();
5784        assert_eq!(db.get(b"counter").unwrap(), Some(encode_i64(5)));
5785    }
5786
5787    #[test]
5788    fn test_merge_counter_chain_of_two() {
5789        let dir = TempDir::new().unwrap();
5790        let db = Db::open(dir.path(), counter_opts()).unwrap();
5791        db.put(b"counter", &encode_i64(10)).unwrap();
5792        db.merge(b"counter", &encode_i64(3)).unwrap();
5793        assert_eq!(db.get(b"counter").unwrap(), Some(encode_i64(13)));
5794    }
5795
5796    #[test]
5797    fn test_merge_counter_chain_of_ten() {
5798        let dir = TempDir::new().unwrap();
5799        let db = Db::open(dir.path(), counter_opts()).unwrap();
5800        db.put(b"counter", &encode_i64(100)).unwrap();
5801        for i in 1..=10 {
5802            db.merge(b"counter", &encode_i64(i)).unwrap();
5803        }
5804        // 100 + (1+2+...+10) = 155
5805        assert_eq!(db.get(b"counter").unwrap(), Some(encode_i64(155)));
5806    }
5807
5808    #[test]
5809    fn test_merge_counter_chain_of_1000() {
5810        let dir = TempDir::new().unwrap();
5811        let db = Db::open(dir.path(), counter_opts()).unwrap();
5812        for _ in 0..1000 {
5813            db.merge(b"counter", &encode_i64(1)).unwrap();
5814        }
5815        assert_eq!(db.get(b"counter").unwrap(), Some(encode_i64(1000)));
5816    }
5817
5818    #[test]
5819    fn test_merge_without_base_defaults_to_none() {
5820        let dir = TempDir::new().unwrap();
5821        let db = Db::open(dir.path(), counter_opts()).unwrap();
5822        // No put - counter starts at 0 (base=None).
5823        db.merge(b"counter", &encode_i64(7)).unwrap();
5824        db.merge(b"counter", &encode_i64(5)).unwrap();
5825        assert_eq!(db.get(b"counter").unwrap(), Some(encode_i64(12)));
5826    }
5827
5828    #[test]
5829    fn test_merge_failure_surfaces_key() {
5830        let dir = TempDir::new().unwrap();
5831        let db = Db::open(dir.path(), counter_opts()).unwrap();
5832        db.merge(b"counter", b"not an i64").unwrap();
5833
5834        let err = db.get(b"counter").unwrap_err();
5835        match err {
5836            Error::MergeFailed(key) => assert_eq!(key, b"counter".to_vec()),
5837            other => panic!("expected merge failure, got {other:?}"),
5838        }
5839    }
5840
5841    #[test]
5842    fn test_merge_snapshot_isolation() {
5843        let dir = TempDir::new().unwrap();
5844        let db = Db::open(dir.path(), counter_opts()).unwrap();
5845        db.put(b"counter", &encode_i64(10)).unwrap();
5846        let snap = db.snapshot();
5847        db.merge(b"counter", &encode_i64(5)).unwrap();
5848        // Live read sees 15; snapshot still sees 10.
5849        assert_eq!(db.get(b"counter").unwrap(), Some(encode_i64(15)));
5850        assert_eq!(snap.get(b"counter").unwrap(), Some(encode_i64(10)));
5851    }
5852
5853    #[test]
5854    fn test_merge_survives_flush() {
5855        let dir = TempDir::new().unwrap();
5856        let db = Db::open(dir.path(), counter_opts()).unwrap();
5857        db.put(b"counter", &encode_i64(0)).unwrap();
5858        for i in 1..=20 {
5859            db.merge(b"counter", &encode_i64(i)).unwrap();
5860        }
5861        // Push past the tiny write buffer so the chain crosses a
5862        // flush boundary (memtable → L0).
5863        force_flush(&db, "merge");
5864        // Sum = 1+2+...+20 = 210
5865        assert_eq!(db.get(b"counter").unwrap(), Some(encode_i64(210)));
5866    }
5867
5868    #[test]
5869    fn test_merge_survives_compaction_and_collapses() {
5870        let dir = TempDir::new().unwrap();
5871        let db = Db::open(dir.path(), counter_opts()).unwrap();
5872        db.put(b"counter", &encode_i64(0)).unwrap();
5873        for i in 1..=50 {
5874            db.merge(b"counter", &encode_i64(i)).unwrap();
5875        }
5876        for tag in 0..4 {
5877            force_flush(&db, &format!("c{tag}"));
5878        }
5879        db.compact_range(None, None).unwrap();
5880        // Sum 1..=50 = 1275
5881        assert_eq!(db.get(b"counter").unwrap(), Some(encode_i64(1275)));
5882    }
5883
5884    #[test]
5885    fn test_merge_tombstone_interaction() {
5886        let dir = TempDir::new().unwrap();
5887        let db = Db::open(dir.path(), counter_opts()).unwrap();
5888        // Value=10, then two merges, then delete, then two more merges.
5889        db.put(b"k", &encode_i64(10)).unwrap();
5890        db.merge(b"k", &encode_i64(5)).unwrap();
5891        db.merge(b"k", &encode_i64(3)).unwrap();
5892        db.delete(b"k").unwrap();
5893        db.merge(b"k", &encode_i64(7)).unwrap();
5894        db.merge(b"k", &encode_i64(1)).unwrap();
5895        // Reads layer the two latest merges on top of the deletion
5896        // (which resets the base to None → 0): 0 + 7 + 1 = 8.
5897        assert_eq!(db.get(b"k").unwrap(), Some(encode_i64(8)));
5898    }
5899
5900    #[test]
5901    fn test_merge_range_tombstone_interaction() {
5902        let dir = TempDir::new().unwrap();
5903        let db = Db::open(dir.path(), counter_opts()).unwrap();
5904        db.put(b"k", &encode_i64(10)).unwrap();
5905        db.merge(b"k", &encode_i64(5)).unwrap();
5906        db.delete_range(b"j", b"l").unwrap(); // hides the base
5907        db.merge(b"k", &encode_i64(7)).unwrap();
5908        // After the RT, only the latest merge (7) applies to a None base.
5909        assert_eq!(db.get(b"k").unwrap(), Some(encode_i64(7)));
5910    }
5911
5912    #[test]
5913    fn test_merge_write_batch() {
5914        let dir = TempDir::new().unwrap();
5915        let db = Db::open(dir.path(), counter_opts()).unwrap();
5916        let mut batch = WriteBatch::new();
5917        batch.put(b"a", &encode_i64(1));
5918        batch.merge(b"a", &encode_i64(2));
5919        batch.merge(b"a", &encode_i64(3));
5920        batch.put(b"b", &encode_i64(100));
5921        db.write(batch).unwrap();
5922        assert_eq!(db.get(b"a").unwrap(), Some(encode_i64(6)));
5923        assert_eq!(db.get(b"b").unwrap(), Some(encode_i64(100)));
5924    }
5925
5926    #[test]
5927    fn test_merge_append_operator() {
5928        let dir = TempDir::new().unwrap();
5929        let opts = Options {
5930            merge_operator: Some(Arc::new(AppendMerge)),
5931            ..Options::default()
5932        };
5933        let db = Db::open(dir.path(), opts).unwrap();
5934        db.put(b"s", b"hello").unwrap();
5935        db.merge(b"s", b" ").unwrap();
5936        db.merge(b"s", b"world").unwrap();
5937        assert_eq!(db.get(b"s").unwrap(), Some(b"hello world".to_vec()));
5938    }
5939
5940    #[test]
5941    fn test_merge_iterator_sees_collapsed_value() {
5942        let dir = TempDir::new().unwrap();
5943        let db = Db::open(dir.path(), counter_opts()).unwrap();
5944        db.put(b"a", &encode_i64(0)).unwrap();
5945        db.merge(b"a", &encode_i64(5)).unwrap();
5946        db.put(b"b", &encode_i64(100)).unwrap();
5947        db.merge(b"b", &encode_i64(10)).unwrap();
5948        db.merge(b"b", &encode_i64(2)).unwrap();
5949
5950        let pairs = db.scan(None, None).unwrap();
5951        assert_eq!(
5952            pairs,
5953            vec![
5954                (b"a".to_vec(), encode_i64(5)),
5955                (b"b".to_vec(), encode_i64(112)),
5956            ]
5957        );
5958    }
5959
5960    #[test]
5961    fn test_merge_iterator_reverse() {
5962        let dir = TempDir::new().unwrap();
5963        let db = Db::open(dir.path(), counter_opts()).unwrap();
5964        db.put(b"a", &encode_i64(0)).unwrap();
5965        db.merge(b"a", &encode_i64(1)).unwrap();
5966        db.put(b"b", &encode_i64(0)).unwrap();
5967        db.merge(b"b", &encode_i64(2)).unwrap();
5968        db.merge(b"b", &encode_i64(3)).unwrap();
5969
5970        let mut iter = db.iter();
5971        iter.seek_to_last();
5972        let mut collected = Vec::new();
5973        while iter.valid() {
5974            collected.push((iter.key().unwrap().to_vec(), iter.value().unwrap().to_vec()));
5975            iter.prev();
5976        }
5977        assert_eq!(
5978            collected,
5979            vec![
5980                (b"b".to_vec(), encode_i64(5)),
5981                (b"a".to_vec(), encode_i64(1)),
5982            ]
5983        );
5984    }
5985
5986    #[test]
5987    fn test_merge_crash_recovery() {
5988        let dir = TempDir::new().unwrap();
5989        {
5990            let db = Db::open(dir.path(), counter_opts()).unwrap();
5991            db.put(b"counter", &encode_i64(0)).unwrap();
5992            db.merge(b"counter", &encode_i64(7)).unwrap();
5993            db.merge(b"counter", &encode_i64(3)).unwrap();
5994            // No close - memtable flush didn't happen; WAL must
5995            // survive the chain.
5996        }
5997        let db = Db::open(dir.path(), counter_opts()).unwrap();
5998        assert_eq!(db.get(b"counter").unwrap(), Some(encode_i64(10)));
5999    }
6000
6001    #[test]
6002    fn test_merge_operator_name_plumbs_through() {
6003        // Surface-area smoke test: the configured operator's `name`
6004        // is reachable via Options::debug.
6005        let opts = counter_opts();
6006        let dbg = format!("{opts:?}");
6007        assert!(dbg.contains("CounterMerge"));
6008    }
6009
6010    // ── column families ─────────────────────────────────────────────────
6011
6012    #[test]
6013    fn test_cf_default_exists_on_open() {
6014        let (db, _dir) = open_tmp();
6015        let default = db.default_cf();
6016        assert_eq!(default.name(), DEFAULT_CF_NAME);
6017        assert!(db.column_family(DEFAULT_CF_NAME).is_some());
6018        assert_eq!(db.list_column_families(), vec![DEFAULT_CF_NAME.to_string()]);
6019    }
6020
6021    #[test]
6022    fn test_cf_create_and_lookup() {
6023        let (db, _dir) = open_tmp();
6024        let users = db.create_column_family("users").unwrap();
6025        let orders = db.create_column_family("orders").unwrap();
6026        assert_ne!(users, orders);
6027        assert_eq!(db.column_family("users"), Some(users.clone()));
6028        assert_eq!(db.column_family("orders"), Some(orders.clone()));
6029        assert!(db.column_family("missing").is_none());
6030
6031        let mut names = db.list_column_families();
6032        names.sort();
6033        assert_eq!(names, vec!["default", "orders", "users"]);
6034    }
6035
6036    #[test]
6037    fn test_cf_create_is_idempotent() {
6038        let (db, _dir) = open_tmp();
6039        let a = db.create_column_family("x").unwrap();
6040        let b = db.create_column_family("x").unwrap();
6041        assert_eq!(a, b);
6042    }
6043
6044    #[test]
6045    fn test_cf_put_get_isolated_from_default() {
6046        let (db, _dir) = open_tmp();
6047        let users = db.create_column_family("users").unwrap();
6048        db.put(b"k", b"default_val").unwrap();
6049        db.put_cf(&users, b"k", b"users_val").unwrap();
6050        assert_eq!(db.get(b"k").unwrap(), Some(b"default_val".to_vec()));
6051        assert_eq!(
6052            db.get_cf(&users, b"k").unwrap(),
6053            Some(b"users_val".to_vec())
6054        );
6055    }
6056
6057    #[test]
6058    fn test_cf_writes_to_a_invisible_from_b() {
6059        let (db, _dir) = open_tmp();
6060        let a = db.create_column_family("a").unwrap();
6061        let b = db.create_column_family("b").unwrap();
6062        db.put_cf(&a, b"shared_key", b"alpha").unwrap();
6063        assert_eq!(
6064            db.get_cf(&a, b"shared_key").unwrap(),
6065            Some(b"alpha".to_vec())
6066        );
6067        assert_eq!(db.get_cf(&b, b"shared_key").unwrap(), None);
6068    }
6069
6070    #[test]
6071    fn test_cf_delete_cf() {
6072        let (db, _dir) = open_tmp();
6073        let cf = db.create_column_family("c").unwrap();
6074        db.put_cf(&cf, b"k", b"v").unwrap();
6075        db.delete_cf(&cf, b"k").unwrap();
6076        assert_eq!(db.get_cf(&cf, b"k").unwrap(), None);
6077    }
6078
6079    #[test]
6080    fn test_cf_scan_strips_prefix() {
6081        let (db, _dir) = open_tmp();
6082        let cf = db.create_column_family("s").unwrap();
6083        db.put_cf(&cf, b"a", b"1").unwrap();
6084        db.put_cf(&cf, b"b", b"2").unwrap();
6085        db.put_cf(&cf, b"c", b"3").unwrap();
6086        let pairs = db.scan_cf(&cf, None, None).unwrap();
6087        assert_eq!(
6088            pairs,
6089            vec![
6090                (b"a".to_vec(), b"1".to_vec()),
6091                (b"b".to_vec(), b"2".to_vec()),
6092                (b"c".to_vec(), b"3".to_vec()),
6093            ]
6094        );
6095        // Bounded scan.
6096        let pairs = db.scan_cf(&cf, Some(b"b"), Some(b"c")).unwrap();
6097        assert_eq!(pairs, vec![(b"b".to_vec(), b"2".to_vec())]);
6098    }
6099
6100    #[test]
6101    fn test_cf_iter_bounded_to_cf() {
6102        let (db, _dir) = open_tmp();
6103        let a = db.create_column_family("a").unwrap();
6104        let b = db.create_column_family("b").unwrap();
6105        db.put_cf(&a, b"a1", b"A1").unwrap();
6106        db.put_cf(&a, b"a2", b"A2").unwrap();
6107        db.put_cf(&b, b"b1", b"B1").unwrap();
6108        db.put(b"d1", b"D1").unwrap();
6109
6110        let mut iter = db.iter_cf(&a);
6111        iter.seek_to_first();
6112        let mut keys = Vec::new();
6113        while iter.valid() {
6114            keys.push(iter.key().unwrap().to_vec());
6115            iter.next();
6116        }
6117        assert_eq!(keys, vec![b"a1".to_vec(), b"a2".to_vec()]);
6118    }
6119
6120    #[test]
6121    fn test_cf_iter_reverse() {
6122        let (db, _dir) = open_tmp();
6123        let cf = db.create_column_family("rev").unwrap();
6124        db.put_cf(&cf, b"a", b"1").unwrap();
6125        db.put_cf(&cf, b"b", b"2").unwrap();
6126        db.put_cf(&cf, b"c", b"3").unwrap();
6127
6128        let mut iter = db.iter_cf(&cf);
6129        iter.seek_to_last();
6130        let mut keys = Vec::new();
6131        while iter.valid() {
6132            keys.push(iter.key().unwrap().to_vec());
6133            iter.prev();
6134        }
6135        assert_eq!(keys, vec![b"c".to_vec(), b"b".to_vec(), b"a".to_vec()]);
6136    }
6137
6138    #[test]
6139    fn test_cf_drop_removes_all_keys_in_cf() {
6140        let (db, _dir) = open_tmp();
6141        let cf = db.create_column_family("tmp").unwrap();
6142        db.put_cf(&cf, b"a", b"1").unwrap();
6143        db.put_cf(&cf, b"b", b"2").unwrap();
6144        db.put_cf(&cf, b"c", b"3").unwrap();
6145        db.put(b"default_key", b"default_val").unwrap();
6146
6147        db.drop_column_family(cf.clone()).unwrap();
6148
6149        // The CF name is unregistered.
6150        assert!(db.column_family("tmp").is_none());
6151        // Default CF survives.
6152        assert_eq!(
6153            db.get(b"default_key").unwrap(),
6154            Some(b"default_val".to_vec())
6155        );
6156        // Re-creating with the same name yields a fresh, empty CF.
6157        let cf2 = db.create_column_family("tmp").unwrap();
6158        assert_eq!(db.get_cf(&cf2, b"a").unwrap(), None);
6159    }
6160
6161    #[test]
6162    fn test_cf_stale_handle_is_rejected_after_drop() {
6163        let (db, _dir) = open_tmp();
6164        let cf = db.create_column_family("tmp").unwrap();
6165        db.put_cf(&cf, b"k", b"v").unwrap();
6166        db.drop_column_family(cf.clone()).unwrap();
6167
6168        let err = db.get_cf(&cf, b"k").unwrap_err();
6169        match err {
6170            Error::InvalidColumnFamily(message) => assert!(message.contains("tmp")),
6171            other => panic!("expected invalid column family error, got {other:?}"),
6172        }
6173        assert!(db.multi_get_cf(&cf, &[b"k"]).is_err());
6174        assert!(db.put_cf(&cf, b"k", b"ghost").is_err());
6175        assert!(db.delete_cf(&cf, b"k").is_err());
6176        assert!(db.delete_range_cf(&cf, b"a", b"z").is_err());
6177        assert!(db.merge_cf(&cf, b"k", b"operand").is_err());
6178        assert!(db.scan_cf(&cf, None, None).is_err());
6179        assert_eq!(
6180            db.get_approximate_sizes_cf(&cf, &[Range::new(b"a", b"z")]),
6181            vec![0]
6182        );
6183        assert_eq!(
6184            db.get_approximate_memtable_stats_cf(&cf, Range::new(b"a", b"z")),
6185            MemTableStats::default()
6186        );
6187
6188        let mut iter = db.iter_cf(&cf);
6189        iter.seek_to_first();
6190        assert!(!iter.valid());
6191
6192        let mut tail = db.iter_tailing_cf(&cf);
6193        tail.seek_to_first();
6194        assert!(!tail.valid());
6195    }
6196
6197    #[test]
6198    fn test_cf_stale_handle_cannot_write_into_recreated_cf_name() {
6199        let (db, _dir) = open_tmp();
6200        let stale = db.create_column_family("tmp").unwrap();
6201        db.put_cf(&stale, b"k", b"old").unwrap();
6202        db.drop_column_family(stale.clone()).unwrap();
6203
6204        let live = db.create_column_family("tmp").unwrap();
6205        assert_ne!(stale, live);
6206        assert!(db.put_cf(&stale, b"k", b"ghost").is_err());
6207        db.put_cf(&live, b"k", b"new").unwrap();
6208
6209        assert!(db.get_cf(&stale, b"k").is_err());
6210        assert_eq!(db.get_cf(&live, b"k").unwrap(), Some(b"new".to_vec()));
6211    }
6212
6213    #[test]
6214    fn test_cf_write_batch_rejects_stale_handle_ops() {
6215        let (db, _dir) = open_tmp();
6216        let stale = db.create_column_family("tmp").unwrap();
6217        db.drop_column_family(stale.clone()).unwrap();
6218
6219        let mut batch = WriteBatch::new();
6220        batch.put_cf(&stale, b"k", b"ghost");
6221        let err = db.write(batch).unwrap_err();
6222        match err {
6223            Error::InvalidColumnFamily(message) => assert!(message.contains("column family id")),
6224            other => panic!("expected invalid column family error, got {other:?}"),
6225        }
6226
6227        let live = db.create_column_family("tmp").unwrap();
6228        assert_eq!(db.get_cf(&live, b"k").unwrap(), None);
6229    }
6230
6231    #[test]
6232    fn test_cf_ingest_rejects_stale_handle_entries() {
6233        let (db, dir) = open_tmp();
6234        let stale = db.create_column_family("tmp").unwrap();
6235        db.drop_column_family(stale.clone()).unwrap();
6236
6237        let path = dir.path().join("stale-cf.sst");
6238        let mut writer = SstFileWriter::create(&path, &Options::default()).unwrap();
6239        writer.put_cf(&stale, b"k", b"ghost").unwrap();
6240        writer.finish().unwrap();
6241
6242        assert!(
6243            db.ingest_external_files(&[path], IngestOptions::default())
6244                .is_err()
6245        );
6246        let live = db.create_column_family("tmp").unwrap();
6247        assert_eq!(db.get_cf(&live, b"k").unwrap(), None);
6248    }
6249
6250    #[test]
6251    fn test_cf_snapshot_rejects_stale_handle_after_drop() {
6252        let (db, _dir) = open_tmp();
6253        let cf = db.create_column_family("tmp").unwrap();
6254        db.put_cf(&cf, b"k", b"v").unwrap();
6255        let snap = db.snapshot();
6256        db.drop_column_family(cf.clone()).unwrap();
6257
6258        assert!(snap.get_cf(&cf, b"k").is_err());
6259        assert!(snap.multi_get_cf(&cf, &[b"k"]).is_err());
6260        assert!(snap.scan_cf(&cf, None, None).is_err());
6261        let mut iter = snap.iter_cf(&cf);
6262        iter.seek_to_first();
6263        assert!(!iter.valid());
6264    }
6265
6266    #[test]
6267    fn test_cf_cannot_drop_default() {
6268        let (db, _dir) = open_tmp();
6269        let default = db.default_cf();
6270        assert!(db.drop_column_family(default).is_err());
6271    }
6272
6273    #[test]
6274    fn test_cf_survives_reopen() {
6275        let dir = TempDir::new().unwrap();
6276        {
6277            let db = Db::open(dir.path(), Options::default()).unwrap();
6278            let cf = db.create_column_family("persistent").unwrap();
6279            db.put_cf(&cf, b"k", b"v").unwrap();
6280            db.close().unwrap();
6281        }
6282        let db = Db::open(dir.path(), Options::default()).unwrap();
6283        let cf = db
6284            .column_family("persistent")
6285            .expect("CF must survive reopen");
6286        assert_eq!(db.get_cf(&cf, b"k").unwrap(), Some(b"v".to_vec()));
6287    }
6288
6289    #[test]
6290    fn test_cf_dropped_cf_does_not_survive_reopen() {
6291        let dir = TempDir::new().unwrap();
6292        {
6293            let db = Db::open(dir.path(), Options::default()).unwrap();
6294            let cf = db.create_column_family("doomed").unwrap();
6295            db.put_cf(&cf, b"k", b"v").unwrap();
6296            db.drop_column_family(cf).unwrap();
6297            db.close().unwrap();
6298        }
6299        let db = Db::open(dir.path(), Options::default()).unwrap();
6300        assert!(db.column_family("doomed").is_none());
6301    }
6302
6303    #[test]
6304    fn test_cf_write_batch_cross_cf_atomic() {
6305        let (db, _dir) = open_tmp();
6306        let a = db.create_column_family("a").unwrap();
6307        let b = db.create_column_family("b").unwrap();
6308        let mut batch = WriteBatch::new();
6309        batch.put_cf(&a, b"k1", b"v_a1");
6310        batch.put_cf(&b, b"k1", b"v_b1");
6311        batch.put(b"k1", b"v_default");
6312        batch.delete_cf(&a, b"ghost");
6313        db.write(batch).unwrap();
6314
6315        assert_eq!(db.get_cf(&a, b"k1").unwrap(), Some(b"v_a1".to_vec()));
6316        assert_eq!(db.get_cf(&b, b"k1").unwrap(), Some(b"v_b1".to_vec()));
6317        assert_eq!(db.get(b"k1").unwrap(), Some(b"v_default".to_vec()));
6318    }
6319
6320    #[test]
6321    fn test_cf_write_batch_survives_crash_recovery() {
6322        let dir = TempDir::new().unwrap();
6323        {
6324            let db = Db::open(dir.path(), Options::default()).unwrap();
6325            let cf = db.create_column_family("txn").unwrap();
6326            let mut batch = WriteBatch::new();
6327            batch.put_cf(&cf, b"a", b"1");
6328            batch.put_cf(&cf, b"b", b"2");
6329            batch.put(b"default_k", b"default_v");
6330            db.write(batch).unwrap();
6331            // No close - simulate a crash. WAL must survive.
6332        }
6333        let db = Db::open(dir.path(), Options::default()).unwrap();
6334        let cf = db.column_family("txn").expect("CF must survive");
6335        assert_eq!(db.get_cf(&cf, b"a").unwrap(), Some(b"1".to_vec()));
6336        assert_eq!(db.get_cf(&cf, b"b").unwrap(), Some(b"2".to_vec()));
6337        assert_eq!(db.get(b"default_k").unwrap(), Some(b"default_v".to_vec()));
6338    }
6339
6340    #[test]
6341    fn test_cf_snapshot_isolation_per_cf() {
6342        let (db, _dir) = open_tmp();
6343        let a = db.create_column_family("a").unwrap();
6344        db.put_cf(&a, b"k", b"v0").unwrap();
6345        let snap = db.snapshot();
6346        db.put_cf(&a, b"k", b"v1").unwrap();
6347        assert_eq!(snap.get_cf(&a, b"k").unwrap(), Some(b"v0".to_vec()));
6348        assert_eq!(db.get_cf(&a, b"k").unwrap(), Some(b"v1".to_vec()));
6349    }
6350
6351    #[test]
6352    fn test_cf_scan_across_cfs_is_isolated() {
6353        let (db, _dir) = open_tmp();
6354        let a = db.create_column_family("a").unwrap();
6355        let b = db.create_column_family("b").unwrap();
6356        db.put_cf(&a, b"apple", b"A").unwrap();
6357        db.put_cf(&b, b"apple", b"B").unwrap();
6358        db.put(b"apple", b"D").unwrap();
6359
6360        assert_eq!(
6361            db.scan_cf(&a, None, None).unwrap(),
6362            vec![(b"apple".to_vec(), b"A".to_vec())]
6363        );
6364        assert_eq!(
6365            db.scan_cf(&b, None, None).unwrap(),
6366            vec![(b"apple".to_vec(), b"B".to_vec())]
6367        );
6368        assert_eq!(
6369            db.scan(None, None).unwrap(),
6370            vec![(b"apple".to_vec(), b"D".to_vec())]
6371        );
6372    }
6373
6374    #[test]
6375    fn test_cf_scan_page_is_scoped_and_resumable() {
6376        let (db, _dir) = open_tmp();
6377        let cf = db.create_column_family("paged").unwrap();
6378
6379        db.put(b"a", b"default").unwrap();
6380        db.put_cf(&cf, b"a", b"1").unwrap();
6381        db.put_cf(&cf, b"b", b"2").unwrap();
6382        db.put_cf(&cf, b"c", b"3").unwrap();
6383
6384        let first = db.scan_page_cf(&cf, None, None, 2).unwrap();
6385        assert_eq!(
6386            first,
6387            ScanPage {
6388                entries: vec![
6389                    (b"a".to_vec(), b"1".to_vec()),
6390                    (b"b".to_vec(), b"2".to_vec()),
6391                ],
6392                next_start: Some(b"c".to_vec()),
6393            }
6394        );
6395
6396        let second = db
6397            .scan_page_cf(&cf, first.next_start.as_deref(), None, 2)
6398            .unwrap();
6399        assert_eq!(
6400            second,
6401            ScanPage {
6402                entries: vec![(b"c".to_vec(), b"3".to_vec())],
6403                next_start: None,
6404            }
6405        );
6406    }
6407
6408    #[test]
6409    fn test_cf_multi_get_cf() {
6410        let (db, _dir) = open_tmp();
6411        let cf = db.create_column_family("mg").unwrap();
6412        db.put_cf(&cf, b"a", b"1").unwrap();
6413        db.put_cf(&cf, b"b", b"2").unwrap();
6414        let keys: Vec<&[u8]> = vec![b"a", b"missing", b"b"];
6415        let got = db.multi_get_cf(&cf, &keys).unwrap();
6416        assert_eq!(got, vec![Some(b"1".to_vec()), None, Some(b"2".to_vec())]);
6417    }
6418
6419    #[test]
6420    fn test_cf_delete_range_cf() {
6421        let (db, _dir) = open_tmp();
6422        let cf = db.create_column_family("r").unwrap();
6423        for c in b'a'..=b'f' {
6424            db.put_cf(&cf, &[c], &[c]).unwrap();
6425        }
6426        db.delete_range_cf(&cf, b"b", b"e").unwrap();
6427        assert_eq!(db.get_cf(&cf, b"a").unwrap(), Some(b"a".to_vec()));
6428        assert_eq!(db.get_cf(&cf, b"b").unwrap(), None);
6429        assert_eq!(db.get_cf(&cf, b"c").unwrap(), None);
6430        assert_eq!(db.get_cf(&cf, b"d").unwrap(), None);
6431        assert_eq!(db.get_cf(&cf, b"e").unwrap(), Some(b"e".to_vec()));
6432        assert_eq!(db.get_cf(&cf, b"f").unwrap(), Some(b"f".to_vec()));
6433    }
6434
6435    #[test]
6436    fn test_cf_create_empty_name_errors() {
6437        let (db, _dir) = open_tmp();
6438        assert!(db.create_column_family("").is_err());
6439    }
6440
6441    #[test]
6442    fn test_cf_many_cfs_all_isolated() {
6443        let (db, _dir) = open_tmp();
6444        let mut handles = Vec::new();
6445        for i in 0..10 {
6446            handles.push(db.create_column_family(&format!("cf{i}")).unwrap());
6447        }
6448        for (i, h) in handles.iter().enumerate() {
6449            db.put_cf(h, b"k", format!("v{i}").as_bytes()).unwrap();
6450        }
6451        for (i, h) in handles.iter().enumerate() {
6452            assert_eq!(
6453                db.get_cf(h, b"k").unwrap(),
6454                Some(format!("v{i}").into_bytes())
6455            );
6456        }
6457    }
6458
6459    // ── get_approximate_sizes / get_approximate_memtable_stats ──────────
6460
6461    #[test]
6462    fn test_approximate_sizes_empty_db() {
6463        let (db, _dir) = open_tmp();
6464        let sizes = db.get_approximate_sizes(&[Range::new(b"a", b"z")]);
6465        assert_eq!(sizes, vec![0]);
6466    }
6467
6468    #[test]
6469    fn test_approximate_sizes_empty_range_returns_zero() {
6470        let (db, _dir) = open_tmp();
6471        db.put(b"k", b"v").unwrap();
6472        // Inverted / empty range must not panic and must be 0.
6473        assert_eq!(db.get_approximate_sizes(&[Range::new(b"z", b"a")]), vec![0]);
6474        assert_eq!(db.get_approximate_sizes(&[Range::new(b"k", b"k")]), vec![0]);
6475    }
6476
6477    #[test]
6478    fn test_approximate_memtable_stats_exact_for_memtable() {
6479        let (db, _dir) = open_tmp();
6480        for c in b'a'..=b'e' {
6481            db.put(&[c], b"v").unwrap();
6482        }
6483        let stats = db.get_approximate_memtable_stats(Range::new(b"b", b"e"));
6484        assert_eq!(stats.count, 3, "count must be exact");
6485        // Each entry is [4-byte cf prefix][1-byte key] as
6486        // internal-key + 9-byte seq/type suffix + 1-byte value.
6487        // The size must be strictly > 0 and < (5 full entries * 50).
6488        assert!(stats.size > 0);
6489        assert!(stats.size < 500);
6490    }
6491
6492    #[test]
6493    fn test_approximate_memtable_stats_empty_range() {
6494        let (db, _dir) = open_tmp();
6495        db.put(b"k", b"v").unwrap();
6496        let stats = db.get_approximate_memtable_stats(Range::new(b"m", b"n"));
6497        assert_eq!(stats, MemTableStats::default());
6498    }
6499
6500    #[test]
6501    fn test_approximate_memtable_stats_counts_every_version() {
6502        let (db, _dir) = open_tmp();
6503        db.put(b"k", b"v1").unwrap();
6504        db.put(b"k", b"v2").unwrap();
6505        db.put(b"k", b"v3").unwrap();
6506        let stats = db.get_approximate_memtable_stats(Range::new(b"k", b"l"));
6507        // Three versions of the same user key.
6508        assert_eq!(stats.count, 3);
6509    }
6510
6511    #[test]
6512    fn test_approximate_sizes_after_flush_within_factor_of_2() {
6513        // Write enough data to materialize into L0, then check the
6514        // approximate size against the on-disk file size. The
6515        // accuracy contract is "within a factor of 2".
6516        //
6517        // Use a high-entropy payload so LZ4 can't crush it - a
6518        // zero-filled payload compresses to near-nothing and would
6519        // undercut the accuracy window we're checking.
6520        let dir = TempDir::new().unwrap();
6521        let opts = Options {
6522            write_buffer_size: 4 * 1024,
6523            compression: CompressionType::None,
6524            ..Options::default()
6525        };
6526        let db = Db::open(dir.path(), opts).unwrap();
6527        let payload: Vec<u8> = (0..256).map(|i| (i % 251) as u8).collect();
6528        for i in 0..100 {
6529            db.put(format!("k_{i:04}").as_bytes(), &payload).unwrap();
6530        }
6531        force_flush(&db, "sizes");
6532        let sizes = db.get_approximate_sizes(&[Range::new(b"k_0000", b"k_9999")]);
6533        assert!(sizes[0] > 0, "whole-range size must be > 0 after flush");
6534        // Raw on-disk footprint of the point data: 100 entries,
6535        // each ≈ 256-byte value + ~20-byte key/overhead + a bit
6536        // of block framing, so ~28-30k. The approximation
6537        // includes whole covered blocks, so a 2x window covers
6538        // it comfortably.
6539        let approx = sizes[0];
6540        assert!(approx > 10_000, "approx={approx} too small; expected > 10k");
6541        assert!(
6542            approx < 1_000_000,
6543            "approx={approx} absurdly large; expected < 1M"
6544        );
6545    }
6546
6547    #[test]
6548    fn test_approximate_sizes_multi_range_preserves_order() {
6549        let dir = TempDir::new().unwrap();
6550        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
6551        let payload = vec![0u8; 256];
6552        for i in 0..200 {
6553            db.put(format!("k_{i:04}").as_bytes(), &payload).unwrap();
6554        }
6555        force_flush(&db, "multi");
6556        let ranges = vec![
6557            Range::new(b"k_0000", b"k_0050"),
6558            Range::new(b"k_0050", b"k_0100"),
6559            Range::new(b"k_0100", b"k_0200"),
6560        ];
6561        let sizes = db.get_approximate_sizes(&ranges);
6562        assert_eq!(sizes.len(), 3);
6563        // Every range should contain some bytes.
6564        for (i, &s) in sizes.iter().enumerate() {
6565            assert!(s > 0, "range {i} size was 0");
6566        }
6567    }
6568
6569    #[test]
6570    fn test_approximate_sizes_cf_scoped() {
6571        let (db, _dir) = open_tmp();
6572        let cf = db.create_column_family("scoped").unwrap();
6573        // Put into default CF but not into `scoped` - the
6574        // scoped CF's whole-range size must be 0.
6575        for i in 0..20 {
6576            db.put(format!("k{i}").as_bytes(), b"v").unwrap();
6577        }
6578        let default_sizes = db.get_approximate_sizes(&[Range::new(b"a", b"z")]);
6579        let cf_sizes = db.get_approximate_sizes_cf(&cf, &[Range::new(b"a", b"z")]);
6580        // Memtable contents aren't in approximate_sizes, but they
6581        // aren't on disk either - the default-CF whole-range
6582        // matches the scoped-CF whole-range (both 0) unless a
6583        // flush happened. With default write_buffer_size, 20 small
6584        // writes don't trigger a flush.
6585        assert_eq!(default_sizes[0], 0);
6586        assert_eq!(cf_sizes[0], 0);
6587
6588        // Memtable-stats however sees the default CF entries but
6589        // not the scoped CF.
6590        let default_mt = db.get_approximate_memtable_stats(Range::new(b"a", b"z"));
6591        let cf_mt = db.get_approximate_memtable_stats_cf(&cf, Range::new(b"a", b"z"));
6592        assert_eq!(default_mt.count, 20);
6593        assert_eq!(cf_mt.count, 0);
6594    }
6595
6596    // ── atomic flush across column families ────────────────────────────
6597
6598    #[test]
6599    fn test_atomic_flush_multi_cf_batch_survives_crash() {
6600        // A WriteBatch that touches multiple CFs must be
6601        // all-or-nothing across a crash, even when the write
6602        // lands in the memtable without an explicit flush.
6603        let dir = TempDir::new().unwrap();
6604        {
6605            let db = Db::open(dir.path(), Options::default()).unwrap();
6606            let cf_a = db.create_column_family("a").unwrap();
6607            let cf_b = db.create_column_family("b").unwrap();
6608            let mut batch = WriteBatch::new();
6609            batch.put_cf(&cf_a, b"k1", b"a1");
6610            batch.put_cf(&cf_a, b"k2", b"a2");
6611            batch.put_cf(&cf_b, b"k1", b"b1");
6612            batch.put_cf(&cf_b, b"k2", b"b2");
6613            batch.put(b"default_k", b"default_v");
6614            db.write(batch).unwrap();
6615            // Drop without close - simulate a crash. WAL is the
6616            // source of truth; recovery must restore every key.
6617        }
6618        let db = Db::open(dir.path(), Options::default()).unwrap();
6619        let cf_a = db.column_family("a").expect("cf a survives reopen");
6620        let cf_b = db.column_family("b").expect("cf b survives reopen");
6621        assert_eq!(db.get_cf(&cf_a, b"k1").unwrap(), Some(b"a1".to_vec()));
6622        assert_eq!(db.get_cf(&cf_a, b"k2").unwrap(), Some(b"a2".to_vec()));
6623        assert_eq!(db.get_cf(&cf_b, b"k1").unwrap(), Some(b"b1".to_vec()));
6624        assert_eq!(db.get_cf(&cf_b, b"k2").unwrap(), Some(b"b2".to_vec()));
6625        assert_eq!(db.get(b"default_k").unwrap(), Some(b"default_v".to_vec()));
6626    }
6627
6628    #[test]
6629    fn test_atomic_flush_cross_cf_survives_rotate_and_flush() {
6630        // Drive the memtable past its flush threshold while a
6631        // multi-CF batch is in flight. The rotated memtable
6632        // produces one L0 SSTable that contains every CF's half
6633        // of the batch atomically.
6634        let dir = TempDir::new().unwrap();
6635        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
6636        let cf_a = db.create_column_family("a").unwrap();
6637        let cf_b = db.create_column_family("b").unwrap();
6638
6639        // Seed enough filler to push the tiny 4KB buffer over
6640        // on the next write.
6641        for i in 0..16 {
6642            db.put_cf(&cf_a, format!("fill_a_{i:02}").as_bytes(), &[0u8; 256])
6643                .unwrap();
6644            db.put_cf(&cf_b, format!("fill_b_{i:02}").as_bytes(), &[0u8; 256])
6645                .unwrap();
6646        }
6647
6648        let mut batch = WriteBatch::new();
6649        batch.put_cf(&cf_a, b"pivot", b"A_PIVOT");
6650        batch.put_cf(&cf_b, b"pivot", b"B_PIVOT");
6651        db.write(batch).unwrap();
6652        force_flush(&db, "atomic");
6653
6654        assert_eq!(
6655            db.get_cf(&cf_a, b"pivot").unwrap(),
6656            Some(b"A_PIVOT".to_vec())
6657        );
6658        assert_eq!(
6659            db.get_cf(&cf_b, b"pivot").unwrap(),
6660            Some(b"B_PIVOT".to_vec())
6661        );
6662    }
6663
6664    #[test]
6665    fn test_atomic_flush_empty_cf_mixed_with_populated() {
6666        // Creating a CF and leaving it empty while another CF
6667        // gets flushed must not corrupt the empty CF.
6668        let dir = TempDir::new().unwrap();
6669        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
6670        let cf_populated = db.create_column_family("populated").unwrap();
6671        let cf_empty = db.create_column_family("empty").unwrap();
6672
6673        for i in 0..50 {
6674            db.put_cf(&cf_populated, format!("k{i:02}").as_bytes(), b"v")
6675                .unwrap();
6676        }
6677        force_flush(&db, "empty_mix");
6678
6679        for i in 0..50 {
6680            assert_eq!(
6681                db.get_cf(&cf_populated, format!("k{i:02}").as_bytes())
6682                    .unwrap(),
6683                Some(b"v".to_vec())
6684            );
6685        }
6686        assert_eq!(db.get_cf(&cf_empty, b"anything").unwrap(), None);
6687
6688        // The empty CF still accepts new writes after the flush.
6689        db.put_cf(&cf_empty, b"new", b"fresh").unwrap();
6690        assert_eq!(
6691            db.get_cf(&cf_empty, b"new").unwrap(),
6692            Some(b"fresh".to_vec())
6693        );
6694    }
6695
6696    #[test]
6697    fn test_atomic_flush_option_accepted() {
6698        // The flag is a no-op for API parity - both values must
6699        // open cleanly and produce the same atomic behavior.
6700        let dir = TempDir::new().unwrap();
6701        let opts = Options {
6702            atomic_flush: true,
6703            ..Options::default()
6704        };
6705        let db = Db::open(dir.path(), opts).unwrap();
6706        let cf = db.create_column_family("cf1").unwrap();
6707        db.put_cf(&cf, b"k", b"v").unwrap();
6708        assert_eq!(db.get_cf(&cf, b"k").unwrap(), Some(b"v".to_vec()));
6709    }
6710
6711    #[test]
6712    fn test_atomic_flush_close_with_pending_multi_cf_writes() {
6713        // A clean close with pending multi-CF writes must flush
6714        // the active memtable to L0 before returning, so every
6715        // CF's state is durable on reopen.
6716        let dir = TempDir::new().unwrap();
6717        {
6718            let db = Db::open(dir.path(), Options::default()).unwrap();
6719            let cf_a = db.create_column_family("a").unwrap();
6720            let cf_b = db.create_column_family("b").unwrap();
6721            let mut batch = WriteBatch::new();
6722            batch.put_cf(&cf_a, b"k", b"a");
6723            batch.put_cf(&cf_b, b"k", b"b");
6724            db.write(batch).unwrap();
6725            db.close().unwrap();
6726        }
6727        let db = Db::open(dir.path(), Options::default()).unwrap();
6728        let cf_a = db.column_family("a").unwrap();
6729        let cf_b = db.column_family("b").unwrap();
6730        assert_eq!(db.get_cf(&cf_a, b"k").unwrap(), Some(b"a".to_vec()));
6731        assert_eq!(db.get_cf(&cf_b, b"k").unwrap(), Some(b"b".to_vec()));
6732    }
6733
6734    // ── event listeners ─────────────────────────────────────────────────
6735
6736    /// Test listener that counts every callback it receives and
6737    /// records enough detail for assertions.
6738    #[derive(Default)]
6739    struct CountingListener {
6740        flush_completed: AtomicUsize,
6741        compaction_begin: AtomicUsize,
6742        compaction_completed: AtomicUsize,
6743        table_file_created: AtomicUsize,
6744        table_file_deleted: AtomicUsize,
6745        external_file_ingested: AtomicUsize,
6746        background_error: AtomicUsize,
6747        last_flush_file_id: AtomicUsize,
6748        last_compaction_output_count: AtomicUsize,
6749    }
6750
6751    impl EventListener for CountingListener {
6752        fn on_flush_completed(&self, info: &FlushJobInfo) {
6753            self.flush_completed.fetch_add(1, AtomicOrdering::Relaxed);
6754            self.last_flush_file_id
6755                .store(info.file_id as usize, AtomicOrdering::Relaxed);
6756        }
6757        fn on_compaction_begin(&self, _info: &CompactionJobInfo) {
6758            self.compaction_begin.fetch_add(1, AtomicOrdering::Relaxed);
6759        }
6760        fn on_compaction_completed(&self, info: &CompactionJobInfo) {
6761            self.compaction_completed
6762                .fetch_add(1, AtomicOrdering::Relaxed);
6763            self.last_compaction_output_count
6764                .store(info.output_files.len(), AtomicOrdering::Relaxed);
6765        }
6766        fn on_table_file_created(&self, _info: &TableFileCreationInfo) {
6767            self.table_file_created
6768                .fetch_add(1, AtomicOrdering::Relaxed);
6769        }
6770        fn on_table_file_deleted(&self, _info: &TableFileDeletionInfo) {
6771            self.table_file_deleted
6772                .fetch_add(1, AtomicOrdering::Relaxed);
6773        }
6774        fn on_external_file_ingested(&self, _info: &ExternalFileIngestionInfo) {
6775            self.external_file_ingested
6776                .fetch_add(1, AtomicOrdering::Relaxed);
6777        }
6778        fn on_background_error(&self, _reason: BackgroundErrorReason, _err: &Error) {
6779            self.background_error.fetch_add(1, AtomicOrdering::Relaxed);
6780        }
6781    }
6782
6783    #[test]
6784    fn test_listener_fires_on_flush() {
6785        let listener = Arc::new(CountingListener::default());
6786        let dir = TempDir::new().unwrap();
6787        let opts = Options {
6788            write_buffer_size: 4 * 1024,
6789            listeners: vec![listener.clone() as Arc<dyn EventListener>],
6790            ..Options::default()
6791        };
6792        let db = Db::open(dir.path(), opts).unwrap();
6793        force_flush(&db, "listener");
6794
6795        assert!(
6796            listener.flush_completed.load(AtomicOrdering::Relaxed) >= 1,
6797            "flush callback should fire at least once"
6798        );
6799        assert!(
6800            listener.table_file_created.load(AtomicOrdering::Relaxed) >= 1,
6801            "table_file_created should fire for every flushed file"
6802        );
6803        assert_ne!(
6804            listener.last_flush_file_id.load(AtomicOrdering::Relaxed),
6805            0,
6806            "file id recorded"
6807        );
6808    }
6809
6810    #[test]
6811    fn test_listener_fires_on_compaction() {
6812        let listener = Arc::new(CountingListener::default());
6813        let dir = TempDir::new().unwrap();
6814        let opts = Options {
6815            write_buffer_size: 4 * 1024,
6816            listeners: vec![listener.clone() as Arc<dyn EventListener>],
6817            ..Options::default()
6818        };
6819        let db = Db::open(dir.path(), opts).unwrap();
6820        // Drive enough writes to generate L0 files, then manually
6821        // compact the range so the compaction callbacks fire on
6822        // the calling thread.
6823        for i in 0..400 {
6824            db.put(format!("k_{i:04}").as_bytes(), b"v").unwrap();
6825        }
6826        force_flush(&db, "listener");
6827        db.compact_range(None, None).unwrap();
6828
6829        let begin = listener.compaction_begin.load(AtomicOrdering::Relaxed);
6830        let complete = listener.compaction_completed.load(AtomicOrdering::Relaxed);
6831        assert!(
6832            begin >= 1,
6833            "compaction_begin must fire at least once, got {begin}"
6834        );
6835        assert_eq!(
6836            begin, complete,
6837            "begin and completed must fire in matched pairs"
6838        );
6839        assert!(
6840            listener.table_file_created.load(AtomicOrdering::Relaxed) >= 2,
6841            "flush + compaction both produce files"
6842        );
6843        assert!(
6844            listener.table_file_deleted.load(AtomicOrdering::Relaxed) >= 1,
6845            "old L0 files must be unlinked after compaction"
6846        );
6847    }
6848
6849    #[test]
6850    fn test_listener_fires_on_ingest() {
6851        let listener = Arc::new(CountingListener::default());
6852        let dir = TempDir::new().unwrap();
6853        let opts = Options {
6854            listeners: vec![listener.clone() as Arc<dyn EventListener>],
6855            ..Options::default()
6856        };
6857        let db = Db::open(dir.path(), opts.clone()).unwrap();
6858
6859        let sst_path = dir.path().join("ingest.sst");
6860        {
6861            let mut w = SstFileWriter::create(&sst_path, &opts).unwrap();
6862            for i in 0..10 {
6863                w.put(format!("ik_{i:02}").as_bytes(), b"iv").unwrap();
6864            }
6865            w.finish().unwrap();
6866        }
6867        db.ingest_external_files(&[sst_path], IngestOptions::default())
6868            .unwrap();
6869
6870        assert_eq!(
6871            listener
6872                .external_file_ingested
6873                .load(AtomicOrdering::Relaxed),
6874            1,
6875            "external_file_ingested fires once per ingested file"
6876        );
6877        assert!(
6878            listener.table_file_created.load(AtomicOrdering::Relaxed) >= 1,
6879            "ingest re-emits the file and fires table_file_created"
6880        );
6881    }
6882
6883    #[test]
6884    fn test_listener_multiple_listeners_all_fire() {
6885        let a = Arc::new(CountingListener::default());
6886        let b = Arc::new(CountingListener::default());
6887        let dir = TempDir::new().unwrap();
6888        let opts = Options {
6889            write_buffer_size: 4 * 1024,
6890            listeners: vec![
6891                a.clone() as Arc<dyn EventListener>,
6892                b.clone() as Arc<dyn EventListener>,
6893            ],
6894            ..Options::default()
6895        };
6896        let db = Db::open(dir.path(), opts).unwrap();
6897        force_flush(&db, "multi");
6898
6899        assert!(a.flush_completed.load(AtomicOrdering::Relaxed) >= 1);
6900        assert!(b.flush_completed.load(AtomicOrdering::Relaxed) >= 1);
6901    }
6902
6903    #[test]
6904    fn test_listener_none_configured_is_noop() {
6905        // Sanity check: with no listeners, all paths still work
6906        // and nothing panics.
6907        let (db, _dir) = open_tmp();
6908        db.put(b"k", b"v").unwrap();
6909        force_flush(&db, "none");
6910        db.compact_range(None, None).unwrap();
6911    }
6912
6913    #[test]
6914    fn test_listener_compaction_job_info_contains_input_files() {
6915        // Capture the last CompactionJobInfo on `on_compaction_completed`
6916        // and assert it carries the expected input file ids.
6917        struct CaptureListener {
6918            captured: Mutex<Option<CompactionJobInfo>>,
6919        }
6920        impl EventListener for CaptureListener {
6921            fn on_compaction_completed(&self, info: &CompactionJobInfo) {
6922                *self.captured.lock() = Some(info.clone());
6923            }
6924        }
6925        use crate::sync::Mutex;
6926
6927        let listener = Arc::new(CaptureListener {
6928            captured: Mutex::new(None),
6929        });
6930        let dir = TempDir::new().unwrap();
6931        let opts = Options {
6932            write_buffer_size: 4 * 1024,
6933            listeners: vec![listener.clone() as Arc<dyn EventListener>],
6934            ..Options::default()
6935        };
6936        let db = Db::open(dir.path(), opts).unwrap();
6937        for i in 0..400 {
6938            db.put(format!("k_{i:04}").as_bytes(), b"v").unwrap();
6939        }
6940        force_flush(&db, "capture");
6941        db.compact_range(None, None).unwrap();
6942
6943        let captured = listener.captured.lock().clone();
6944        let info = captured.expect("compaction_completed must have fired");
6945        assert!(
6946            !info.input_files_input_level.is_empty(),
6947            "at least one L0 input file was picked"
6948        );
6949        assert_eq!(info.output_level, info.input_level + 1);
6950        assert!(!info.output_files.is_empty(), "compaction produced outputs");
6951    }
6952
6953    // ── statistics ──────────────────────────────────────────────────────
6954
6955    fn stats_opts(stats: Arc<Statistics>) -> Options {
6956        Options {
6957            statistics: Some(stats),
6958            ..Options::default()
6959        }
6960    }
6961
6962    fn tiny_flush_stats_opts(stats: Arc<Statistics>) -> Options {
6963        Options {
6964            write_buffer_size: 4 * 1024,
6965            statistics: Some(stats),
6966            ..Options::default()
6967        }
6968    }
6969
6970    #[test]
6971    fn test_stats_keys_written_and_bytes_written() {
6972        let stats = Arc::new(Statistics::new());
6973        let dir = TempDir::new().unwrap();
6974        let db = Db::open(dir.path(), stats_opts(stats.clone())).unwrap();
6975        db.put(b"k1", b"value1").unwrap();
6976        db.put(b"k2", b"value2").unwrap();
6977        assert_eq!(stats.get_ticker(Ticker::KeysWritten), 2);
6978        // Expected bytes = 2 + 6 + 2 + 6 = 16
6979        assert_eq!(stats.get_ticker(Ticker::BytesWritten), 16);
6980    }
6981
6982    #[test]
6983    fn test_stats_keys_read_and_bytes_read() {
6984        let stats = Arc::new(Statistics::new());
6985        let dir = TempDir::new().unwrap();
6986        let db = Db::open(dir.path(), stats_opts(stats.clone())).unwrap();
6987        db.put(b"k", b"value").unwrap();
6988        db.get(b"k").unwrap();
6989        db.get(b"missing").unwrap();
6990        assert_eq!(stats.get_ticker(Ticker::KeysRead), 2);
6991        // Only the found value contributes to BytesRead.
6992        assert_eq!(stats.get_ticker(Ticker::BytesRead), 5);
6993        let get_hist = stats.get_histogram_snapshot(Histogram::DbGet);
6994        assert_eq!(get_hist.count, 2);
6995    }
6996
6997    #[test]
6998    fn test_stats_delete_counter() {
6999        let stats = Arc::new(Statistics::new());
7000        let dir = TempDir::new().unwrap();
7001        let db = Db::open(dir.path(), stats_opts(stats.clone())).unwrap();
7002        db.put(b"k", b"v").unwrap();
7003        db.delete(b"k").unwrap();
7004        assert_eq!(stats.get_ticker(Ticker::KeysDeleted), 1);
7005    }
7006
7007    #[test]
7008    fn test_stats_block_cache_hit_and_miss_populate() {
7009        // After a deterministic flush + compact_range (so no
7010        // concurrent background compaction can race the reads
7011        // and contaminate the counters), every point lookup
7012        // that reaches a data block fires either a hit or a
7013        // miss on the block cache. We don't assert the strict
7014        // `adds == misses` invariant here - LRU eviction plus
7015        // any lingering background work can perturb that
7016        // equality on fast machines. The weaker "both hits and
7017        // misses see traffic" is the observable contract.
7018        let stats = Arc::new(Statistics::new());
7019        let dir = TempDir::new().unwrap();
7020        let db = Db::open(dir.path(), tiny_flush_stats_opts(stats.clone())).unwrap();
7021        for i in 0..200 {
7022            db.put(format!("k_{i:05}").as_bytes(), b"value").unwrap();
7023        }
7024        force_flush(&db, "cache");
7025        // Drain any pending compaction before measuring.
7026        db.compact_range(None, None).unwrap();
7027        stats.reset();
7028        // Read the same few keys twice: the first read is a
7029        // miss + add, the second is a hit.
7030        for _ in 0..2 {
7031            for i in 0..5 {
7032                db.get(format!("k_{i:05}").as_bytes()).unwrap();
7033            }
7034        }
7035        let hits = stats.get_ticker(Ticker::BlockCacheHit);
7036        let misses = stats.get_ticker(Ticker::BlockCacheMiss);
7037        let adds = stats.get_ticker(Ticker::BlockCacheAdd);
7038        assert!(misses > 0, "expected at least one block cache miss");
7039        assert!(hits > 0, "expected at least one block cache hit");
7040        // `adds` tracks inserts after a miss - it can never
7041        // exceed `misses`.
7042        assert!(adds <= misses, "adds={adds} misses={misses}");
7043    }
7044
7045    #[test]
7046    fn test_stats_bloom_filter_useful_increments_on_absent_key() {
7047        // Deterministic layout: write 200 keys spaced on even
7048        // suffixes (so the resulting SST covers `[k_00000,
7049        // k_00398]`), compact to L1, then query odd suffixes
7050        // within that range. The partition_point-based file
7051        // lookup lands on the single L1 file for every query
7052        // and the bloom has a chance to say "not present".
7053        let stats = Arc::new(Statistics::new());
7054        let dir = TempDir::new().unwrap();
7055        let db = Db::open(dir.path(), tiny_flush_stats_opts(stats.clone())).unwrap();
7056        for i in 0..200 {
7057            let even = i * 2;
7058            db.put(format!("k_{even:05}").as_bytes(), b"v").unwrap();
7059        }
7060        force_flush(&db, "bloom");
7061        db.compact_range(None, None).unwrap();
7062        stats.reset();
7063        // Query 100 absent (odd-suffix) keys inside the range.
7064        // With ~10 bits/key the false-positive rate is ~1%, so
7065        // almost all queries will register as "useful".
7066        for i in 0..100 {
7067            let odd = i * 2 + 1;
7068            db.get(format!("k_{odd:05}").as_bytes()).unwrap();
7069        }
7070        let useful = stats.get_ticker(Ticker::BloomFilterUseful);
7071        assert!(
7072            useful > 0,
7073            "bloom filter should have ruled out at least one absent key"
7074        );
7075    }
7076
7077    #[test]
7078    fn test_stats_bloom_filter_full_positive_on_present_key() {
7079        let stats = Arc::new(Statistics::new());
7080        let dir = TempDir::new().unwrap();
7081        let db = Db::open(dir.path(), tiny_flush_stats_opts(stats.clone())).unwrap();
7082        for i in 0..100 {
7083            db.put(format!("k_{i:04}").as_bytes(), b"v").unwrap();
7084        }
7085        force_flush(&db, "bloom_pos");
7086        db.compact_range(None, None).unwrap();
7087        stats.reset();
7088        for i in 0..100 {
7089            db.get(format!("k_{i:04}").as_bytes()).unwrap();
7090        }
7091        let pos = stats.get_ticker(Ticker::BloomFilterFullPositive);
7092        assert!(
7093            pos > 0,
7094            "bloom filter should have returned 'maybe' and we found the key"
7095        );
7096    }
7097
7098    #[test]
7099    fn test_stats_flush_and_compaction_counters() {
7100        let stats = Arc::new(Statistics::new());
7101        let dir = TempDir::new().unwrap();
7102        let db = Db::open(dir.path(), tiny_flush_stats_opts(stats.clone())).unwrap();
7103        for i in 0..200 {
7104            db.put(format!("k_{i:04}").as_bytes(), b"v").unwrap();
7105        }
7106        force_flush(&db, "fcstats");
7107        db.compact_range(None, None).unwrap();
7108        assert!(stats.get_ticker(Ticker::FlushCount) >= 1);
7109        assert!(stats.get_ticker(Ticker::FlushBytesWritten) > 0);
7110        assert!(stats.get_ticker(Ticker::CompactionCount) >= 1);
7111        assert!(stats.get_ticker(Ticker::CompactionBytesRead) > 0);
7112        assert!(stats.get_ticker(Ticker::CompactionBytesWritten) > 0);
7113        assert!(stats.get_histogram_snapshot(Histogram::FlushTime).count > 0);
7114        assert!(
7115            stats
7116                .get_histogram_snapshot(Histogram::CompactionTime)
7117                .count
7118                > 0
7119        );
7120    }
7121
7122    #[test]
7123    fn test_stats_wal_counters() {
7124        let stats = Arc::new(Statistics::new());
7125        let dir = TempDir::new().unwrap();
7126        let opts = Options {
7127            statistics: Some(stats.clone()),
7128            durability: DurabilityMode::Immediate,
7129            ..Options::default()
7130        };
7131        let db = Db::open(dir.path(), opts).unwrap();
7132        db.put(b"k", b"v").unwrap();
7133        db.put(b"k2", b"v2").unwrap();
7134        assert!(stats.get_ticker(Ticker::WalBytesWritten) > 0);
7135        // Immediate durability fsyncs per call.
7136        assert_eq!(stats.get_ticker(Ticker::WalSyncCount), 2);
7137        assert!(stats.get_histogram_snapshot(Histogram::WalWriteTime).count >= 2);
7138    }
7139
7140    #[test]
7141    fn test_stats_iter_seek_and_next_counters() {
7142        let stats = Arc::new(Statistics::new());
7143        let dir = TempDir::new().unwrap();
7144        let db = Db::open(dir.path(), stats_opts(stats.clone())).unwrap();
7145        db.put(b"a", b"1").unwrap();
7146        db.put(b"b", b"2").unwrap();
7147        db.put(b"c", b"3").unwrap();
7148        let mut it = db.iter();
7149        it.seek_to_first();
7150        while it.valid() {
7151            it.next();
7152        }
7153        assert!(stats.get_ticker(Ticker::IterSeekCount) >= 1);
7154        // Two `next` calls produced keys (b, c); the third
7155        // invalidated and doesn't count.
7156        assert_eq!(stats.get_ticker(Ticker::IterNextCount), 2);
7157    }
7158
7159    #[test]
7160    fn test_stats_snapshot_register_release() {
7161        let stats = Arc::new(Statistics::new());
7162        let dir = TempDir::new().unwrap();
7163        let db = Db::open(dir.path(), stats_opts(stats.clone())).unwrap();
7164        {
7165            let _snap = db.snapshot();
7166        }
7167        assert_eq!(stats.get_ticker(Ticker::SnapshotsRegistered), 1);
7168        assert_eq!(stats.get_ticker(Ticker::SnapshotsReleased), 1);
7169    }
7170
7171    #[test]
7172    fn test_stats_reset_clears_everything() {
7173        let stats = Arc::new(Statistics::new());
7174        let dir = TempDir::new().unwrap();
7175        let db = Db::open(dir.path(), stats_opts(stats.clone())).unwrap();
7176        db.put(b"k", b"v").unwrap();
7177        assert!(stats.get_ticker(Ticker::KeysWritten) > 0);
7178        stats.reset();
7179        assert_eq!(stats.get_ticker(Ticker::KeysWritten), 0);
7180        assert_eq!(stats.get_ticker(Ticker::BytesWritten), 0);
7181    }
7182
7183    #[test]
7184    fn test_stats_none_configured_is_noop() {
7185        // Sanity: with statistics disabled every hot path still
7186        // works and nothing panics.
7187        let (db, _dir) = open_tmp();
7188        db.put(b"k", b"v").unwrap();
7189        assert_eq!(db.get(b"k").unwrap(), Some(b"v".to_vec()));
7190    }
7191
7192    #[test]
7193    fn test_stats_dump_is_non_empty_and_contains_every_ticker() {
7194        let stats = Arc::new(Statistics::new());
7195        let dir = TempDir::new().unwrap();
7196        let db = Db::open(dir.path(), stats_opts(stats.clone())).unwrap();
7197        db.put(b"k", b"v").unwrap();
7198        let dump = stats.dump();
7199        for ticker_name in [
7200            "regolith.bytes_written",
7201            "regolith.keys_written",
7202            "regolith.bloom_filter_useful",
7203            "regolith.compaction_count",
7204            "regolith.flush_count",
7205        ] {
7206            assert!(dump.contains(ticker_name), "dump missing {ticker_name}");
7207        }
7208    }
7209
7210    // ── properties API ──────────────────────────────────────────────────
7211
7212    #[test]
7213    fn test_property_unknown_name_returns_none() {
7214        let (db, _dir) = open_tmp();
7215        assert!(db.get_property("not.a.real.property").is_none());
7216        assert!(db.get_int_property("not.a.real.property").is_none());
7217    }
7218
7219    #[test]
7220    fn test_property_num_files_at_level() {
7221        let (db, _dir) = open_tmp();
7222        assert_eq!(db.get_int_property("regolith.num-files-at-level0"), Some(0));
7223        assert_eq!(db.get_int_property("regolith.num-files-at-level6"), Some(0));
7224        // Out-of-range level is a valid query that returns 0.
7225        assert_eq!(
7226            db.get_int_property("regolith.num-files-at-level99"),
7227            Some(0)
7228        );
7229    }
7230
7231    #[test]
7232    fn test_property_level_counts_after_flush_and_compact() {
7233        let dir = TempDir::new().unwrap();
7234        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
7235        for i in 0..200 {
7236            db.put(format!("k_{i:04}").as_bytes(), b"v").unwrap();
7237        }
7238        force_flush(&db, "props");
7239        // At this point we expect some L0 files.
7240        let l0_before = db.get_int_property("regolith.num-files-at-level0").unwrap();
7241        assert!(l0_before > 0 || db.get_int_property("regolith.num-files-at-level1").unwrap() > 0);
7242
7243        // Drain everything to the deepest level.
7244        db.compact_range(None, None).unwrap();
7245        assert_eq!(
7246            db.get_int_property("regolith.num-files-at-level0"),
7247            Some(0),
7248            "L0 should be empty after compact_range"
7249        );
7250    }
7251
7252    #[test]
7253    fn test_property_total_sst_size_after_flush() {
7254        let dir = TempDir::new().unwrap();
7255        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
7256        assert_eq!(
7257            db.get_int_property("regolith.total-sst-files-size"),
7258            Some(0)
7259        );
7260        for i in 0..100 {
7261            db.put(format!("k_{i:04}").as_bytes(), b"v").unwrap();
7262        }
7263        force_flush(&db, "size");
7264        let size = db
7265            .get_int_property("regolith.total-sst-files-size")
7266            .unwrap();
7267        assert!(size > 0, "SST size should be > 0 after a flush");
7268    }
7269
7270    #[test]
7271    fn test_property_cur_size_active_mem_table() {
7272        let (db, _dir) = open_tmp();
7273        assert_eq!(
7274            db.get_int_property("regolith.cur-size-active-mem-table"),
7275            Some(0)
7276        );
7277        for i in 0..50 {
7278            db.put(format!("k_{i:03}").as_bytes(), b"value").unwrap();
7279        }
7280        let size = db
7281            .get_int_property("regolith.cur-size-active-mem-table")
7282            .unwrap();
7283        assert!(size > 0, "active memtable should have non-zero size");
7284    }
7285
7286    #[test]
7287    fn test_property_cur_size_all_mem_tables_aggregates() {
7288        let (db, _dir) = open_tmp();
7289        db.put(b"k", b"v").unwrap();
7290        let active = db
7291            .get_int_property("regolith.cur-size-active-mem-table")
7292            .unwrap();
7293        let all = db
7294            .get_int_property("regolith.cur-size-all-mem-tables")
7295            .unwrap();
7296        assert!(all >= active, "all mem tables must be >= active");
7297    }
7298
7299    #[test]
7300    fn test_property_estimate_num_keys() {
7301        let dir = TempDir::new().unwrap();
7302        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
7303        for i in 0..100 {
7304            db.put(format!("k_{i:04}").as_bytes(), b"v").unwrap();
7305        }
7306        force_flush(&db, "estimate");
7307        db.compact_range(None, None).unwrap();
7308        let estimate = db.get_int_property("regolith.estimate-num-keys").unwrap();
7309        // Exact count per SST includes the flush filler + the 100
7310        // writes; the property is a lower bound, so > 50 is a
7311        // safe floor.
7312        assert!(estimate > 50, "estimate-num-keys={estimate} too low");
7313    }
7314
7315    #[test]
7316    fn test_property_num_snapshots_and_oldest_snapshot_time() {
7317        let (db, _dir) = open_tmp();
7318        assert_eq!(db.get_int_property("regolith.num-snapshots"), Some(0));
7319        assert!(
7320            db.get_int_property("regolith.oldest-snapshot-time")
7321                .is_none(),
7322            "oldest-snapshot-time should be None when no snapshots are live"
7323        );
7324        let _snap_a = db.snapshot();
7325        let _snap_b = db.snapshot();
7326        assert_eq!(db.get_int_property("regolith.num-snapshots"), Some(2));
7327        assert!(
7328            db.get_int_property("regolith.oldest-snapshot-time")
7329                .is_some()
7330        );
7331    }
7332
7333    #[test]
7334    fn test_property_background_errors_returns_zero() {
7335        let (db, _dir) = open_tmp();
7336        // No background errors on a fresh db.
7337        assert_eq!(db.get_int_property("regolith.background-errors"), Some(0));
7338    }
7339
7340    #[test]
7341    fn test_property_stats_string_includes_level_header_and_counters() {
7342        let stats = Arc::new(Statistics::new());
7343        let opts = Options {
7344            statistics: Some(stats),
7345            ..Options::default()
7346        };
7347        let dir = TempDir::new().unwrap();
7348        let db = Db::open(dir.path(), opts).unwrap();
7349        db.put(b"k", b"v").unwrap();
7350        let text = db.get_property("regolith.stats").unwrap();
7351        assert!(text.contains("== regolith engine stats =="));
7352        assert!(text.contains("Level  Files     Size(B)"));
7353        assert!(text.contains("regolith.keys_written"));
7354    }
7355
7356    #[test]
7357    fn test_property_stats_string_without_statistics_configured() {
7358        let (db, _dir) = open_tmp();
7359        let text = db.get_property("regolith.stats").unwrap();
7360        assert!(text.contains("== regolith engine stats =="));
7361        assert!(text.contains("(no Statistics object configured"));
7362    }
7363
7364    #[test]
7365    fn test_property_sstables_lists_files_after_flush() {
7366        let dir = TempDir::new().unwrap();
7367        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
7368        for i in 0..50 {
7369            db.put(format!("k_{i:03}").as_bytes(), b"v").unwrap();
7370        }
7371        force_flush(&db, "ssts");
7372        let text = db.get_property("regolith.sstables").unwrap();
7373        assert!(text.contains("Level    FileID"));
7374        // Should list at least one file with non-zero size.
7375        assert!(
7376            text.lines().any(|l| l.contains("\"k_")),
7377            "expected a file line to include a user key from the writes"
7378        );
7379    }
7380
7381    #[test]
7382    fn test_property_levelstats_format() {
7383        let dir = TempDir::new().unwrap();
7384        let db = Db::open(dir.path(), tiny_flush_opts()).unwrap();
7385        for i in 0..20 {
7386            db.put(format!("k_{i:03}").as_bytes(), b"v").unwrap();
7387        }
7388        force_flush(&db, "lvl");
7389        let text = db.get_property("regolith.levelstats").unwrap();
7390        assert!(text.starts_with("Level  Files     Size(B)"));
7391        // Every level row is present, not just the populated ones.
7392        for lvl in 0..7 {
7393            assert!(
7394                text.contains(&format!("{lvl:5}")),
7395                "level {lvl} should appear in levelstats"
7396            );
7397        }
7398    }
7399
7400    #[test]
7401    fn test_property_options_debug_dump() {
7402        let (db, _dir) = open_tmp();
7403        let text = db.get_property("regolith.options").unwrap();
7404        assert!(text.contains("OptionsSnapshot"));
7405        assert!(text.contains("default"));
7406    }
7407
7408    #[test]
7409    fn test_property_integer_forms_available_via_get_property() {
7410        // Integer properties should also be reachable via
7411        // get_property, returning their decimal string form.
7412        let (db, _dir) = open_tmp();
7413        assert_eq!(
7414            db.get_property("regolith.num-files-at-level0").as_deref(),
7415            Some("0")
7416        );
7417        assert_eq!(
7418            db.get_property("regolith.num-snapshots").as_deref(),
7419            Some("0")
7420        );
7421    }
7422
7423    #[test]
7424    fn test_multi_worker_compaction_reads_are_correct() {
7425        // With 4 background workers, heavy writes produce many L0
7426        // files that trigger multiple concurrent L1+ compactions.
7427        // Every key must still read back its latest value after
7428        // the dust settles.
7429        let opts = Options {
7430            write_buffer_size: 4 * 1024,
7431            max_background_compactions: 4,
7432            l0_compaction_trigger: 2,
7433            target_file_size: 8 * 1024,
7434            ..Options::default()
7435        };
7436        let dir = TempDir::new().unwrap();
7437        let db = Db::open(dir.path(), opts).unwrap();
7438
7439        let mut expected = std::collections::BTreeMap::new();
7440        for i in 0..2048 {
7441            let k = format!("k{i:06}");
7442            let v = format!("v{i}");
7443            db.put(k.as_bytes(), v.as_bytes()).unwrap();
7444            expected.insert(k, v);
7445        }
7446        // Overwrite a window to exercise dedup across workers.
7447        for i in 100..300 {
7448            let k = format!("k{i:06}");
7449            let v = format!("v{i}-new");
7450            db.put(k.as_bytes(), v.as_bytes()).unwrap();
7451            expected.insert(k, v);
7452        }
7453
7454        // Give background workers time to process L0 files.
7455        // Use a generous sleep so slow CI runners don't flake.
7456        std::thread::sleep(std::time::Duration::from_millis(500));
7457        db.compact_range(None, None).unwrap();
7458
7459        for (k, v) in &expected {
7460            assert_eq!(
7461                db.get(k.as_bytes()).unwrap(),
7462                Some(v.as_bytes().to_vec()),
7463                "key {k} must read back its latest value"
7464            );
7465        }
7466    }
7467
7468    #[test]
7469    fn test_multi_worker_single_thread_matches_default() {
7470        // max_background_compactions=1 must behave identically to
7471        // the default (which is also 1). Sanity check that the
7472        // RwLock path doesn't break the single-worker case.
7473        let opts = Options {
7474            write_buffer_size: 4 * 1024,
7475            max_background_compactions: 1,
7476            ..Options::default()
7477        };
7478        let dir = TempDir::new().unwrap();
7479        let db = Db::open(dir.path(), opts).unwrap();
7480        for i in 0..500 {
7481            let k = format!("k{i:04}");
7482            db.put(k.as_bytes(), b"v").unwrap();
7483        }
7484        db.compact_range(None, None).unwrap();
7485        for i in 0..500 {
7486            let k = format!("k{i:04}");
7487            assert_eq!(db.get(k.as_bytes()).unwrap(), Some(b"v".to_vec()));
7488        }
7489    }
7490
7491    #[test]
7492    fn test_partitioned_index_reads_are_correct() {
7493        // Enable partitioned index with a tiny metadata_block_size
7494        // so the test actually exercises the two-level path.
7495        let opts = Options {
7496            write_buffer_size: 4 * 1024,
7497            partitioned_index: true,
7498            metadata_block_size: 128,
7499            ..Options::default()
7500        };
7501        let dir = TempDir::new().unwrap();
7502        let db = Db::open(dir.path(), opts).unwrap();
7503
7504        for i in 0..500 {
7505            let k = format!("k{i:04}");
7506            let v = format!("v{i}");
7507            db.put(k.as_bytes(), v.as_bytes()).unwrap();
7508        }
7509        db.compact_range(None, None).unwrap();
7510
7511        for i in 0..500 {
7512            let k = format!("k{i:04}");
7513            let v = format!("v{i}");
7514            assert_eq!(
7515                db.get(k.as_bytes()).unwrap(),
7516                Some(v.into_bytes()),
7517                "key {k} must read back correctly with partitioned index"
7518            );
7519        }
7520    }
7521
7522    #[test]
7523    fn test_partitioned_index_scan_matches_flat() {
7524        // Write the same data with and without partitioned index.
7525        // Scans must produce identical results.
7526        let write_and_scan = |partitioned: bool| -> Vec<(Vec<u8>, Vec<u8>)> {
7527            let dir = TempDir::new().unwrap();
7528            let opts = Options {
7529                write_buffer_size: 4 * 1024,
7530                partitioned_index: partitioned,
7531                metadata_block_size: 128,
7532                ..Options::default()
7533            };
7534            let db = Db::open(dir.path(), opts).unwrap();
7535            for i in 0..200 {
7536                let k = format!("k{i:04}");
7537                let v = format!("v{i}");
7538                db.put(k.as_bytes(), v.as_bytes()).unwrap();
7539            }
7540            db.compact_range(None, None).unwrap();
7541            db.scan(None, None).unwrap()
7542        };
7543        let flat = write_and_scan(false);
7544        let partitioned = write_and_scan(true);
7545        assert_eq!(flat, partitioned, "partitioned scan must match flat scan");
7546    }
7547
7548    #[test]
7549    fn test_partitioned_index_survives_reopen() {
7550        let dir = TempDir::new().unwrap();
7551        let opts = Options {
7552            write_buffer_size: 4 * 1024,
7553            partitioned_index: true,
7554            metadata_block_size: 128,
7555            ..Options::default()
7556        };
7557        {
7558            let db = Db::open(dir.path(), opts.clone()).unwrap();
7559            for i in 0..200 {
7560                let k = format!("k{i:04}");
7561                db.put(k.as_bytes(), b"v").unwrap();
7562            }
7563            db.compact_range(None, None).unwrap();
7564        }
7565        // Reopen - the V2 SSTables must still be readable.
7566        let db = Db::open(dir.path(), opts).unwrap();
7567        for i in 0..200 {
7568            let k = format!("k{i:04}");
7569            assert_eq!(db.get(k.as_bytes()).unwrap(), Some(b"v".to_vec()));
7570        }
7571    }
7572
7573    #[test]
7574    fn test_mixed_v1_v2_sstables_read_correctly() {
7575        // Write some data with flat index (V1), then switch to
7576        // partitioned (V2) and write more. Reads that span both
7577        // file types must work correctly.
7578        let dir = TempDir::new().unwrap();
7579        {
7580            let opts = Options {
7581                write_buffer_size: 4 * 1024,
7582                partitioned_index: false,
7583                ..Options::default()
7584            };
7585            let db = Db::open(dir.path(), opts).unwrap();
7586            for i in 0..100 {
7587                let k = format!("k{i:04}");
7588                db.put(k.as_bytes(), b"v1").unwrap();
7589            }
7590            db.compact_range(None, None).unwrap();
7591        }
7592        {
7593            let opts = Options {
7594                write_buffer_size: 4 * 1024,
7595                partitioned_index: true,
7596                metadata_block_size: 128,
7597                ..Options::default()
7598            };
7599            let db = Db::open(dir.path(), opts).unwrap();
7600            for i in 100..200 {
7601                let k = format!("k{i:04}");
7602                db.put(k.as_bytes(), b"v2").unwrap();
7603            }
7604            // Don't compact - leave V1 files at lower levels and
7605            // V2 files in L0/L1.
7606        }
7607        let opts = Options {
7608            partitioned_index: true,
7609            ..Options::default()
7610        };
7611        let db = Db::open(dir.path(), opts).unwrap();
7612        for i in 0..100 {
7613            let k = format!("k{i:04}");
7614            assert_eq!(db.get(k.as_bytes()).unwrap(), Some(b"v1".to_vec()));
7615        }
7616        for i in 100..200 {
7617            let k = format!("k{i:04}");
7618            assert_eq!(db.get(k.as_bytes()).unwrap(), Some(b"v2".to_vec()));
7619        }
7620    }
7621
7622    #[test]
7623    fn test_streaming_compaction_produces_correct_reads() {
7624        // A compaction large enough to span multiple output files;
7625        // read everything back and confirm the streaming path keeps
7626        // the latest version for every user key.
7627        let opts = Options {
7628            write_buffer_size: 4 * 1024,
7629            max_subcompactions: 4,
7630            target_file_size: 8 * 1024,
7631            ..Options::default()
7632        };
7633        let dir = TempDir::new().unwrap();
7634        let db = Db::open(dir.path(), opts).unwrap();
7635
7636        let expected: Vec<(String, String)> = (0..2048)
7637            .map(|i| (format!("k{i:06}"), format!("v{i}")))
7638            .collect();
7639        for (k, v) in &expected {
7640            db.put(k.as_bytes(), v.as_bytes()).unwrap();
7641        }
7642        // Overwrite a window to exercise dedup across input files.
7643        for i in 100..200 {
7644            let k = format!("k{i:06}");
7645            let v = format!("v{i}-new");
7646            db.put(k.as_bytes(), v.as_bytes()).unwrap();
7647        }
7648
7649        db.compact_range(None, None).unwrap();
7650
7651        for (k, v) in &expected {
7652            let i: usize = k[1..].parse().unwrap();
7653            let want = if (100..200).contains(&i) {
7654                format!("v{i}-new")
7655            } else {
7656                v.clone()
7657            };
7658            assert_eq!(
7659                db.get(k.as_bytes()).unwrap(),
7660                Some(want.into_bytes()),
7661                "key {k} must read back the latest value"
7662            );
7663        }
7664    }
7665
7666    #[test]
7667    fn test_streaming_compaction_handles_single_worker_option() {
7668        // max_subcompactions is accepted for API compatibility, but
7669        // the streaming compaction path writes from the compaction
7670        // worker thread.
7671        let opts = Options {
7672            write_buffer_size: 4 * 1024,
7673            max_subcompactions: 1,
7674            target_file_size: 8 * 1024,
7675            ..Options::default()
7676        };
7677        let dir = TempDir::new().unwrap();
7678        let db = Db::open(dir.path(), opts).unwrap();
7679        for i in 0..256 {
7680            let k = format!("k{i:04}");
7681            db.put(k.as_bytes(), b"v").unwrap();
7682        }
7683        db.compact_range(None, None).unwrap();
7684        for i in 0..256 {
7685            let k = format!("k{i:04}");
7686            assert_eq!(db.get(k.as_bytes()).unwrap(), Some(b"v".to_vec()));
7687        }
7688    }
7689
7690    #[test]
7691    fn test_streaming_compaction_accepts_subcompaction_option() {
7692        // The compatibility knob should not change correctness even
7693        // though output writing is now part of the bounded-memory
7694        // streaming path.
7695        let opts = Options {
7696            write_buffer_size: 4 * 1024,
7697            max_subcompactions: 8,
7698            ..Options::default()
7699        };
7700        let dir = TempDir::new().unwrap();
7701        let db = Db::open(dir.path(), opts).unwrap();
7702        for i in 0..32 {
7703            let k = format!("k{i:02}");
7704            db.put(k.as_bytes(), b"v").unwrap();
7705        }
7706        db.compact_range(None, None).unwrap();
7707        for i in 0..32 {
7708            let k = format!("k{i:02}");
7709            assert_eq!(db.get(k.as_bytes()).unwrap(), Some(b"v".to_vec()));
7710        }
7711    }
7712
7713    #[test]
7714    fn test_perf_context_captures_db_get_and_put_activity() {
7715        // End-to-end: enable PerfContext timing on the current
7716        // thread, do a few writes and reads, then snapshot. The
7717        // counters should show one get_count per read, one
7718        // write_count per put, and non-zero time in both the
7719        // WAL/memtable write phases and the memtable read phase.
7720        let (db, _dir) = open_tmp();
7721
7722        PerfContext::set_level(PerfLevel::EnableTime);
7723        PerfContext::reset();
7724
7725        db.put(b"alpha", b"1").unwrap();
7726        db.put(b"beta", b"2").unwrap();
7727        db.put(b"gamma", b"3").unwrap();
7728
7729        let _ = db.get(b"alpha").unwrap();
7730        let _ = db.get(b"beta").unwrap();
7731
7732        let snap = PerfContext::capture();
7733        assert_eq!(snap.write_count, 3, "3 puts → write_count 3");
7734        assert_eq!(snap.get_count, 2, "2 gets → get_count 2");
7735        assert!(
7736            snap.write_wal_time_nanos > 0,
7737            "WAL phase should record non-zero time under EnableTime"
7738        );
7739        assert!(
7740            snap.write_memtable_time_nanos > 0,
7741            "memtable write phase should record non-zero time"
7742        );
7743        assert!(
7744            snap.get_from_memtable_time_nanos > 0,
7745            "memtable read phase should record non-zero time"
7746        );
7747
7748        // Disable and confirm subsequent activity is invisible.
7749        PerfContext::set_level(PerfLevel::Disable);
7750        let before = snap;
7751        db.put(b"delta", b"4").unwrap();
7752        let _ = db.get(b"alpha").unwrap();
7753        let after = PerfContext::capture();
7754        assert_eq!(after, before, "Disable level must freeze counters");
7755    }
7756
7757    #[test]
7758    fn test_evict_compaction_data_from_page_cache_is_correctness_neutral() {
7759        // Enabling the page-cache hint must not change what a
7760        // compaction produces. On Linux the `posix_fadvise`
7761        // syscall runs but is a best-effort hint; on other
7762        // platforms it's a no-op. Either way, the output SSTs
7763        // contain the same data as the leveled baseline, so
7764        // readers must see identical values afterward.
7765        let opts = Options {
7766            write_buffer_size: 4 * 1024,
7767            evict_compaction_data_from_page_cache: true,
7768            ..Options::default()
7769        };
7770        let dir = TempDir::new().unwrap();
7771        let db = Db::open(dir.path(), opts).unwrap();
7772
7773        for i in 0..128 {
7774            let k = format!("k{i:04}");
7775            let v = format!("v{i}");
7776            db.put(k.as_bytes(), v.as_bytes()).unwrap();
7777        }
7778        // Overwrite a few so dedup runs through the hint path.
7779        for i in 0..32 {
7780            let k = format!("k{i:04}");
7781            let v = format!("v{i}-new");
7782            db.put(k.as_bytes(), v.as_bytes()).unwrap();
7783        }
7784        db.compact_range(None, None).unwrap();
7785
7786        for i in 0..128 {
7787            let k = format!("k{i:04}");
7788            let expected = if i < 32 {
7789                format!("v{i}-new")
7790            } else {
7791                format!("v{i}")
7792            };
7793            assert_eq!(
7794                db.get(k.as_bytes()).unwrap(),
7795                Some(expected.into_bytes()),
7796                "key {k} must still read its latest value"
7797            );
7798        }
7799    }
7800
7801    #[test]
7802    fn test_universal_compaction_reads_are_correct_after_merge() {
7803        // Write a batch under Universal, force a full merge via
7804        // compact_range, and verify every key is still readable
7805        // with the most recent value.
7806        let opts = Options {
7807            write_buffer_size: 4 * 1024,
7808            compaction_style: CompactionStyle::Universal,
7809            ..Options::default()
7810        };
7811        let dir = TempDir::new().unwrap();
7812        let db = Db::open(dir.path(), opts).unwrap();
7813
7814        for i in 0..64 {
7815            let k = format!("k{i:04}");
7816            let v = format!("v{i}");
7817            db.put(k.as_bytes(), v.as_bytes()).unwrap();
7818        }
7819        // Overwrite the first 16 keys so dedup has to pick the
7820        // newest version during the merge.
7821        for i in 0..16 {
7822            let k = format!("k{i:04}");
7823            let v = format!("v{i}-updated");
7824            db.put(k.as_bytes(), v.as_bytes()).unwrap();
7825        }
7826
7827        db.compact_range(None, None).unwrap();
7828        std::thread::sleep(std::time::Duration::from_millis(100));
7829
7830        for i in 0..64 {
7831            let k = format!("k{i:04}");
7832            let expected = if i < 16 {
7833                format!("v{i}-updated")
7834            } else {
7835                format!("v{i}")
7836            };
7837            assert_eq!(
7838                db.get(k.as_bytes()).unwrap(),
7839                Some(expected.into_bytes()),
7840                "key {k} must read back its latest value"
7841            );
7842        }
7843    }
7844
7845    #[test]
7846    fn test_universal_compaction_never_creates_l1_files() {
7847        // Every Universal merge output should stay at L0 - the
7848        // level-size push-down rule must not fire for this style.
7849        let opts = Options {
7850            write_buffer_size: 4 * 1024,
7851            l0_compaction_trigger: 1,
7852            compaction_style: CompactionStyle::Universal,
7853            ..Options::default()
7854        };
7855        let dir = TempDir::new().unwrap();
7856        let db = Db::open(dir.path(), opts).unwrap();
7857        for i in 0..64 {
7858            let k = format!("k{i:04}");
7859            db.put(k.as_bytes(), &vec![0xCC; 256]).unwrap();
7860        }
7861        db.compact_range(None, None).unwrap();
7862        std::thread::sleep(std::time::Duration::from_millis(100));
7863
7864        let l1 = db.get_int_property("regolith.num-files-at-level1").unwrap();
7865        assert_eq!(l1, 0, "Universal must not produce L1 files, saw {l1}");
7866        let l0 = db.get_int_property("regolith.num-files-at-level0").unwrap();
7867        assert!(
7868            l0 >= 1,
7869            "Universal compaction should leave at least one L0 file"
7870        );
7871    }
7872
7873    #[test]
7874    fn test_universal_compaction_full_merge_drops_shadowed_versions() {
7875        // After a full universal compact_range, we expect the
7876        // output to be a single L0 file (min cardinality). This
7877        // exercises the compact_range full-merge path.
7878        let opts = Options {
7879            write_buffer_size: 4 * 1024,
7880            compaction_style: CompactionStyle::Universal,
7881            ..Options::default()
7882        };
7883        let dir = TempDir::new().unwrap();
7884        let db = Db::open(dir.path(), opts).unwrap();
7885        for i in 0..32 {
7886            let k = format!("k{i:04}");
7887            db.put(k.as_bytes(), &vec![0xAA; 512]).unwrap();
7888        }
7889        // Give the background scheduler a moment to potentially
7890        // kick off work, then force-merge synchronously.
7891        std::thread::sleep(std::time::Duration::from_millis(50));
7892        db.compact_range(None, None).unwrap();
7893        std::thread::sleep(std::time::Duration::from_millis(100));
7894
7895        let l0 = db.get_int_property("regolith.num-files-at-level0").unwrap();
7896        assert_eq!(
7897            l0, 1,
7898            "full universal compact_range should fold everything into one L0 file, saw {l0}"
7899        );
7900    }
7901
7902    #[test]
7903    fn test_fifo_compaction_bounds_total_size() {
7904        // Tiny memtable + tight FIFO cap: sustained writes should
7905        // produce many L0 files, and after each flush the oldest
7906        // ones should be unlinked so the total stays bounded.
7907        let opts = Options {
7908            write_buffer_size: 4 * 1024,
7909            compaction_style: CompactionStyle::Fifo,
7910            fifo_compaction_options: FifoCompactionOptions {
7911                max_table_files_size: 32 * 1024,
7912            },
7913            ..Options::default()
7914        };
7915        let dir = TempDir::new().unwrap();
7916        let db = Db::open(dir.path(), opts).unwrap();
7917
7918        // Write enough data to produce ~16 flushes of ~4 KB each,
7919        // well over the 32 KB cap. Each write has a distinct
7920        // monotonically increasing key so flushes don't overlap.
7921        let payload = vec![0xEEu8; 256];
7922        for i in 0..256 {
7923            let k = format!("k{i:06}");
7924            db.put(k.as_bytes(), &payload).unwrap();
7925        }
7926
7927        // Give the background compaction thread a moment to
7928        // process the trailing flushes + FIFO drops.
7929        std::thread::sleep(std::time::Duration::from_millis(200));
7930
7931        // Force any remaining flushes through and run one more
7932        // FIFO pass via compact_range (which acquires the
7933        // compaction lock and drains pending work).
7934        db.compact_range(None, None).unwrap();
7935        std::thread::sleep(std::time::Duration::from_millis(100));
7936
7937        let total = db
7938            .get_int_property("regolith.total-sst-files-size")
7939            .unwrap_or(0);
7940        assert!(
7941            total <= 64 * 1024,
7942            "FIFO cap 32KB should keep total < 64KB slack, got {total}"
7943        );
7944        // Meanwhile the newest keys must still be readable (the
7945        // oldest ones may have been dropped by FIFO).
7946        assert_eq!(
7947            db.get(b"k000255").unwrap(),
7948            Some(payload.clone()),
7949            "newest key must survive FIFO compaction"
7950        );
7951    }
7952
7953    #[test]
7954    fn test_fifo_compaction_keeps_at_least_one_file() {
7955        // A single oversized file must not be deleted - FIFO
7956        // refuses to drop the last surviving SST because that
7957        // would wipe the database.
7958        let opts = Options {
7959            write_buffer_size: 4 * 1024,
7960            compaction_style: CompactionStyle::Fifo,
7961            fifo_compaction_options: FifoCompactionOptions {
7962                max_table_files_size: 1, // 1 byte cap: always over limit
7963            },
7964            ..Options::default()
7965        };
7966        let dir = TempDir::new().unwrap();
7967        let db = Db::open(dir.path(), opts).unwrap();
7968        for i in 0..32 {
7969            let k = format!("k{i:04}");
7970            db.put(k.as_bytes(), &vec![0xAA; 512]).unwrap();
7971        }
7972        std::thread::sleep(std::time::Duration::from_millis(200));
7973        db.compact_range(None, None).unwrap();
7974        std::thread::sleep(std::time::Duration::from_millis(100));
7975
7976        let l0 = db.get_int_property("regolith.num-files-at-level0").unwrap();
7977        assert!(
7978            l0 >= 1,
7979            "FIFO must keep at least one L0 file even when over the cap"
7980        );
7981    }
7982
7983    #[test]
7984    fn test_fifo_compaction_never_promotes_to_l1() {
7985        // Under FIFO, the background scheduler should never
7986        // promote files from L0 to L1. The `l0_compaction_trigger`
7987        // knob is a level-style knob and must have no effect.
7988        let opts = Options {
7989            write_buffer_size: 4 * 1024,
7990            l0_compaction_trigger: 2,
7991            compaction_style: CompactionStyle::Fifo,
7992            fifo_compaction_options: FifoCompactionOptions {
7993                max_table_files_size: 10 * 1024 * 1024,
7994            },
7995            ..Options::default()
7996        };
7997        let dir = TempDir::new().unwrap();
7998        let db = Db::open(dir.path(), opts).unwrap();
7999        for i in 0..64 {
8000            let k = format!("k{i:04}");
8001            db.put(k.as_bytes(), &vec![0xBB; 256]).unwrap();
8002        }
8003        std::thread::sleep(std::time::Duration::from_millis(200));
8004
8005        let l1 = db.get_int_property("regolith.num-files-at-level1").unwrap();
8006        assert_eq!(l1, 0, "FIFO must not produce L1 files, saw {l1}");
8007    }
8008
8009    #[test]
8010    fn test_tailing_iter_sees_writes_after_creation() {
8011        // Initial writes, then a tailing iterator, then more
8012        // writes - the tailing iterator must surface the later
8013        // writes once it advances past the initial set.
8014        let (db, _dir) = open_tmp();
8015        for i in 0..5 {
8016            let k = format!("log/{i:04}");
8017            db.put(k.as_bytes(), format!("v{i}").as_bytes()).unwrap();
8018        }
8019
8020        let mut tail = db.iter_tailing();
8021        tail.seek_to_first();
8022
8023        // Drain the initial 5 entries.
8024        let mut seen: Vec<String> = Vec::new();
8025        while tail.valid() {
8026            seen.push(String::from_utf8(tail.key().unwrap().to_vec()).unwrap());
8027            tail.next();
8028        }
8029        assert_eq!(seen.len(), 5, "first drain saw {seen:?}");
8030
8031        // Now push more writes at strictly larger keys.
8032        for i in 5..10 {
8033            let k = format!("log/{i:04}");
8034            db.put(k.as_bytes(), format!("v{i}").as_bytes()).unwrap();
8035        }
8036
8037        // Stepping again should refresh the view and surface
8038        // the new entries without re-emitting the first batch.
8039        tail.next();
8040        while tail.valid() {
8041            seen.push(String::from_utf8(tail.key().unwrap().to_vec()).unwrap());
8042            tail.next();
8043        }
8044        assert_eq!(seen.len(), 10, "tail saw {seen:?}");
8045        for (i, k) in seen.iter().enumerate() {
8046            assert_eq!(k, &format!("log/{i:04}"));
8047        }
8048    }
8049
8050    #[test]
8051    fn test_tailing_iter_survives_flush_and_compaction() {
8052        // Tiny write_buffer so writes between drains roll
8053        // memtables and produce L0 files. The tailing iter must
8054        // pick up those new SSTs on the next refresh.
8055        let opts = Options {
8056            write_buffer_size: 4 * 1024,
8057            ..Options::default()
8058        };
8059        let dir = TempDir::new().unwrap();
8060        let db = Db::open(dir.path(), opts).unwrap();
8061
8062        for i in 0..16 {
8063            let k = format!("log/{i:04}");
8064            db.put(k.as_bytes(), &vec![0xAA; 256]).unwrap();
8065        }
8066
8067        let mut tail = db.iter_tailing();
8068        tail.seek_to_first();
8069        let mut seen: usize = 0;
8070        while tail.valid() {
8071            seen += 1;
8072            tail.next();
8073        }
8074        assert_eq!(seen, 16);
8075
8076        // Force a flush and a compaction - the existing tail
8077        // iter is no longer pinned to anything visible, but a
8078        // refresh + new writes should still work.
8079        db.compact_range(None, None).unwrap();
8080        for i in 16..32 {
8081            let k = format!("log/{i:04}");
8082            db.put(k.as_bytes(), &vec![0xBB; 256]).unwrap();
8083        }
8084
8085        tail.refresh();
8086        let mut seen_after = 0;
8087        while tail.valid() {
8088            seen_after += 1;
8089            tail.next();
8090        }
8091        assert_eq!(
8092            seen_after, 16,
8093            "tail should pick up the 16 new entries after refresh"
8094        );
8095    }
8096
8097    #[test]
8098    fn test_tailing_iter_no_re_emission_after_explicit_refresh() {
8099        let (db, _dir) = open_tmp();
8100        for i in 0..3 {
8101            db.put(format!("k{i}").as_bytes(), b"v").unwrap();
8102        }
8103        let mut tail = db.iter_tailing();
8104        tail.seek_to_first();
8105        assert!(tail.valid());
8106        let first_key = tail.key().unwrap().to_vec();
8107        assert_eq!(first_key, b"k0");
8108        tail.next();
8109        assert_eq!(tail.key().unwrap(), b"k1");
8110
8111        // Explicit refresh in the middle of iteration must NOT
8112        // re-emit k0.
8113        tail.refresh();
8114        // After refresh we should be positioned strictly after
8115        // the last returned key (k1), so the next valid key is
8116        // k2.
8117        assert!(tail.valid());
8118        assert_eq!(tail.key().unwrap(), b"k2");
8119        tail.next();
8120        assert!(!tail.valid(), "no more keys after k2");
8121    }
8122
8123    #[test]
8124    fn test_tailing_iter_cf_scoping() {
8125        // Tailing iterator scoped to one CF must not surface
8126        // keys from other CFs.
8127        let (db, _dir) = open_tmp();
8128        let cf_logs = db.create_column_family("logs").unwrap();
8129        let cf_other = db.create_column_family("other").unwrap();
8130
8131        db.put_cf(&cf_logs, b"a", b"1").unwrap();
8132        db.put_cf(&cf_other, b"a", b"x").unwrap();
8133        db.put_cf(&cf_logs, b"b", b"2").unwrap();
8134
8135        let mut tail = db.iter_tailing_cf(&cf_logs);
8136        tail.seek_to_first();
8137        let mut seen = Vec::new();
8138        while tail.valid() {
8139            seen.push((tail.key().unwrap().to_vec(), tail.value().unwrap().to_vec()));
8140            tail.next();
8141        }
8142        assert_eq!(
8143            seen,
8144            vec![
8145                (b"a".to_vec(), b"1".to_vec()),
8146                (b"b".to_vec(), b"2".to_vec())
8147            ]
8148        );
8149
8150        // A write to the other CF must not bleed in even after
8151        // refresh.
8152        db.put_cf(&cf_other, b"c", b"y").unwrap();
8153        tail.refresh();
8154        assert!(!tail.valid());
8155    }
8156
8157    #[test]
8158    fn test_block_cache_usage_property_reports_nonzero_after_reads() {
8159        // A cache with a small-but-nonzero budget fills with
8160        // decompressed data blocks as reads touch SSTables. The
8161        // `regolith.block-cache-usage` property must report a
8162        // positive number once at least one read has happened
8163        // against a file that isn't entirely in the memtable.
8164        let opts = Options {
8165            write_buffer_size: 4 * 1024,
8166            block_cache_size: 1024 * 1024,
8167            ..Options::default()
8168        };
8169        let dir = TempDir::new().unwrap();
8170        let db = Db::open(dir.path(), opts).unwrap();
8171
8172        let payload = vec![0xABu8; 256];
8173        for i in 0..200 {
8174            let k = format!("k{i:04}");
8175            db.put(k.as_bytes(), &payload).unwrap();
8176        }
8177        // Force a flush so the reads below have to touch SST blocks.
8178        db.compact_range(None, None).unwrap();
8179
8180        // Read a few keys to populate the block cache.
8181        for i in 0..50 {
8182            let k = format!("k{i:04}");
8183            let _ = db.get(k.as_bytes()).unwrap();
8184        }
8185
8186        let usage = db
8187            .get_int_property("regolith.block-cache-usage")
8188            .expect("property must exist");
8189        assert!(
8190            usage > 0,
8191            "expected block-cache-usage > 0 after reads, got {usage}"
8192        );
8193        let cap = db
8194            .get_int_property("regolith.block-cache-capacity")
8195            .expect("property must exist");
8196        assert!(
8197            cap >= 512 * 1024,
8198            "expected at least 512KB capacity, got {cap}"
8199        );
8200        assert!(usage <= cap, "usage {usage} must not exceed capacity {cap}");
8201    }
8202
8203    #[test]
8204    fn test_rate_limiter_throttles_compaction() {
8205        use std::sync::Arc;
8206        use std::time::{Duration, Instant};
8207
8208        // 100 KB/s sustained, 5 KB burst. Compression is disabled so
8209        // the on-disk SST size stays proportional to the data we
8210        // feed in (otherwise LZ4 would collapse the payload to a
8211        // few KB and nothing meaningful would be throttled). A
8212        // 16 MB buffer keeps everything in the memtable until
8213        // compact_range triggers a flush + compaction, both of
8214        // which the limiter throttles.
8215        let limiter = Arc::new(TokenBucketRateLimiter::new(
8216            100_000,
8217            Duration::from_millis(50),
8218            5_000,
8219        ));
8220        let opts = Options {
8221            write_buffer_size: 16 * 1024 * 1024,
8222            compression: CompressionType::None,
8223            rate_limiter: Some(limiter.clone() as Arc<dyn RateLimiter>),
8224            ..Options::default()
8225        };
8226
8227        let dir = TempDir::new().unwrap();
8228        let db = Db::open(dir.path(), opts).unwrap();
8229
8230        // Write ~100 KB of well-dispersed keys so the resulting SST
8231        // is large enough that the limiter has real work to do.
8232        for i in 0..200 {
8233            let k = format!("key-{i:010}");
8234            // Each value is distinct so prefix compression can't
8235            // collapse the block.
8236            let v = format!("value-for-key-{i:010}-payload-{}", i);
8237            db.put(k.as_bytes(), v.as_bytes()).unwrap();
8238        }
8239
8240        let start = Instant::now();
8241        db.compact_range(None, None).unwrap();
8242        let elapsed = start.elapsed();
8243
8244        // With ~10 KB of uncompressed output flushed + compacted at
8245        // 100 KB/s past a 5 KB burst, we expect the critical path
8246        // to block for at least one refill period (~50 ms) and in
8247        // practice several. Assert a conservative floor to confirm
8248        // the limiter was actually consulted without flaking on
8249        // CI variance.
8250        assert!(
8251            elapsed >= Duration::from_millis(100),
8252            "compaction with 100KB/s limiter finished in {elapsed:?}, expected >= 100ms"
8253        );
8254
8255        // The limiter must have been consulted for background I/O.
8256        assert!(
8257            limiter.get_total_bytes_through(Priority::Low) > 0,
8258            "limiter saw zero background bytes"
8259        );
8260        assert_eq!(limiter.get_total_bytes_through(Priority::High), 0);
8261    }
8262
8263    #[test]
8264    fn test_write_stall_slowdown_accumulates_micros() {
8265        use std::sync::Arc;
8266
8267        let stats = Arc::new(Statistics::new());
8268        let opts = Options {
8269            // Tiny memtable so every handful of puts rolls an L0 file.
8270            write_buffer_size: 4 * 1024,
8271            // Disable automatic compaction so L0 can't drain on us.
8272            l0_compaction_trigger: 1000,
8273            // Slow down once L0 has 2 files, never stop (high trigger).
8274            level0_slowdown_writes_trigger: 2,
8275            level0_stop_writes_trigger: 10_000,
8276            // Disable the memtable-count trigger for this test so we
8277            // isolate the L0 slowdown path.
8278            max_write_buffer_number: 0,
8279            statistics: Some(stats.clone()),
8280            ..Options::default()
8281        };
8282        let dir = TempDir::new().unwrap();
8283        let db = Db::open(dir.path(), opts).unwrap();
8284
8285        // Write enough data to cross the slowdown trigger and keep
8286        // going. Each put is ~600 bytes, so after ~7 puts the
8287        // memtable rolls, and after the 2nd flush L0 hits the
8288        // slowdown trigger.
8289        let payload = vec![0xCDu8; 600];
8290        for i in 0..128 {
8291            let k = format!("k{i:04}");
8292            db.put(k.as_bytes(), &payload).unwrap();
8293        }
8294
8295        let stall = stats.get_ticker(Ticker::WriteStallMicros);
8296        assert!(
8297            stall > 0,
8298            "expected WriteStallMicros > 0 after crossing slowdown trigger, got {stall}"
8299        );
8300    }
8301
8302    #[test]
8303    fn test_write_stall_no_slowdown_returns_busy() {
8304        let opts = Options {
8305            write_buffer_size: 4 * 1024,
8306            l0_compaction_trigger: 1000,
8307            level0_slowdown_writes_trigger: 2,
8308            level0_stop_writes_trigger: 10_000,
8309            max_write_buffer_number: 0,
8310            ..Options::default()
8311        };
8312        let dir = TempDir::new().unwrap();
8313        let db = Db::open(dir.path(), opts).unwrap();
8314
8315        // Build up L0 past the slowdown trigger.
8316        let payload = vec![0xEFu8; 600];
8317        for i in 0..64 {
8318            let k = format!("k{i:04}");
8319            db.put(k.as_bytes(), &payload).unwrap();
8320        }
8321
8322        // A write with `no_slowdown` must now return Busy rather
8323        // than sleep or block.
8324        let wo = WriteOptions {
8325            no_slowdown: true,
8326            ..WriteOptions::default()
8327        };
8328        let err = db.put_opt(&wo, b"extra", b"value").unwrap_err();
8329        assert!(
8330            matches!(err, Error::Busy(_)),
8331            "expected Error::Busy, got {err:?}"
8332        );
8333    }
8334
8335    #[test]
8336    fn test_write_stall_stop_unblocks_after_compaction() {
8337        use std::sync::Arc;
8338        use std::thread;
8339        use std::time::{Duration, Instant};
8340
8341        // Stop writes entirely once L0 hits 2 files.
8342        let opts = Options {
8343            write_buffer_size: 4 * 1024,
8344            l0_compaction_trigger: 1000,
8345            level0_slowdown_writes_trigger: 0,
8346            level0_stop_writes_trigger: 2,
8347            max_write_buffer_number: 0,
8348            ..Options::default()
8349        };
8350        let dir = TempDir::new().unwrap();
8351        let db = Arc::new(Db::open(dir.path(), opts).unwrap());
8352
8353        // Fill L0 to the stop trigger. Writes go through until the
8354        // snapshot after the flush shows L0 >= 2; from then on the
8355        // next write would block, so we time it carefully with a
8356        // spawned thread.
8357        let payload = vec![0x12u8; 600];
8358        for i in 0..32 {
8359            let k = format!("fill{i:04}");
8360            db.put(k.as_bytes(), &payload).unwrap();
8361            if db
8362                .get_int_property("regolith.num-files-at-level0")
8363                .unwrap_or(0)
8364                >= 2
8365            {
8366                break;
8367            }
8368        }
8369        let l0 = db
8370            .get_int_property("regolith.num-files-at-level0")
8371            .unwrap_or(0);
8372        assert!(l0 >= 2, "precondition: need L0 >= 2, got {l0}");
8373
8374        let db_writer = db.clone();
8375        let blocked = thread::spawn(move || {
8376            let start = Instant::now();
8377            db_writer.put(b"stopkey", b"stopval").unwrap();
8378            start.elapsed()
8379        });
8380
8381        // Give the writer time to fully enter the stall loop.
8382        thread::sleep(Duration::from_millis(50));
8383        assert!(!blocked.is_finished(), "writer should be blocked on stall");
8384
8385        // compact_range empties L0 and fires stall_signal.notify_all
8386        // from the compaction loop after the pass. The writer should
8387        // wake promptly.
8388        db.compact_range(None, None).unwrap();
8389
8390        let waited = blocked.join().unwrap();
8391        assert!(
8392            waited < Duration::from_secs(5),
8393            "blocked writer took too long to unblock: {waited:?}"
8394        );
8395
8396        // The key we wrote while stalled is readable afterwards.
8397        assert_eq!(db.get(b"stopkey").unwrap(), Some(b"stopval".to_vec()));
8398    }
8399
8400    #[test]
8401    fn test_rate_limiter_unset_leaves_compaction_uncapped() {
8402        // Sanity check: with no limiter in Options, compaction still
8403        // runs and produces correct results. This is the default
8404        // configuration; the test exists mainly to pin the no-op
8405        // branch.
8406        use std::time::Instant;
8407
8408        let opts = Options {
8409            write_buffer_size: 64 * 1024,
8410            ..Options::default()
8411        };
8412        let dir = TempDir::new().unwrap();
8413        let db = Db::open(dir.path(), opts).unwrap();
8414
8415        let payload = vec![0xABu8; 1024];
8416        for i in 0..256 {
8417            let k = format!("k{i:06}");
8418            db.put(k.as_bytes(), &payload).unwrap();
8419        }
8420
8421        let start = Instant::now();
8422        db.compact_range(None, None).unwrap();
8423        assert!(
8424            start.elapsed() < std::time::Duration::from_secs(5),
8425            "unthrottled compaction took unreasonably long: {:?}",
8426            start.elapsed()
8427        );
8428
8429        // Reads still work after compaction.
8430        for i in 0..256 {
8431            let k = format!("k{i:06}");
8432            assert_eq!(db.get(k.as_bytes()).unwrap(), Some(payload.clone()));
8433        }
8434    }
8435}