1pub mod tables;
8
9mod error;
10pub use error::StorageError;
11
12mod txn;
13pub use txn::{MultimapTableHandle, TableHandle, Txn};
14
15pub use redb::{
18 MultimapTableDefinition, ReadTransaction, ReadableDatabase, ReadableMultimapTable,
19 ReadableTable, ReadableTableMetadata, TableDefinition, WriteTransaction,
20};
21
22use std::fs::OpenOptions;
23use std::path::Path;
24
25pub const CURRENT_FORMAT_VERSION: u64 = 2;
32pub const OLDEST_SUPPORTED_FORMAT_VERSION: u64 = 2;
33
34pub struct StorageEngine {
35 db: redb::Database,
36}
37
38impl StorageEngine {
39 pub fn open_file(path: impl AsRef<Path>) -> Result<Self, StorageError> {
41 let db = redb::Database::create(path)?;
42 Self::from_db(db)
43 }
44
45 pub fn open_memory() -> Result<Self, StorageError> {
48 let backend = redb::backends::InMemoryBackend::new();
49 let db = redb::Database::builder().create_with_backend(backend)?;
50 Self::from_db(db)
51 }
52
53 fn from_db(db: redb::Database) -> Result<Self, StorageError> {
59 let write_txn = db.begin_write()?;
60 let is_fresh = write_txn.list_tables()?.next().is_none()
71 && write_txn.list_multimap_tables()?.next().is_none();
72 {
73 let mut meta = write_txn.open_table(tables::META)?;
74 let stored_version = meta.get("schema_version")?.map(|value| value.value());
75 match stored_version {
76 None if is_fresh => {
77 meta.insert("schema_version", CURRENT_FORMAT_VERSION)?;
78 }
79 None => {
84 drop(meta);
85 write_txn.abort()?;
86 return Err(StorageError::UnsupportedFormat {
87 found: 1,
88 oldest_supported: OLDEST_SUPPORTED_FORMAT_VERSION,
89 current: CURRENT_FORMAT_VERSION,
90 });
91 }
92 Some(found)
93 if !(OLDEST_SUPPORTED_FORMAT_VERSION..=CURRENT_FORMAT_VERSION)
94 .contains(&found) =>
95 {
96 drop(meta);
97 write_txn.abort()?;
98 return Err(StorageError::UnsupportedFormat {
99 found,
100 oldest_supported: OLDEST_SUPPORTED_FORMAT_VERSION,
101 current: CURRENT_FORMAT_VERSION,
102 });
103 }
104 Some(_) => {}
105 }
106 drop(meta);
107 write_txn.open_table(tables::LABEL_TO_ID)?;
108 write_txn.open_table(tables::ID_TO_LABEL)?;
109 write_txn.open_table(tables::NODES)?;
110 write_txn.open_table(tables::EDGES)?;
111 write_txn.open_table(tables::ADJ_OUT)?;
112 write_txn.open_table(tables::ADJ_IN)?;
113 write_txn.open_table(tables::REL_TYPE_COUNTS)?;
114 write_txn.open_multimap_table(tables::NODE_LABEL_INDEX)?;
115 write_txn.open_table(tables::PROP_TO_ID)?;
116 write_txn.open_table(tables::ID_TO_PROP)?;
117 write_txn.open_table(tables::INDEX_DEFS)?;
118 write_txn.open_multimap_table(tables::PROPERTY_INDEX)?;
119 }
120 write_txn.commit()?;
121 Ok(Self { db })
122 }
123
124 pub fn begin_write(&self) -> Result<WriteTransaction, StorageError> {
125 Ok(self.db.begin_write()?)
126 }
127
128 pub fn begin_read(&self) -> Result<ReadTransaction, StorageError> {
129 Ok(self.db.begin_read()?)
130 }
131
132 pub fn check_integrity(&mut self) -> Result<bool, StorageError> {
136 Ok(self.db.check_integrity()?)
137 }
138
139 pub fn backup_to(&self, path: impl AsRef<Path>) -> Result<(), StorageError> {
143 let path = path.as_ref();
144 let file = OpenOptions::new()
145 .read(true)
146 .write(true)
147 .create_new(true)
148 .open(path)?;
149
150 let result = (|| {
151 let source = self.db.begin_read()?;
152 let destination = redb::Database::builder().create_file(file)?;
153 let write = destination.begin_write()?;
154
155 macro_rules! copy_table {
156 ($definition:expr) => {{
157 let source_table = source.open_table($definition)?;
158 let mut destination_table = write.open_table($definition)?;
159 for entry in source_table.iter()? {
160 let (key, value) = entry?;
161 destination_table.insert(key.value(), value.value())?;
162 }
163 }};
164 }
165
166 macro_rules! copy_multimap {
167 ($definition:expr) => {{
168 let source_table = source.open_multimap_table($definition)?;
169 let mut destination_table = write.open_multimap_table($definition)?;
170 for entry in source_table.iter()? {
171 let (key, values) = entry?;
172 for value in values {
173 destination_table.insert(key.value(), value?.value())?;
174 }
175 }
176 }};
177 }
178
179 copy_table!(tables::META);
180 copy_table!(tables::LABEL_TO_ID);
181 copy_table!(tables::ID_TO_LABEL);
182 copy_table!(tables::NODES);
183 copy_table!(tables::EDGES);
184 copy_table!(tables::ADJ_OUT);
185 copy_table!(tables::ADJ_IN);
186 copy_table!(tables::REL_TYPE_COUNTS);
187 copy_multimap!(tables::NODE_LABEL_INDEX);
188 copy_table!(tables::PROP_TO_ID);
189 copy_table!(tables::ID_TO_PROP);
190 copy_table!(tables::INDEX_DEFS);
191 copy_multimap!(tables::PROPERTY_INDEX);
192
193 write.commit()?;
194 Ok::<(), StorageError>(())
195 })();
196
197 if result.is_err() {
198 let _ = std::fs::remove_file(path);
201 }
202 result
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn fresh_database_records_current_format_version() {
212 let engine = StorageEngine::open_memory().unwrap();
213 let read = engine.begin_read().unwrap();
214 let meta = read.open_table(tables::META).unwrap();
215 assert_eq!(
216 meta.get("schema_version").unwrap().unwrap().value(),
217 CURRENT_FORMAT_VERSION
218 );
219 }
220
221 #[test]
226 fn unversioned_v1_era_database_is_rejected_not_adopted() {
227 let dir = tempfile::tempdir().unwrap();
228 let path = dir.path().join("v1-era.redb");
229 {
230 let db = redb::Database::create(&path).unwrap();
231 let write = db.begin_write().unwrap();
232 {
233 write
237 .open_table(tables::NODES)
238 .unwrap()
239 .insert(1, &[0u8][..])
240 .unwrap();
241 write
242 .open_table(tables::META)
243 .unwrap()
244 .insert("next_node_id", 1)
245 .unwrap();
246 }
247 write.commit().unwrap();
248 }
249
250 let err = match StorageEngine::open_file(&path) {
251 Ok(_) => panic!("unversioned v1-era database unexpectedly opened"),
252 Err(err) => err,
253 };
254 assert!(matches!(
255 err,
256 StorageError::UnsupportedFormat { found: 1, .. }
257 ));
258 let db = redb::Database::create(&path).unwrap();
260 let read = db.begin_read().unwrap();
261 assert!(read
262 .open_table(tables::META)
263 .unwrap()
264 .get("schema_version")
265 .unwrap()
266 .is_none());
267 }
268
269 #[test]
270 fn database_from_newer_marsdb_is_rejected() {
271 let dir = tempfile::tempdir().unwrap();
272 let path = dir.path().join("future.redb");
273 {
274 let db = redb::Database::create(&path).unwrap();
275 let write = db.begin_write().unwrap();
276 {
277 let mut meta = write.open_table(tables::META).unwrap();
278 meta.insert("schema_version", CURRENT_FORMAT_VERSION + 1)
279 .unwrap();
280 }
281 write.commit().unwrap();
282 }
283
284 let err = match StorageEngine::open_file(&path) {
285 Ok(_) => panic!("newer database format unexpectedly opened"),
286 Err(err) => err,
287 };
288 assert!(matches!(
289 err,
290 StorageError::UnsupportedFormat {
291 found,
292 current: CURRENT_FORMAT_VERSION,
293 ..
294 } if found == CURRENT_FORMAT_VERSION + 1
295 ));
296 }
297
298 #[test]
299 fn backup_copies_all_tables_and_refuses_to_overwrite() {
300 let source = StorageEngine::open_memory().unwrap();
301 let write = source.begin_write().unwrap();
302 {
303 write
304 .open_table(tables::META)
305 .unwrap()
306 .insert("next_node_id", 7)
307 .unwrap();
308 write
309 .open_table(tables::LABEL_TO_ID)
310 .unwrap()
311 .insert("Person", 3)
312 .unwrap();
313 write
314 .open_table(tables::ID_TO_LABEL)
315 .unwrap()
316 .insert(3, "Person")
317 .unwrap();
318 write
319 .open_table(tables::NODES)
320 .unwrap()
321 .insert(6, &[1, 2, 3][..])
322 .unwrap();
323 write
324 .open_multimap_table(tables::NODE_LABEL_INDEX)
325 .unwrap()
326 .insert(3, 6)
327 .unwrap();
328 }
329 write.commit().unwrap();
330
331 let dir = tempfile::tempdir().unwrap();
332 let path = dir.path().join("backup.redb");
333 source.backup_to(&path).unwrap();
334
335 let backup = StorageEngine::open_file(&path).unwrap();
336 let read = backup.begin_read().unwrap();
337 assert_eq!(
338 read.open_table(tables::META)
339 .unwrap()
340 .get("next_node_id")
341 .unwrap()
342 .unwrap()
343 .value(),
344 7
345 );
346 assert_eq!(
347 read.open_table(tables::NODES)
348 .unwrap()
349 .get(6)
350 .unwrap()
351 .unwrap()
352 .value(),
353 &[1, 2, 3]
354 );
355 assert_eq!(
356 read.open_multimap_table(tables::NODE_LABEL_INDEX)
357 .unwrap()
358 .get(3)
359 .unwrap()
360 .next()
361 .unwrap()
362 .unwrap()
363 .value(),
364 6
365 );
366
367 assert!(matches!(source.backup_to(&path), Err(StorageError::Io(_))));
368 }
369
370 #[test]
377 fn backup_copies_every_table_that_exists_in_the_source() {
378 use redb::{MultimapTableHandle as _, TableHandle as _};
379 use std::collections::BTreeSet;
380
381 fn table_names(read: &ReadTransaction) -> BTreeSet<String> {
382 let mut names: BTreeSet<String> = read
383 .list_tables()
384 .unwrap()
385 .map(|t| t.name().to_string())
386 .collect();
387 names.extend(
388 read.list_multimap_tables()
389 .unwrap()
390 .map(|t| t.name().to_string()),
391 );
392 names
393 }
394
395 let source = StorageEngine::open_memory().unwrap();
396 let write = source.begin_write().unwrap();
400 write
401 .open_table(tables::PROP_TO_ID)
402 .unwrap()
403 .insert("email", 1)
404 .unwrap();
405 write
406 .open_table(tables::ID_TO_PROP)
407 .unwrap()
408 .insert(1, "email")
409 .unwrap();
410 write
411 .open_table(tables::INDEX_DEFS)
412 .unwrap()
413 .insert(&[0u8, 0, 0, 0, 0, 0, 0, 1][..], &[0u8][..])
414 .unwrap();
415 write
416 .open_multimap_table(tables::PROPERTY_INDEX)
417 .unwrap()
418 .insert(&[0u8, 0, 0, 0, 0, 0, 0, 1][..], 6)
419 .unwrap();
420 write.commit().unwrap();
421
422 let dir = tempfile::tempdir().unwrap();
423 let path = dir.path().join("completeness.redb");
424 source.backup_to(&path).unwrap();
425 let backup = StorageEngine::open_file(&path).unwrap();
426
427 let source_tables = table_names(&source.begin_read().unwrap());
428 let backup_tables = table_names(&backup.begin_read().unwrap());
429 assert_eq!(source_tables, backup_tables);
430 }
431}