Skip to main content

inferadb_ledger_state/
engine.rs

1//! Storage engine wrapper around [`inferadb-ledger-store`].
2//!
3//! Provides lifecycle management for the underlying B+ tree database:
4//! - File-based ([`StorageEngine`]) and in-memory ([`InMemoryStorageEngine`]) backends
5//! - Shared ownership via [`Arc`] for concurrent access across state layer components
6
7use std::{path::Path, sync::Arc};
8
9use inferadb_ledger_store::{Database, FileBackend, InMemoryBackend};
10use snafu::{ResultExt, Snafu};
11
12/// Errors returned when opening or operating on a [`StorageEngine`].
13#[derive(Debug, Snafu)]
14#[snafu(visibility(pub(crate)))]
15pub enum EngineError {
16    /// Engine failed to open the storage backend.
17    #[snafu(display("Failed to open database at {path}: {source}"))]
18    Open {
19        path: String,
20        source: inferadb_ledger_store::Error,
21        #[snafu(implicit)]
22        location: snafu::Location,
23    },
24
25    /// Underlying storage operation failed.
26    #[snafu(display("Storage operation failed: {source}"))]
27    Storage {
28        source: inferadb_ledger_store::Error,
29        #[snafu(implicit)]
30        location: snafu::Location,
31    },
32}
33
34/// File-backed storage engine wrapping [`Database<FileBackend>`].
35///
36/// Use [`open`](Self::open) to create or open a database at a filesystem path.
37/// The database handle is reference-counted; cloning this struct shares the
38/// same underlying database.
39pub struct StorageEngine {
40    db: Arc<Database<FileBackend>>,
41}
42
43#[allow(clippy::result_large_err)]
44impl StorageEngine {
45    /// Opens or creates a database at the given path.
46    ///
47    /// # Errors
48    ///
49    /// Returns `EngineError::Open` if the database cannot be opened or created at the given path.
50    pub fn open(path: impl AsRef<Path>) -> std::result::Result<Self, EngineError> {
51        let path = path.as_ref();
52        let db = if path.exists() { Database::open(path) } else { Database::create(path) }
53            .context(OpenSnafu { path: path.display().to_string() })?;
54
55        Ok(Self { db: Arc::new(db) })
56    }
57
58    /// Returns a shared reference to the underlying database (via [`Arc::clone`]).
59    pub fn db(&self) -> Arc<Database<FileBackend>> {
60        Arc::clone(&self.db)
61    }
62}
63
64impl Clone for StorageEngine {
65    fn clone(&self) -> Self {
66        Self { db: Arc::clone(&self.db) }
67    }
68}
69
70/// In-memory storage engine wrapping [`Database<InMemoryBackend>`].
71///
72/// Intended for testing. Data is not persisted across process restarts.
73/// The database handle is reference-counted; cloning this struct shares
74/// the same underlying database.
75pub struct InMemoryStorageEngine {
76    db: Arc<Database<InMemoryBackend>>,
77}
78
79#[allow(clippy::result_large_err)]
80impl InMemoryStorageEngine {
81    /// Creates a new in-memory database.
82    ///
83    /// # Errors
84    ///
85    /// Returns `EngineError::Open` if the in-memory database cannot be created.
86    pub fn open() -> std::result::Result<Self, EngineError> {
87        let db = Database::open_in_memory().context(OpenSnafu { path: ":memory:".to_string() })?;
88
89        Ok(Self { db: Arc::new(db) })
90    }
91
92    /// Returns a shared reference to the underlying database (via [`Arc::clone`]).
93    pub fn db(&self) -> Arc<Database<InMemoryBackend>> {
94        Arc::clone(&self.db)
95    }
96}
97
98impl Clone for InMemoryStorageEngine {
99    fn clone(&self) -> Self {
100        Self { db: Arc::clone(&self.db) }
101    }
102}
103
104#[cfg(test)]
105#[allow(clippy::unwrap_used, clippy::expect_used, clippy::disallowed_methods, unused_mut)]
106mod tests {
107    use inferadb_ledger_store::tables;
108
109    use super::*;
110
111    #[test]
112    fn test_open_in_memory() {
113        let engine = InMemoryStorageEngine::open().expect("should open");
114        let db = engine.db();
115        let _read = db.read().expect("should begin read");
116        let _write = db.write().expect("should begin write");
117    }
118
119    #[test]
120    fn test_write_and_read() {
121        let engine = InMemoryStorageEngine::open().expect("should open");
122        let db = engine.db();
123
124        // Write some data
125        {
126            let mut txn = db.write().expect("should begin write");
127            txn.insert::<tables::Entities>(&b"test_key".to_vec(), &b"test_value".to_vec())
128                .expect("insert");
129            txn.commit().expect("commit");
130        }
131
132        // Read it back
133        {
134            let txn = db.read().expect("should begin read");
135            let value = txn.get::<tables::Entities>(&b"test_key".to_vec()).expect("get");
136            assert!(value.is_some());
137            assert_eq!(value.unwrap(), b"test_value");
138        }
139    }
140}