Skip to main content

content_index/backend/
redb.rs

1//! Redb (Rust embedded database) backend implementation for UCFP index storage.
2//!
3//! Redb is a pure Rust embedded key-value store that provides ACID transactions
4//! without requiring external dependencies. This makes it ideal for all deployments
5//! where fast compilation and easy setup are priorities.
6//!
7//! # Features
8//! - ACID transactions with MVCC
9//! - Zero-copy reads
10//! - Crash-safe by default
11//! - No external dependencies (pure Rust)
12//!
13//! # Configuration Example
14//! ```yaml
15//! index:
16//!   backend: "redb"
17//!   redb:
18//!     path: "/data/ucfp.redb"
19//! ```
20
21use crate::{IndexBackend, IndexError};
22use redb::{Database, ReadableTable, TableDefinition};
23use std::path::Path;
24use std::sync::Arc;
25
26/// Table definition for the UCFP index data
27const UCFP_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("ucfp_data");
28
29/// Redb backend implementation for persistent key-value storage.
30///
31/// This backend uses redb's ACID transactions to ensure data consistency.
32/// All operations are atomic and durable by default.
33///
34/// # Thread Safety
35/// The `Arc<Database>` wrapper allows safe sharing across threads.
36/// Redb handles its own internal locking and MVCC.
37pub struct RedbBackend {
38    db: Arc<Database>,
39}
40
41impl RedbBackend {
42    /// Open or create a Redb database at the given path.
43    ///
44    /// # Arguments
45    /// * `path` - The file path where the database will be stored
46    ///
47    /// # Returns
48    /// * `Ok(RedbBackend)` - Successfully opened or created database
49    /// * `Err(IndexError)` - Failed to open/create database
50    ///
51    /// # Example
52    /// ```no_run
53    /// use index::RedbBackend;
54    ///
55    /// let backend = RedbBackend::open("/tmp/test.redb").unwrap();
56    /// ```
57    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, IndexError> {
58        let db = Database::create(path).map_err(|e| IndexError::backend(e.to_string()))?;
59
60        // Initialize the table if it doesn't exist
61        let write_txn = db
62            .begin_write()
63            .map_err(|e| IndexError::backend(e.to_string()))?;
64        {
65            // Accessing the table creates it if it doesn't exist
66            let _table = write_txn
67                .open_table(UCFP_TABLE)
68                .map_err(|e| IndexError::backend(e.to_string()))?;
69        }
70        write_txn
71            .commit()
72            .map_err(|e| IndexError::backend(e.to_string()))?;
73
74        Ok(Self { db: Arc::new(db) })
75    }
76}
77
78impl IndexBackend for RedbBackend {
79    fn put(&self, key: &str, value: &[u8]) -> Result<(), IndexError> {
80        let write_txn = self
81            .db
82            .begin_write()
83            .map_err(|e| IndexError::backend(e.to_string()))?;
84
85        {
86            let mut table = write_txn
87                .open_table(UCFP_TABLE)
88                .map_err(|e| IndexError::backend(e.to_string()))?;
89            table
90                .insert(key, value)
91                .map_err(|e| IndexError::backend(e.to_string()))?;
92        }
93
94        write_txn
95            .commit()
96            .map_err(|e| IndexError::backend(e.to_string()))?;
97        Ok(())
98    }
99
100    fn get(&self, key: &str) -> Result<Option<Vec<u8>>, IndexError> {
101        let read_txn = self
102            .db
103            .begin_read()
104            .map_err(|e| IndexError::backend(e.to_string()))?;
105        let table = read_txn
106            .open_table(UCFP_TABLE)
107            .map_err(|e| IndexError::backend(e.to_string()))?;
108
109        match table
110            .get(key)
111            .map_err(|e| IndexError::backend(e.to_string()))?
112        {
113            Some(value) => Ok(Some(value.value().to_vec())),
114            None => Ok(None),
115        }
116    }
117
118    fn delete(&self, key: &str) -> Result<(), IndexError> {
119        let write_txn = self
120            .db
121            .begin_write()
122            .map_err(|e| IndexError::backend(e.to_string()))?;
123
124        {
125            let mut table = write_txn
126                .open_table(UCFP_TABLE)
127                .map_err(|e| IndexError::backend(e.to_string()))?;
128            table
129                .remove(key)
130                .map_err(|e| IndexError::backend(e.to_string()))?;
131        }
132
133        write_txn
134            .commit()
135            .map_err(|e| IndexError::backend(e.to_string()))?;
136        Ok(())
137    }
138
139    fn batch_put(&self, entries: Vec<(String, Vec<u8>)>) -> Result<(), IndexError> {
140        let write_txn = self
141            .db
142            .begin_write()
143            .map_err(|e| IndexError::backend(e.to_string()))?;
144
145        {
146            let mut table = write_txn
147                .open_table(UCFP_TABLE)
148                .map_err(|e| IndexError::backend(e.to_string()))?;
149
150            for (key, value) in entries {
151                table
152                    .insert(key.as_str(), value.as_slice())
153                    .map_err(|e| IndexError::backend(e.to_string()))?;
154            }
155        }
156
157        write_txn
158            .commit()
159            .map_err(|e| IndexError::backend(e.to_string()))?;
160        Ok(())
161    }
162
163    fn scan(
164        &self,
165        visitor: &mut dyn FnMut(&[u8]) -> Result<(), IndexError>,
166    ) -> Result<(), IndexError> {
167        let read_txn = self
168            .db
169            .begin_read()
170            .map_err(|e| IndexError::backend(e.to_string()))?;
171        let table = read_txn
172            .open_table(UCFP_TABLE)
173            .map_err(|e| IndexError::backend(e.to_string()))?;
174
175        for item in table
176            .iter()
177            .map_err(|e| IndexError::backend(e.to_string()))?
178        {
179            let (_, value) = item.map_err(|e| IndexError::backend(e.to_string()))?;
180            visitor(value.value())?;
181        }
182
183        Ok(())
184    }
185
186    fn flush(&self) -> Result<(), IndexError> {
187        // Redb commits are synchronous by default, so flush is a no-op
188        // This ensures data is immediately durable
189        Ok(())
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use tempfile::NamedTempFile;
197
198    #[test]
199    fn test_redb_backend_roundtrip() {
200        let temp_file = NamedTempFile::new().unwrap();
201        let backend = RedbBackend::open(temp_file.path()).unwrap();
202
203        // Test put and get
204        backend.put("key1", b"value1").unwrap();
205        let result = backend.get("key1").unwrap();
206        assert_eq!(result, Some(b"value1".to_vec()));
207
208        // Test non-existent key
209        let result = backend.get("nonexistent").unwrap();
210        assert_eq!(result, None);
211    }
212
213    #[test]
214    fn test_redb_backend_batch() {
215        let temp_file = NamedTempFile::new().unwrap();
216        let backend = RedbBackend::open(temp_file.path()).unwrap();
217
218        let entries = vec![
219            ("key1".to_string(), b"value1".to_vec()),
220            ("key2".to_string(), b"value2".to_vec()),
221            ("key3".to_string(), b"value3".to_vec()),
222        ];
223
224        backend.batch_put(entries).unwrap();
225
226        assert_eq!(backend.get("key1").unwrap(), Some(b"value1".to_vec()));
227        assert_eq!(backend.get("key2").unwrap(), Some(b"value2".to_vec()));
228        assert_eq!(backend.get("key3").unwrap(), Some(b"value3".to_vec()));
229    }
230
231    #[test]
232    fn test_redb_backend_delete() {
233        let temp_file = NamedTempFile::new().unwrap();
234        let backend = RedbBackend::open(temp_file.path()).unwrap();
235
236        backend.put("key1", b"value1").unwrap();
237        assert_eq!(backend.get("key1").unwrap(), Some(b"value1".to_vec()));
238
239        backend.delete("key1").unwrap();
240        assert_eq!(backend.get("key1").unwrap(), None);
241    }
242
243    #[test]
244    fn test_redb_backend_scan() {
245        let temp_file = NamedTempFile::new().unwrap();
246        let backend = RedbBackend::open(temp_file.path()).unwrap();
247
248        backend.put("key1", b"value1").unwrap();
249        backend.put("key2", b"value2").unwrap();
250
251        let mut collected = Vec::new();
252        backend
253            .scan(&mut |value| {
254                collected.push(value.to_vec());
255                Ok(())
256            })
257            .unwrap();
258
259        assert_eq!(collected.len(), 2);
260        assert!(collected.contains(&b"value1".to_vec()));
261        assert!(collected.contains(&b"value2".to_vec()));
262    }
263}