Skip to main content

content_index/
backend.rs

1use crate::IndexError;
2use std::sync::RwLock;
3
4/// Trait for a key-value storage backend for the index.
5/// This allows for different storage implementations (e.g., in-memory, Redb).
6pub trait IndexBackend: Send + Sync {
7    /// Insert or update a key-value pair.
8    fn put(&self, key: &str, value: &[u8]) -> Result<(), IndexError>;
9    /// Retrieve a value by key.
10    fn get(&self, key: &str) -> Result<Option<Vec<u8>>, IndexError>;
11    /// Delete a key-value pair.
12    fn delete(&self, key: &str) -> Result<(), IndexError>;
13    /// Insert or update multiple key-value pairs in a batch.
14    fn batch_put(&self, entries: Vec<(String, Vec<u8>)>) -> Result<(), IndexError>;
15    /// Scan all values in the backend, calling the visitor for each one.
16    fn scan(
17        &self,
18        visitor: &mut dyn FnMut(&[u8]) -> Result<(), IndexError>,
19    ) -> Result<(), IndexError>;
20    /// Flush any buffered writes to the backend.
21    fn flush(&self) -> Result<(), IndexError> {
22        Ok(())
23    }
24}
25
26/// Configuration for selecting and building a backend.
27///
28/// This enum provides a unified way to configure different storage backends.
29/// Each variant contains the necessary configuration for its respective backend.
30///
31/// # Example
32/// ```
33/// use index::BackendConfig;
34///
35/// // In-memory (for testing)
36/// let config = BackendConfig::in_memory();
37///
38/// // Redb (pure Rust, recommended)
39/// let config = BackendConfig::redb("/data/ucfp.redb");
40/// ```
41#[derive(Clone, Debug, Default)]
42pub enum BackendConfig {
43    /// Use Redb for storage. The `path` is the file path for the database.
44    ///
45    /// Redb is a pure Rust embedded database that doesn't require C++ dependencies.
46    /// This is the recommended backend for most deployments.
47    ///
48    /// Requires the `backend-redb` feature to be enabled at compile time (enabled by default).
49    Redb { path: String },
50    /// Use an in-memory HashMap for storage. This is useful for testing.
51    #[default]
52    InMemory,
53}
54
55impl BackendConfig {
56    /// Create an in-memory backend configuration.
57    pub fn in_memory() -> Self {
58        BackendConfig::InMemory
59    }
60
61    /// Create a Redb backend configuration.
62    ///
63    /// # Arguments
64    /// * `path` - The file path where the database will be stored
65    ///
66    /// # Example
67    /// ```
68    /// use index::BackendConfig;
69    ///
70    /// let config = BackendConfig::redb("/data/ucfp.redb");
71    /// ```
72    pub fn redb<P: Into<String>>(path: P) -> Self {
73        BackendConfig::Redb { path: path.into() }
74    }
75
76    /// Build the backend based on the configuration.
77    ///
78    /// This method creates the appropriate backend implementation based on the
79    /// configuration variant. Each backend type is only available if its
80    /// corresponding feature flag is enabled at compile time.
81    ///
82    /// # Returns
83    /// * `Ok(Box<dyn IndexBackend>)` - Successfully created backend
84    /// * `Err(IndexError)` - Failed to create backend or feature not enabled
85    pub fn build(&self) -> Result<Box<dyn IndexBackend>, IndexError> {
86        match self {
87            BackendConfig::InMemory => Ok(Box::new(InMemoryBackend::new())),
88            BackendConfig::Redb { path } => {
89                // The Redb backend is only available if the `backend-redb` feature is enabled.
90                #[cfg(feature = "backend-redb")]
91                {
92                    Ok(Box::new(RedbBackend::open(path)?))
93                }
94                #[cfg(not(feature = "backend-redb"))]
95                {
96                    // If the feature is not enabled, return an error.
97                    let _ = path;
98                    Err(IndexError::backend("redb backend disabled at compile time"))
99                }
100            }
101        }
102    }
103}
104
105/// An in-memory backend using a `RwLock` around a `HashMap`.
106pub struct InMemoryBackend {
107    records: RwLock<std::collections::HashMap<String, Vec<u8>>>,
108}
109
110impl InMemoryBackend {
111    pub fn new() -> Self {
112        Self {
113            records: RwLock::new(std::collections::HashMap::new()),
114        }
115    }
116}
117
118impl Default for InMemoryBackend {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl IndexBackend for InMemoryBackend {
125    fn put(&self, key: &str, value: &[u8]) -> Result<(), IndexError> {
126        // The lock is held for the duration of the insert.
127        self.records
128            .write()
129            .map_err(|_| IndexError::backend("poisoned lock"))?
130            .insert(key.to_string(), value.to_vec());
131        Ok(())
132    }
133
134    fn get(&self, key: &str) -> Result<Option<Vec<u8>>, IndexError> {
135        // The read lock is held for the duration of the get.
136        let guard = self
137            .records
138            .read()
139            .map_err(|_| IndexError::backend("poisoned lock"))?;
140        Ok(guard.get(key).cloned())
141    }
142
143    fn delete(&self, key: &str) -> Result<(), IndexError> {
144        self.records
145            .write()
146            .map_err(|_| IndexError::backend("poisoned lock"))?
147            .remove(key);
148        Ok(())
149    }
150
151    fn batch_put(&self, entries: Vec<(String, Vec<u8>)>) -> Result<(), IndexError> {
152        // A single write lock is held for the entire batch insert.
153        let mut guard = self
154            .records
155            .write()
156            .map_err(|_| IndexError::backend("poisoned lock"))?;
157        for (key, value) in entries {
158            guard.insert(key, value);
159        }
160        Ok(())
161    }
162
163    fn scan(
164        &self,
165        visitor: &mut dyn FnMut(&[u8]) -> Result<(), IndexError>,
166    ) -> Result<(), IndexError> {
167        // A read lock is held for the duration of the scan.
168        let guard = self
169            .records
170            .read()
171            .map_err(|_| IndexError::backend("poisoned lock"))?;
172        for value in guard.values() {
173            visitor(value)?;
174        }
175        Ok(())
176    }
177}
178
179/// The Redb backend implementation.
180///
181/// Redb is a pure Rust ACID-compliant embedded database that serves as the
182/// default storage backend for UCFP.
183#[cfg(feature = "backend-redb")]
184pub mod redb;
185
186#[cfg(feature = "backend-redb")]
187pub use redb::RedbBackend;