Skip to main content

commonware_storage/qmdb/store/
db.rs

1//! A mutable key-value database that supports variable-sized values, but without authentication.
2//!
3//! # Example
4//!
5//! ```rust
6//! use commonware_storage::{
7//!     journal::contiguous::variable::Config as JournalConfig,
8//!     qmdb::store::db::{Config, Db},
9//!     translator::TwoCap,
10//! };
11//! use commonware_utils::{NZUsize, NZU16, NZU64};
12//! use commonware_cryptography::{blake3::Digest, Digest as _};
13//! use commonware_math::algebra::Random;
14//! use commonware_runtime::{
15//!     buffer::paged::CacheRef, deterministic::Runner, Metrics, Runner as _, Supervisor as _,
16//! };
17//!
18//! use std::num::NonZeroU16;
19//! const PAGE_SIZE: NonZeroU16 = NZU16!(8192);
20//! const PAGE_CACHE_SIZE: usize = 100;
21//!
22//! let executor = Runner::default();
23//! executor.start(|mut ctx| async move {
24//!     let config = Config {
25//!         log: JournalConfig {
26//!             partition: "test-partition".into(),
27//!             write_buffer: NZUsize!(64 * 1024),
28//!             compression: None,
29//!             codec_config: ((), ()),
30//!             items_per_section: NZU64!(4),
31//!             page_cache: CacheRef::from_pooler(&ctx, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)),
32//!         },
33//!         translator: TwoCap,
34//!         init_cache_size: Some(NZUsize!(1 << 16)),
35//!     };
36//!     let mut db =
37//!         Db::<_, Digest, Digest, TwoCap>::init(ctx.child("store"), config)
38//!             .await
39//!             .unwrap();
40//!
41//!     // Insert a key-value pair
42//!     let k = Digest::random(&mut ctx);
43//!     let v = Digest::random(&mut ctx);
44//!     let metadata = Some(Digest::random(&mut ctx));
45//!     db.apply_batch(db.new_batch().update(k, v).finalize(metadata)).await.unwrap();
46//!     db.commit().await.unwrap();
47//!
48//!     // Fetch the value
49//!     let fetched_value = db.get(&k).await.unwrap();
50//!     assert_eq!(fetched_value.unwrap(), v);
51//!
52//!     // Delete the key's value
53//!     db.apply_batch(db.new_batch().delete(k).finalize(None)).await.unwrap();
54//!     db.commit().await.unwrap();
55//!
56//!     // Fetch the value
57//!     let fetched_value = db.get(&k).await.unwrap();
58//!     assert!(fetched_value.is_none());
59//!
60//!     // Destroy the store
61//!     db.destroy().await.unwrap();
62//! });
63//! ```
64//!
65//! ```ignore
66//! // Apply a batch and commit it, then build a child batch from the newly published state
67//! // and apply it. `commit` takes `&mut self`, so committing and building share the same
68//! // exclusive borrow and run in sequence.
69//! db.apply_batch(db.new_batch().update(key_a, value_a).finalize(None)).await?;
70//! db.commit().await?;
71//!
72//! let child = db.new_batch().update(key_b, value_b).finalize(None);
73//! db.apply_batch(child).await?;
74//! db.commit().await?;
75//! ```
76
77use crate::{
78    index::{unordered::Index, Unordered as _},
79    journal::contiguous::{
80        variable::{Config as JournalConfig, Journal},
81        Contiguous, Mutable as _,
82    },
83    merkle::mmr::Location,
84    qmdb::{
85        any::{
86            unordered::{variable::Operation, Update},
87            VariableValue,
88        },
89        build_snapshot_from_log, delete_key,
90        operation::{Committable as _, Key, Operation as _},
91        update_key, FloorHelper,
92    },
93    translator::Translator,
94    Context,
95};
96use commonware_codec::{CodecShared, Read};
97use commonware_macros::boxed;
98use commonware_utils::Array;
99use core::{num::NonZeroUsize, ops::Range};
100use std::collections::BTreeMap;
101use tracing::{debug, warn};
102
103type Error = crate::qmdb::Error<crate::mmr::Family>;
104
105/// Configuration for initializing a [Db].
106#[derive(Clone)]
107pub struct Config<T: Translator, C> {
108    /// Configuration for the variable-size operations log journal.
109    pub log: JournalConfig<C>,
110
111    /// The [Translator] used by the [Index].
112    pub translator: T,
113
114    /// Capacity (in entries) of the `(location -> key)` cache used during init to resolve snapshot
115    /// collisions without re-reading the log; `None` disables it.
116    pub init_cache_size: Option<NonZeroUsize>,
117}
118
119/// A finalized batch of writes and deletes ready to be applied to the store.
120pub struct Changeset<K: Key, V: CodecShared + Clone> {
121    diff: BTreeMap<K, Option<V>>,
122    metadata: Option<V>,
123}
124
125impl<K: Key, V: CodecShared + Clone> Changeset<K, V> {
126    fn into_parts(self) -> (BTreeMap<K, Option<V>>, Option<V>) {
127        (self.diff, self.metadata)
128    }
129}
130
131impl<K: Key, V: CodecShared + Clone> FromIterator<(K, Option<V>)> for Changeset<K, V> {
132    fn from_iter<TIter: IntoIterator<Item = (K, Option<V>)>>(iter: TIter) -> Self {
133        Self {
134            diff: iter.into_iter().collect(),
135            metadata: None,
136        }
137    }
138}
139
140impl<K: Key, V: CodecShared + Clone, const N: usize> From<[(K, Option<V>); N]> for Changeset<K, V> {
141    fn from(items: [(K, Option<V>); N]) -> Self {
142        items.into_iter().collect()
143    }
144}
145
146/// A mutable batch of writes and deletes staged against the current store state.
147pub struct Batch<'a, E, K, V, T>
148where
149    E: Context,
150    K: Array,
151    V: VariableValue,
152    T: Translator,
153{
154    db: &'a Db<E, K, V, T>,
155    diff: BTreeMap<K, Option<V>>,
156}
157
158impl<'a, E, K, V, T> Batch<'a, E, K, V, T>
159where
160    E: Context,
161    K: Array,
162    V: VariableValue,
163    T: Translator,
164{
165    const fn new(db: &'a Db<E, K, V, T>) -> Self {
166        Self {
167            db,
168            diff: BTreeMap::new(),
169        }
170    }
171
172    /// Finalize the batch into a changeset that can be applied to the store.
173    pub fn finalize(self, metadata: Option<V>) -> Changeset<K, V> {
174        Changeset {
175            diff: self.diff,
176            metadata,
177        }
178    }
179
180    /// Get the value of `key` in the batch, or the value in the store if it has
181    /// not been modified by the batch.
182    pub async fn get(&self, key: &K) -> Result<Option<V>, Error> {
183        if let Some(value) = self.diff.get(key) {
184            return Ok(value.clone());
185        }
186        self.db.get(key).await
187    }
188
189    /// Update the value of `key` in the batch.
190    pub fn update(mut self, key: K, value: V) -> Self {
191        self.diff.insert(key, Some(value));
192        self
193    }
194
195    /// Delete the value of `key` in the batch.
196    pub fn delete(mut self, key: K) -> Self {
197        self.diff.insert(key, None);
198        self
199    }
200}
201
202/// An unauthenticated key-value database based off of an append-only [Journal] of operations.
203pub struct Db<E, K, V, T>
204where
205    E: Context,
206    K: Array,
207    V: VariableValue,
208    T: Translator,
209{
210    /// A log of all [Operation]s that have been applied to the store.
211    ///
212    /// # Invariants
213    ///
214    /// - There is always at least one commit operation in the log.
215    /// - The log is never pruned beyond the inactivity floor.
216    log: Journal<E, Operation<crate::mmr::Family, K, V>>,
217
218    /// A snapshot of all currently active operations in the form of a map from each key to the
219    /// location containing its most recent update.
220    ///
221    /// # Invariant
222    ///
223    /// Only references operations of type [Operation::Update].
224    snapshot: Index<T, Location>,
225
226    /// The number of active keys in the store.
227    active_keys: usize,
228
229    /// A location before which all operations are "inactive" (that is, operations before this point
230    /// are over keys that have been updated by some operation at or after this point).
231    pub inactivity_floor_loc: Location,
232
233    /// The location of the last commit operation.
234    pub last_commit_loc: Location,
235
236    /// The number of _steps_ to raise the inactivity floor. Each step involves moving exactly one
237    /// active operation to tip.
238    pub steps: u64,
239}
240
241impl<E, K, V, T> Db<E, K, V, T>
242where
243    E: Context,
244    K: Array,
245    V: VariableValue,
246    T: Translator,
247{
248    /// Get the value of `key` in the db, or None if it has no value.
249    pub async fn get(&self, key: &K) -> Result<Option<V>, Error> {
250        for &loc in self.snapshot.get(key) {
251            let Operation::Update(Update(k, v)) = self.get_op(loc).await? else {
252                unreachable!("location ({loc}) does not reference update operation");
253            };
254
255            if &k == key {
256                return Ok(Some(v));
257            }
258        }
259
260        Ok(None)
261    }
262
263    /// Returns a new empty batch of changes.
264    pub const fn new_batch(&self) -> Batch<'_, E, K, V, T> {
265        Batch::new(self)
266    }
267
268    /// Whether the db currently has no active keys.
269    pub const fn is_empty(&self) -> bool {
270        self.active_keys == 0
271    }
272
273    /// Gets a [Operation] from the log at the given location. Returns [Error::OperationPruned]
274    /// if the location precedes the oldest retained location. The location is otherwise assumed
275    /// valid.
276    async fn get_op(&self, loc: Location) -> Result<Operation<crate::mmr::Family, K, V>, Error> {
277        assert!(*loc < self.log.bounds().end);
278        self.log.read(*loc).await.map_err(|e| match e {
279            crate::journal::Error::ItemPruned(_) => Error::OperationPruned(loc),
280            e => Error::Journal(e),
281        })
282    }
283
284    /// Return [start, end) where `start` and `end - 1` are the Locations of the oldest and newest
285    /// retained operations respectively.
286    pub fn bounds(&self) -> std::ops::Range<Location> {
287        let bounds = self.log.bounds();
288        Location::new(bounds.start)..Location::new(bounds.end)
289    }
290
291    /// Return the Location of the next operation appended to this db.
292    pub const fn size(&self) -> Location {
293        Location::new(self.log.size())
294    }
295
296    /// Return the inactivity floor location. This is the location before which all operations are
297    /// known to be inactive. Operations before this point can be safely pruned.
298    pub const fn inactivity_floor_loc(&self) -> Location {
299        self.inactivity_floor_loc
300    }
301
302    /// Get the metadata associated with the last commit.
303    pub async fn get_metadata(&self) -> Result<Option<V>, Error> {
304        let Operation::CommitFloor(metadata, _) = self.log.read(*self.last_commit_loc).await?
305        else {
306            unreachable!("last commit should be a commit floor operation");
307        };
308
309        Ok(metadata)
310    }
311
312    /// Prune historical operations prior to `prune_loc`. This does not affect the db's root
313    /// or current snapshot.
314    ///
315    /// `prune` requires no prior commit. After a crash, the database remains recoverable;
316    /// uncommitted operations are not guaranteed to survive.
317    pub async fn prune(&mut self, prune_loc: Location) -> Result<(), Error> {
318        if prune_loc > self.inactivity_floor_loc {
319            return Err(Error::PruneBeyondMinRequired(
320                prune_loc,
321                self.inactivity_floor_loc,
322            ));
323        }
324
325        // The floor justifying the boundary may exist only in buffered operations (it
326        // advances before its batch is durable), and pruning does not guarantee buffered
327        // appends are durable. Commit so the justification survives the prune.
328        self.log.commit().await?;
329
330        // Prune the log. The log will prune at section boundaries, so the actual oldest retained
331        // location may be less than requested.
332        if !self.log.prune(*prune_loc).await? {
333            return Ok(());
334        }
335
336        let bounds = self.log.bounds();
337        let log_size = Location::new(bounds.end);
338        let oldest_retained_loc = Location::new(bounds.start);
339        debug!(
340            ?log_size,
341            ?oldest_retained_loc,
342            ?prune_loc,
343            "pruned inactive ops"
344        );
345
346        Ok(())
347    }
348
349    /// Initializes a new [Db] with the given configuration.
350    pub async fn init(
351        context: E,
352        cfg: Config<T, <Operation<crate::mmr::Family, K, V> as Read>::Cfg>,
353    ) -> Result<Self, Error> {
354        let mut log =
355            Journal::<E, Operation<crate::mmr::Family, K, V>>::init(context.child("log"), cfg.log)
356                .await?;
357
358        // Rewind log to remove uncommitted operations.
359        if log.rewind_to(|op| op.is_commit()).await? == 0 {
360            warn!("Log is empty, initializing new db");
361            log.append(&Operation::CommitFloor(None, Location::new(0)))
362                .await?;
363        }
364
365        // Sync the log to avoid having to repeat any recovery that may have been performed on next
366        // startup.
367        log.sync().await?;
368
369        let last_commit_loc =
370            Location::new(log.size().checked_sub(1).expect("commit should exist"));
371
372        // Build the snapshot.
373        let cache_size = cfg.init_cache_size;
374        let mut snapshot = Index::new(context.child("snapshot"), cfg.translator);
375        let (inactivity_floor_loc, active_keys) = {
376            let op = log.read(*last_commit_loc).await?;
377            let inactivity_floor_loc = op.has_floor().expect("last op should be a commit");
378            if inactivity_floor_loc > last_commit_loc {
379                return Err(crate::qmdb::Error::DataCorrupted(
380                    "inactivity floor exceeds last commit",
381                ));
382            }
383            let active_keys = build_snapshot_from_log(
384                inactivity_floor_loc,
385                &log,
386                &mut snapshot,
387                cache_size,
388                |_, _| {},
389            )
390            .await?;
391            (inactivity_floor_loc, active_keys)
392        };
393
394        Ok(Self {
395            log,
396            snapshot,
397            active_keys,
398            inactivity_floor_loc,
399            last_commit_loc,
400            steps: 0,
401        })
402    }
403
404    /// Sync all database state to disk. While this isn't necessary to ensure durability of
405    /// committed operations, periodic invocation may reduce memory usage and the time required to
406    /// recover the database on restart.
407    pub async fn sync(&mut self) -> Result<(), Error> {
408        self.log.sync().await.map_err(Into::into)
409    }
410
411    /// Destroy the db, removing all data from disk.
412    #[boxed]
413    pub async fn destroy(self) -> Result<(), Error> {
414        self.log.destroy().await.map_err(Into::into)
415    }
416
417    #[allow(clippy::type_complexity)]
418    const fn as_floor_helper(
419        &mut self,
420    ) -> FloorHelper<
421        '_,
422        crate::mmr::Family,
423        Index<T, Location>,
424        Journal<E, Operation<crate::mmr::Family, K, V>>,
425    > {
426        FloorHelper {
427            snapshot: &mut self.snapshot,
428            log: &mut self.log,
429        }
430    }
431
432    /// Applies a finalized batch to the in-memory database state and appends its operations to the
433    /// journal, returning the range of written locations.
434    ///
435    /// This publishes the batch to the in-memory database state and appends it to the journal, but
436    /// does not durably persist it. Call [`Db::commit`] or [`Db::sync`] to guarantee durability.
437    pub async fn apply_batch(&mut self, batch: Changeset<K, V>) -> Result<Range<Location>, Error> {
438        let start_loc = self.last_commit_loc + 1;
439        let (diff, metadata) = batch.into_parts();
440
441        for (key, value) in diff {
442            if let Some(value) = value {
443                let updated = {
444                    let new_loc = self.log.bounds().end;
445                    update_key::<crate::mmr::Family, _, _>(
446                        &mut self.snapshot,
447                        &self.log,
448                        &key,
449                        Location::new(new_loc),
450                        None,
451                    )
452                    .await?
453                };
454                if updated.is_some() {
455                    self.steps += 1;
456                } else {
457                    self.active_keys += 1;
458                }
459                self.log
460                    .append(&Operation::Update(Update(key, value)))
461                    .await?;
462            } else {
463                let deleted = delete_key::<crate::mmr::Family, _, _>(
464                    &mut self.snapshot,
465                    &self.log,
466                    &key,
467                    None,
468                )
469                .await?;
470                if deleted.is_some() {
471                    self.log.append(&Operation::Delete(key)).await?;
472                    self.steps += 1;
473                    self.active_keys -= 1;
474                }
475            }
476        }
477
478        // Raise the inactivity floor by `self.steps` steps, plus 1 to account for the previous
479        // commit becoming inactive.
480        if self.is_empty() {
481            self.inactivity_floor_loc = self.size();
482            debug!(tip = ?self.inactivity_floor_loc, "db is empty, raising floor to tip");
483        } else {
484            let steps_to_take = self.steps + 1;
485            for _ in 0..steps_to_take {
486                let loc = self.inactivity_floor_loc;
487                self.inactivity_floor_loc = self.as_floor_helper().raise_floor(loc).await?;
488            }
489        }
490
491        // Append the commit operation with the new inactivity floor.
492        self.last_commit_loc = Location::new(
493            self.log
494                .append(&Operation::CommitFloor(metadata, self.inactivity_floor_loc))
495                .await?,
496        );
497
498        self.steps = 0;
499
500        let end_loc = self.size();
501        Ok(start_loc..end_loc)
502    }
503
504    /// Durably commit the journal state published by prior [`Db::apply_batch`] calls.
505    pub async fn commit(&mut self) -> Result<(), Error> {
506        self.log.commit().await.map_err(Into::into)
507    }
508}
509
510#[cfg(test)]
511mod test {
512    use super::*;
513    use crate::translator::TwoCap;
514    use commonware_cryptography::{
515        blake3::{Blake3, Digest},
516        Hasher as _,
517    };
518    use commonware_macros::test_traced;
519    use commonware_math::algebra::Random;
520    use commonware_runtime::{buffer::paged::CacheRef, deterministic, Runner, Supervisor as _};
521    use commonware_utils::{NZUsize, NZU16, NZU64};
522    use std::num::{NonZeroU16, NonZeroUsize};
523
524    const PAGE_SIZE: NonZeroU16 = NZU16!(77);
525    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(9);
526
527    /// The type of the store used in tests.
528    type TestStore = Db<deterministic::Context, Digest, Vec<u8>, TwoCap>;
529
530    async fn create_test_store(context: deterministic::Context) -> TestStore {
531        let cfg = Config {
532            log: JournalConfig {
533                partition: "journal".into(),
534                write_buffer: NZUsize!(64 * 1024),
535                compression: None,
536                codec_config: ((), ((0..=10000).into(), ())),
537                items_per_section: NZU64!(7),
538                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
539            },
540            translator: TwoCap,
541            init_cache_size: Some(NZUsize!(1024)),
542        };
543        TestStore::init(context, cfg).await.unwrap()
544    }
545
546    async fn apply_entries(
547        db: &mut TestStore,
548        iter: impl IntoIterator<Item = (Digest, Option<Vec<u8>>)> + Send,
549    ) -> Range<Location> {
550        db.apply_batch(iter.into_iter().collect()).await.unwrap()
551    }
552
553    #[test_traced("DEBUG")]
554    pub fn test_store_construct_empty() {
555        let executor = deterministic::Runner::default();
556        executor.start(|mut context| async move {
557            let mut db = create_test_store(context.child("store").with_attribute("index", 0)).await;
558            assert_eq!(db.bounds().end, 1);
559            assert_eq!(db.log.bounds().start, 0);
560            assert_eq!(db.inactivity_floor_loc(), 0);
561            assert!(matches!(db.prune(db.inactivity_floor_loc()).await, Ok(())));
562            assert!(matches!(
563                db.prune(Location::new(1)).await,
564                Err(Error::PruneBeyondMinRequired(_, _))
565            ));
566            assert!(db.get_metadata().await.unwrap().is_none());
567
568            // Make sure closing/reopening gets us back to the same state, even after adding an uncommitted op.
569            let d1 = Digest::random(&mut context);
570            let v1 = vec![1, 2, 3];
571            apply_entries(&mut db, [(d1, Some(v1))]).await;
572            drop(db);
573
574            let mut db = create_test_store(context.child("store").with_attribute("index", 1)).await;
575            assert_eq!(db.bounds().end, 1);
576
577            // Test calling commit on an empty db which should make it (durably) non-empty.
578            let metadata = vec![1, 2, 3];
579            let batch = db.new_batch().finalize(Some(metadata.clone()));
580            let range = db.apply_batch(batch).await.unwrap();
581            assert_eq!(range.start, 1);
582            assert_eq!(range.end, 2);
583            db.commit().await.unwrap();
584            assert_eq!(db.bounds().end, 2);
585            assert!(matches!(db.prune(db.inactivity_floor_loc()).await, Ok(())));
586            assert_eq!(db.get_metadata().await.unwrap(), Some(metadata.clone()));
587
588            let mut db = create_test_store(context.child("store").with_attribute("index", 2)).await;
589            assert_eq!(db.get_metadata().await.unwrap(), Some(metadata));
590
591            // Confirm the inactivity floor doesn't fall endlessly behind with multiple commits on a
592            // non-empty db.
593            apply_entries(
594                &mut db,
595                [(Digest::random(&mut context), Some(vec![1, 2, 3]))],
596            )
597            .await;
598            db.commit().await.unwrap();
599            for _ in 1..100 {
600                db.apply_batch(db.new_batch().finalize(None)).await.unwrap();
601                db.commit().await.unwrap();
602                // Distance should equal 3 after the second commit, with inactivity_floor
603                // referencing the previous commit operation.
604                assert!(db.bounds().end - db.inactivity_floor_loc <= 3);
605                assert!(db.get_metadata().await.unwrap().is_none());
606            }
607
608            db.destroy().await.unwrap();
609        });
610    }
611
612    #[test_traced("DEBUG")]
613    fn test_store_construct_basic() {
614        let executor = deterministic::Runner::default();
615
616        executor.start(|mut ctx| async move {
617            let mut db = create_test_store(ctx.child("store").with_attribute("index", 0)).await;
618
619            // Ensure the store is empty
620            assert_eq!(db.bounds().end, 1);
621            assert_eq!(db.inactivity_floor_loc, 0);
622
623            let key = Digest::random(&mut ctx);
624            let value = vec![2, 3, 4, 5];
625
626            // Attempt to get a key that does not exist
627            let result = db.get(&key).await;
628            assert!(result.unwrap().is_none());
629
630            // Insert a key-value pair. apply_batch writes the Update, a floor-raise move, and a
631            // CommitFloor: 3 new ops on top of the initial commit.
632            apply_entries(&mut db, [(key, Some(value.clone()))]).await;
633
634            assert_eq!(*db.bounds().end, 4);
635            assert_eq!(*db.inactivity_floor_loc, 2);
636
637            // Fetch the value
638            let fetched_value = db.get(&key).await.unwrap();
639            assert_eq!(fetched_value.unwrap(), value);
640
641            // Simulate commit failure: drop without commit.
642            drop(db);
643
644            // Re-open the store
645            let mut db = create_test_store(ctx.child("store").with_attribute("index", 1)).await;
646
647            // Ensure the re-opened store removed the uncommitted operations
648            assert_eq!(*db.bounds().end, 1);
649            assert_eq!(*db.inactivity_floor_loc, 0);
650            assert!(db.get_metadata().await.unwrap().is_none());
651
652            // Insert a key-value pair and persist with metadata.
653            let metadata = vec![99, 100];
654            let range = db
655                .apply_batch(
656                    db.new_batch()
657                        .update(key, value.clone())
658                        .finalize(Some(metadata.clone())),
659                )
660                .await
661                .unwrap();
662            assert_eq!(*range.start, 1);
663            assert_eq!(*range.end, 4);
664            db.commit().await.unwrap();
665            assert_eq!(db.get_metadata().await.unwrap(), Some(metadata.clone()));
666
667            assert_eq!(*db.bounds().end, 4);
668            assert_eq!(*db.inactivity_floor_loc, 2);
669
670            // Re-open the store
671            let mut db = create_test_store(ctx.child("store").with_attribute("index", 2)).await;
672
673            // Ensure the re-opened store retained the committed operations
674            assert_eq!(*db.bounds().end, 4);
675            assert_eq!(*db.inactivity_floor_loc, 2);
676
677            // Fetch the value, ensuring it is still present
678            let fetched_value = db.get(&key).await.unwrap();
679            assert_eq!(fetched_value.unwrap(), value);
680
681            // Insert two new k/v pairs to force pruning of the first section.
682            let (k1, v1) = (Digest::random(&mut ctx), vec![2, 3, 4, 5, 6]);
683            let (k2, v2) = (Digest::random(&mut ctx), vec![6, 7, 8]);
684            apply_entries(&mut db, [(k1, Some(v1.clone()))]).await;
685            apply_entries(&mut db, [(k2, Some(v2.clone()))]).await;
686
687            assert_eq!(*db.bounds().end, 10);
688            assert_eq!(*db.inactivity_floor_loc, 5);
689
690            // Each apply_entries writes a CommitFloor with None metadata, replacing
691            // the previously committed metadata.
692            assert_eq!(db.get_metadata().await.unwrap(), None);
693
694            db.commit().await.unwrap();
695            assert_eq!(db.get_metadata().await.unwrap(), None);
696
697            // commit() is just an fsync now, so bounds and floor are unchanged.
698            assert_eq!(*db.bounds().end, 10);
699            assert_eq!(*db.inactivity_floor_loc, 5);
700
701            // Ensure all keys can be accessed, despite the first section being pruned.
702            assert_eq!(db.get(&key).await.unwrap().unwrap(), value);
703            assert_eq!(db.get(&k1).await.unwrap().unwrap(), v1);
704            assert_eq!(db.get(&k2).await.unwrap().unwrap(), v2);
705
706            // Update existing key with modified value.
707            let mut v1_updated = db.get(&k1).await.unwrap().unwrap();
708            v1_updated.push(7);
709            apply_entries(&mut db, [(k1, Some(v1_updated))]).await;
710            db.commit().await.unwrap();
711            assert_eq!(db.get(&k1).await.unwrap().unwrap(), vec![2, 3, 4, 5, 6, 7]);
712
713            // Create new key.
714            let k3 = Digest::random(&mut ctx);
715            apply_entries(&mut db, [(k3, Some(vec![8]))]).await;
716            db.commit().await.unwrap();
717            assert_eq!(db.get(&k3).await.unwrap().unwrap(), vec![8]);
718
719            // Destroy the store
720            db.destroy().await.unwrap();
721        });
722    }
723
724    #[test_traced("DEBUG")]
725    fn test_store_log_replay() {
726        let executor = deterministic::Runner::default();
727
728        executor.start(|mut ctx| async move {
729            let mut db = create_test_store(ctx.child("store").with_attribute("index", 0)).await;
730
731            // Update the same key many times.
732            const UPDATES: u64 = 100;
733            let k = Digest::random(&mut ctx);
734            for _ in 0..UPDATES {
735                let v = vec![1, 2, 3, 4, 5];
736                apply_entries(&mut db, [(k, Some(v.clone()))]).await;
737            }
738
739            let iter = db.snapshot.get(&k);
740            assert_eq!(iter.count(), 1);
741
742            db.commit().await.unwrap();
743            db.sync().await.unwrap();
744            drop(db);
745
746            // Re-open the store, prune it, then ensure it replays the log correctly.
747            let mut db = create_test_store(ctx.child("store").with_attribute("index", 1)).await;
748            db.prune(db.inactivity_floor_loc()).await.unwrap();
749
750            let iter = db.snapshot.get(&k);
751            assert_eq!(iter.count(), 1);
752
753            // First apply_entries: Update + 1 move + CommitFloor = 3 ops. Subsequent 99: Update + 2
754            // moves + CommitFloor = 4 ops each. Total: 1 (init) + 3 + 99*4 = 400.
755            assert_eq!(*db.bounds().end, 400);
756            // Only the last Update and CommitFloor are active → floor = 398.
757            assert_eq!(*db.inactivity_floor_loc, 398);
758            let floor = db.inactivity_floor_loc;
759
760            // All blobs prior to the inactivity floor are pruned, so the oldest retained location
761            // is the first in the last retained blob.
762            assert_eq!(db.log.bounds().start, *floor - *floor % 7);
763
764            db.destroy().await.unwrap();
765        });
766    }
767
768    #[test_traced("DEBUG")]
769    fn test_store_build_snapshot_keys_with_shared_prefix() {
770        let executor = deterministic::Runner::default();
771
772        executor.start(|mut ctx| async move {
773            let mut db = create_test_store(ctx.child("store").with_attribute("index", 0)).await;
774
775            let (k1, v1) = (Digest::random(&mut ctx), vec![1, 2, 3, 4, 5]);
776            let (mut k2, v2) = (Digest::random(&mut ctx), vec![6, 7, 8, 9, 10]);
777
778            // Ensure k2 shares 2 bytes with k1 (test DB uses `TwoCap` translator.)
779            k2.0[0..2].copy_from_slice(&k1.0[0..2]);
780
781            apply_entries(&mut db, [(k1, Some(v1.clone()))]).await;
782            apply_entries(&mut db, [(k2, Some(v2.clone()))]).await;
783
784            assert_eq!(db.get(&k1).await.unwrap().unwrap(), v1);
785            assert_eq!(db.get(&k2).await.unwrap().unwrap(), v2);
786
787            db.commit().await.unwrap();
788            db.sync().await.unwrap();
789            drop(db);
790
791            // Re-open the store to ensure it builds the snapshot for the conflicting
792            // keys correctly.
793            let db = create_test_store(ctx.child("store").with_attribute("index", 1)).await;
794
795            assert_eq!(db.get(&k1).await.unwrap().unwrap(), v1);
796            assert_eq!(db.get(&k2).await.unwrap().unwrap(), v2);
797
798            db.destroy().await.unwrap();
799        });
800    }
801
802    #[test_traced("DEBUG")]
803    fn test_store_delete() {
804        let executor = deterministic::Runner::default();
805
806        executor.start(|mut ctx| async move {
807            let mut db = create_test_store(ctx.child("store").with_attribute("index", 0)).await;
808
809            // Insert a key-value pair
810            let k = Digest::random(&mut ctx);
811            let v = vec![1, 2, 3, 4, 5];
812            apply_entries(&mut db, [(k, Some(v.clone()))]).await;
813            db.commit().await.unwrap();
814
815            // Fetch the value
816            let fetched_value = db.get(&k).await.unwrap();
817            assert_eq!(fetched_value.unwrap(), v);
818
819            // Delete the key
820            assert!(db.get(&k).await.unwrap().is_some());
821            apply_entries(&mut db, [(k, None)]).await;
822
823            // Ensure the key is no longer present
824            let fetched_value = db.get(&k).await.unwrap();
825            assert!(fetched_value.is_none());
826            assert!(db.get(&k).await.unwrap().is_none());
827
828            // Commit the changes
829            db.commit().await.unwrap();
830
831            // Re-open the store and ensure the key is still deleted
832            let mut db = create_test_store(ctx.child("store").with_attribute("index", 1)).await;
833            let fetched_value = db.get(&k).await.unwrap();
834            assert!(fetched_value.is_none());
835
836            // Re-insert the key
837            apply_entries(&mut db, [(k, Some(v.clone()))]).await;
838            let fetched_value = db.get(&k).await.unwrap();
839            assert_eq!(fetched_value.unwrap(), v);
840
841            // Commit the changes
842            db.commit().await.unwrap();
843
844            // Re-open the store and ensure the snapshot restores the key, after processing
845            // the delete and the subsequent set.
846            let mut db = create_test_store(ctx.child("store").with_attribute("index", 2)).await;
847            let fetched_value = db.get(&k).await.unwrap();
848            assert_eq!(fetched_value.unwrap(), v);
849
850            // Delete a non-existent key (no-op)
851            let k_n = Digest::random(&mut ctx);
852            let range = apply_entries(&mut db, [(k_n, None)]).await;
853            assert_eq!(range.start, 9);
854            assert_eq!(range.end, 11);
855            db.commit().await.unwrap();
856
857            assert!(db.get(&k_n).await.unwrap().is_none());
858            // Make sure k is still there
859            assert!(db.get(&k).await.unwrap().is_some());
860
861            db.destroy().await.unwrap();
862        });
863    }
864
865    /// Tests the pruning example in the module documentation.
866    #[test_traced("DEBUG")]
867    fn test_store_pruning() {
868        let executor = deterministic::Runner::default();
869
870        executor.start(|mut ctx| async move {
871            let mut db = create_test_store(ctx.child("store")).await;
872
873            let k_a = Digest::random(&mut ctx);
874            let k_b = Digest::random(&mut ctx);
875
876            let v_a = vec![1];
877            let v_b = vec![];
878            let v_c = vec![4, 5, 6];
879
880            apply_entries(&mut db, [(k_a, Some(v_a.clone()))]).await;
881            apply_entries(&mut db, [(k_b, Some(v_b.clone()))]).await;
882
883            db.commit().await.unwrap();
884            assert_eq!(*db.bounds().end, 7);
885            assert_eq!(*db.inactivity_floor_loc, 3);
886            assert_eq!(db.get(&k_a).await.unwrap().unwrap(), v_a);
887
888            apply_entries(&mut db, [(k_b, Some(v_a.clone()))]).await;
889            apply_entries(&mut db, [(k_a, Some(v_c.clone()))]).await;
890
891            db.commit().await.unwrap();
892            assert_eq!(*db.bounds().end, 15);
893            assert_eq!(*db.inactivity_floor_loc, 12);
894            assert_eq!(db.get(&k_a).await.unwrap().unwrap(), v_c);
895            assert_eq!(db.get(&k_b).await.unwrap().unwrap(), v_a);
896
897            db.destroy().await.unwrap();
898        });
899    }
900
901    /// Pruning to a floor advanced by applied-but-uncommitted entries must not durably outrun
902    /// the last durable commit: after a crash, the recovered floor would lie below the pruned
903    /// boundary and the store could never reopen.
904    #[test_traced("WARN")]
905    pub fn test_store_db_prune_after_unsynced_floor_recovery() {
906        let executor = deterministic::Runner::default();
907        const ELEMENTS: u64 = 1000;
908        executor.start(|context| async move {
909            let mut db = create_test_store(context.child("store").with_attribute("index", 0)).await;
910
911            // Establish a durable state whose last commit declares an early inactivity floor.
912            for i in 0u64..ELEMENTS {
913                let k = Blake3::hash(&i.to_be_bytes());
914                let v = vec![(i % 255) as u8; ((i % 13) + 7) as usize];
915                apply_entries(&mut db, [(k, Some(v))]).await;
916            }
917            db.commit().await.unwrap();
918            let durable_floor = db.inactivity_floor_loc;
919
920            // Apply (but do not commit) entries that advance the in-memory floor past the
921            // durable commit's floor.
922            for i in 0u64..ELEMENTS {
923                let k = Blake3::hash(&i.to_be_bytes());
924                let v = vec![((i + 1) % 255) as u8; ((i % 13) + 8) as usize];
925                apply_entries(&mut db, [(k, Some(v))]).await;
926            }
927            let unsynced_floor = db.inactivity_floor_loc;
928            assert!(unsynced_floor > durable_floor);
929
930            // Prune to the in-memory floor, then crash before any further commit.
931            db.prune(unsynced_floor).await.unwrap();
932            let op_count = db.bounds().end;
933            drop(db);
934
935            // Reopening must succeed: prune committed the buffered operations first, so the
936            // replayed log reproduces the advanced floor.
937            let db = create_test_store(context.child("store").with_attribute("index", 1)).await;
938            assert_eq!(db.bounds().end, op_count);
939            assert_eq!(db.inactivity_floor_loc, unsynced_floor);
940            db.destroy().await.unwrap();
941        });
942    }
943
944    #[test_traced("WARN")]
945    pub fn test_store_db_recovery() {
946        let executor = deterministic::Runner::default();
947        // Build a db with 1000 keys, some of which we update and some of which we delete.
948        const ELEMENTS: u64 = 1000;
949        executor.start(|context| async move {
950            let db = create_test_store(context.child("store").with_attribute("index", 0)).await;
951
952            // Simulate building batches but not applying them (data is not persisted).
953            {
954                let mut batch = db.new_batch();
955                for i in 0u64..ELEMENTS {
956                    let k = Blake3::hash(&i.to_be_bytes());
957                    let v = vec![(i % 255) as u8; ((i % 13) + 7) as usize];
958                    batch = batch.update(k, v);
959                }
960                // Drop the batch without applying -- simulates a failure before apply.
961            }
962            drop(db);
963            let mut db = create_test_store(context.child("store").with_attribute("index", 1)).await;
964            assert_eq!(*db.bounds().end, 1);
965
966            // Apply the updates and commit them.
967            for i in 0u64..ELEMENTS {
968                let k = Blake3::hash(&i.to_be_bytes());
969                let v = vec![(i % 255) as u8; ((i % 13) + 7) as usize];
970                apply_entries(&mut db, [(k, Some(v.clone()))]).await;
971            }
972            db.commit().await.unwrap();
973
974            // Update every 3rd key and commit.
975            for i in 0u64..ELEMENTS {
976                if i % 3 != 0 {
977                    continue;
978                }
979                let k = Blake3::hash(&i.to_be_bytes());
980                let v = vec![((i + 1) % 255) as u8; ((i % 13) + 8) as usize];
981                apply_entries(&mut db, [(k, Some(v.clone()))]).await;
982            }
983            db.commit().await.unwrap();
984            assert_eq!(db.snapshot.items(), 1000);
985
986            // Delete every 7th key and commit.
987            for i in 0u64..ELEMENTS {
988                if i % 7 != 1 {
989                    continue;
990                }
991                let k = Blake3::hash(&i.to_be_bytes());
992                apply_entries(&mut db, [(k, None)]).await;
993            }
994            db.commit().await.unwrap();
995            let final_count = db.bounds().end;
996            let final_floor = db.inactivity_floor_loc;
997
998            // Sync and reopen the store to ensure the state is preserved.
999            db.sync().await.unwrap();
1000            drop(db);
1001            let mut db = create_test_store(context.child("store").with_attribute("index", 2)).await;
1002            assert_eq!(db.bounds().end, final_count);
1003            assert_eq!(db.inactivity_floor_loc, final_floor);
1004
1005            db.prune(db.inactivity_floor_loc()).await.unwrap();
1006            assert_eq!(db.log.bounds().start, *final_floor - *final_floor % 7);
1007            assert_eq!(db.snapshot.items(), 857);
1008
1009            db.destroy().await.unwrap();
1010        });
1011    }
1012
1013    #[test_traced("WARN")]
1014    pub fn test_store_commit_after_sync_recovers_without_second_sync() {
1015        let executor = deterministic::Runner::default();
1016        executor.start(|context| async move {
1017            let mut db = create_test_store(context.child("store").with_attribute("index", 0)).await;
1018            let key0 = Blake3::hash(&0u64.to_be_bytes());
1019            let key1 = Blake3::hash(&1u64.to_be_bytes());
1020            let value0 = vec![0, 1, 2];
1021            let value1 = vec![3, 4, 5, 6];
1022
1023            // Commit and sync an initial update so restart recovery has an older watermark.
1024            apply_entries(&mut db, [(key0, Some(value0.clone()))]).await;
1025            db.commit().await.unwrap();
1026            db.sync().await.unwrap();
1027
1028            // Persist a later commit without syncing; recovery must replay it after reopen.
1029            apply_entries(&mut db, [(key1, Some(value1.clone()))]).await;
1030            db.commit().await.unwrap();
1031            let committed_end = db.bounds().end;
1032            let committed_floor = db.inactivity_floor_loc();
1033            drop(db);
1034
1035            let db = create_test_store(context.child("store").with_attribute("index", 1)).await;
1036            assert_eq!(db.bounds().end, committed_end);
1037            assert_eq!(db.inactivity_floor_loc(), committed_floor);
1038            assert_eq!(db.get(&key0).await.unwrap(), Some(value0));
1039            assert_eq!(db.get(&key1).await.unwrap(), Some(value1));
1040
1041            db.destroy().await.unwrap();
1042        });
1043    }
1044
1045    #[test_traced("DEBUG")]
1046    fn test_store_batch() {
1047        let executor = deterministic::Runner::default();
1048
1049        executor.start(|mut ctx| async move {
1050            let mut db = create_test_store(ctx.child("store").with_attribute("index", 0)).await;
1051
1052            // Ensure the store is empty
1053            assert_eq!(db.bounds().end, 1);
1054            assert_eq!(db.inactivity_floor_loc, 0);
1055
1056            let key = Digest::random(&mut ctx);
1057            let value = vec![2, 3, 4, 5];
1058
1059            let batch = db.new_batch();
1060
1061            // Attempt to get a key that does not exist
1062            let result = batch.get(&key).await;
1063            assert!(result.unwrap().is_none());
1064
1065            // Insert a key-value pair
1066            let batch = batch.update(key, value.clone());
1067
1068            assert_eq!(db.bounds().end, 1); // The batch is not applied yet
1069            assert_eq!(db.inactivity_floor_loc, 0);
1070
1071            // Fetch the value
1072            let fetched_value = batch.get(&key).await.unwrap();
1073            assert_eq!(fetched_value.unwrap(), value);
1074            db.apply_batch(batch.finalize(None)).await.unwrap();
1075            drop(db);
1076
1077            // Re-open the store
1078            let mut db = create_test_store(ctx.child("store").with_attribute("index", 1)).await;
1079
1080            // Ensure the batch was not applied since we didn't commit.
1081            assert_eq!(db.bounds().end, 1);
1082            assert_eq!(db.inactivity_floor_loc, 0);
1083            assert!(db.get_metadata().await.unwrap().is_none());
1084
1085            // Insert a key-value pair and persist the change.
1086            let metadata = vec![99, 100];
1087            let range = db
1088                .apply_batch(
1089                    db.new_batch()
1090                        .update(key, value.clone())
1091                        .finalize(Some(metadata.clone())),
1092                )
1093                .await
1094                .unwrap();
1095            assert_eq!(range.start, 1);
1096            assert_eq!(range.end, 4);
1097            db.commit().await.unwrap();
1098            assert_eq!(db.get_metadata().await.unwrap(), Some(metadata.clone()));
1099            drop(db);
1100
1101            // Re-open the store
1102            let db = create_test_store(ctx.child("store").with_attribute("index", 2)).await;
1103
1104            // Ensure the re-opened store retained the committed operations
1105            assert_eq!(db.bounds().end, 4);
1106            assert_eq!(db.inactivity_floor_loc, 2);
1107
1108            // Fetch the value, ensuring it is still present
1109            let fetched_value = db.get(&key).await.unwrap();
1110            assert_eq!(fetched_value.unwrap(), value);
1111
1112            // Destroy the store
1113            db.destroy().await.unwrap();
1114        });
1115    }
1116
1117    fn is_send<T: Send>(_: T) {}
1118
1119    #[allow(dead_code)]
1120    fn assert_read_futures_are_send(db: &mut TestStore, key: Digest, loc: Location) {
1121        is_send(db.get(&key));
1122        is_send(db.get_metadata());
1123        is_send(db.prune(loc));
1124        is_send(db.sync());
1125    }
1126
1127    #[allow(dead_code)]
1128    fn assert_write_futures_are_send(
1129        db: &mut Db<deterministic::Context, Digest, Vec<u8>, TwoCap>,
1130        key: Digest,
1131        value: Vec<u8>,
1132    ) {
1133        is_send(db.get(&key));
1134        is_send(db.apply_batch(Changeset::from([(key, Some(value))])));
1135        is_send(db.apply_batch(Changeset::from([(key, None)])));
1136        let batch = db.new_batch();
1137        is_send(batch.get(&key));
1138    }
1139
1140    #[allow(dead_code)]
1141    fn assert_commit_is_send(db: &mut Db<deterministic::Context, Digest, Vec<u8>, TwoCap>) {
1142        is_send(db.commit());
1143    }
1144}