Skip to main content

spacedb_store/
redb_engine.rs

1//! redb-backed [`KvEngine`] — the durable engine.
2//!
3//! redb is the mission's chosen engine: MIT/Apache-2.0 (no BUSL in an open-core
4//! product), a stable on-disk format, rigorously crash-tested, and **synchronous**
5//! (so there is no async/sync bridge to maintain). redb already gives us exactly
6//! the transaction model the seam promises — MVCC snapshot reads, single-writer
7//! atomic commits, and abort-on-drop rollback — so this adapter is thin: it maps
8//! dynamic `&str` table names to `TableDefinition`s and flattens redb's error
9//! types into [`StoreError`].
10//!
11//! **Operational discipline (mission L0):** keep read transactions short — redb
12//! reclaims space only past the oldest live read snapshot. Open the database on a
13//! **local disk** only; redb's mmap is unsafe over a network filesystem.
14
15use std::path::Path;
16
17use redb::{Database, ReadableTable, TableDefinition, TableError};
18
19use crate::engine::{Durability, KvEngine, ReadTx, Readable, WriteTx};
20use crate::error::{StoreError, StoreResult};
21
22/// Byte-keyed, byte-valued table. Typing happens one layer up in [`crate::Table`].
23type ByteTable<'a> = TableDefinition<'a, &'static [u8], &'static [u8]>;
24
25fn table_def(name: &str) -> ByteTable<'_> {
26    TableDefinition::new(name)
27}
28
29/// A durable, transactional key/value engine backed by a single redb file.
30pub struct RedbEngine {
31    db: Database,
32}
33
34impl RedbEngine {
35    /// Open (creating if absent) the redb database at `path`.
36    pub fn open(path: impl AsRef<Path>) -> StoreResult<Self> {
37        let db = Database::create(path).map_err(StoreError::engine)?;
38        Ok(Self { db })
39    }
40
41    /// Wrap an already-opened redb database.
42    pub fn from_db(db: Database) -> Self {
43        Self { db }
44    }
45}
46
47// ─── read transaction ────────────────────────────────────────────────────────
48
49/// A redb read transaction (a consistent MVCC snapshot for its lifetime).
50pub struct RedbReadTx {
51    txn: redb::ReadTransaction,
52}
53
54impl Readable for RedbReadTx {
55    fn get_raw(&self, table: &str, key: &[u8]) -> StoreResult<Option<Vec<u8>>> {
56        let t = match self.txn.open_table(table_def(table)) {
57            Ok(t) => t,
58            // A never-written table reads as empty, not an error — matching the
59            // in-memory engine's "missing table = no keys" semantics.
60            Err(TableError::TableDoesNotExist(_)) => return Ok(None),
61            Err(e) => return Err(StoreError::engine(e)),
62        };
63        let got = t.get(key).map_err(StoreError::engine)?;
64        Ok(got.map(|g| g.value().to_vec()))
65    }
66
67    fn range_raw(&self, table: &str, lo: &[u8], hi: &[u8]) -> StoreResult<Vec<(Vec<u8>, Vec<u8>)>> {
68        let t = match self.txn.open_table(table_def(table)) {
69            Ok(t) => t,
70            Err(TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
71            Err(e) => return Err(StoreError::engine(e)),
72        };
73        let mut out = Vec::new();
74        for entry in t.range(lo..hi).map_err(StoreError::engine)? {
75            let (k, v) = entry.map_err(StoreError::engine)?;
76            out.push((k.value().to_vec(), v.value().to_vec()));
77        }
78        Ok(out)
79    }
80}
81
82impl ReadTx for RedbReadTx {}
83
84// ─── write transaction ───────────────────────────────────────────────────────
85
86/// A redb write transaction. redb buffers mutations and applies them atomically
87/// on `commit`; dropping without committing aborts (rollback).
88pub struct RedbWriteTx {
89    txn: redb::WriteTransaction,
90}
91
92impl Readable for RedbWriteTx {
93    fn get_raw(&self, table: &str, key: &[u8]) -> StoreResult<Option<Vec<u8>>> {
94        // Opening a table in a write txn creates it if absent; an empty table
95        // simply reads as `None`, giving read-your-own-writes uniformly.
96        let t = self.txn.open_table(table_def(table)).map_err(StoreError::engine)?;
97        let got = t.get(key).map_err(StoreError::engine)?;
98        Ok(got.map(|g| g.value().to_vec()))
99    }
100
101    fn range_raw(&self, table: &str, lo: &[u8], hi: &[u8]) -> StoreResult<Vec<(Vec<u8>, Vec<u8>)>> {
102        let t = self.txn.open_table(table_def(table)).map_err(StoreError::engine)?;
103        let mut out = Vec::new();
104        for entry in t.range(lo..hi).map_err(StoreError::engine)? {
105            let (k, v) = entry.map_err(StoreError::engine)?;
106            out.push((k.value().to_vec(), v.value().to_vec()));
107        }
108        Ok(out)
109    }
110}
111
112impl WriteTx for RedbWriteTx {
113    fn put_raw(&mut self, table: &str, key: &[u8], val: &[u8]) -> StoreResult<()> {
114        let mut t = self.txn.open_table(table_def(table)).map_err(StoreError::engine)?;
115        t.insert(key, val).map_err(StoreError::engine)?;
116        Ok(())
117    }
118
119    fn delete_raw(&mut self, table: &str, key: &[u8]) -> StoreResult<bool> {
120        let mut t = self.txn.open_table(table_def(table)).map_err(StoreError::engine)?;
121        let removed = t.remove(key).map_err(StoreError::engine)?;
122        Ok(removed.is_some())
123    }
124
125    fn commit(self) -> StoreResult<()> {
126        self.txn.commit().map_err(StoreError::engine)
127    }
128}
129
130// ─── engine ──────────────────────────────────────────────────────────────────
131
132impl KvEngine for RedbEngine {
133    type RTx<'a> = RedbReadTx;
134    type WTx<'a> = RedbWriteTx;
135
136    fn begin_read(&self) -> StoreResult<Self::RTx<'_>> {
137        let txn = self.db.begin_read().map_err(StoreError::engine)?;
138        Ok(RedbReadTx { txn })
139    }
140
141    fn begin_write(&self, durability: Durability) -> StoreResult<Self::WTx<'_>> {
142        let mut txn = self.db.begin_write().map_err(StoreError::engine)?;
143        let redb_durability = match durability {
144            Durability::Immediate => redb::Durability::Immediate,
145            Durability::Eventual => redb::Durability::Eventual,
146        };
147        txn.set_durability(redb_durability);
148        Ok(RedbWriteTx { txn })
149    }
150}