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;