content_index/backend/
redb.rs1use crate::{IndexBackend, IndexError};
22use redb::{Database, ReadableTable, TableDefinition};
23use std::path::Path;
24use std::sync::Arc;
25
26const UCFP_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("ucfp_data");
28
29pub struct RedbBackend {
38 db: Arc<Database>,
39}
40
41impl RedbBackend {
42 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 let write_txn = db
62 .begin_write()
63 .map_err(|e| IndexError::backend(e.to_string()))?;
64 {
65 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 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 backend.put("key1", b"value1").unwrap();
205 let result = backend.get("key1").unwrap();
206 assert_eq!(result, Some(b"value1".to_vec()));
207
208 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}