1use std::path::Path;
2
3use redb::{Database, ReadableTable, TableDefinition};
4
5use crate::entry::Entry;
6use crate::oplog::{OpLog, OpLogError};
7
8const ENTRIES_TABLE: TableDefinition<&[u8], &[u8]> = TableDefinition::new("entries");
10
11const META_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("meta");
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum FlushMode {
17 Immediate,
20 Deferred,
24}
25
26pub struct Store {
33 db: Database,
34 pub oplog: OpLog,
35 flush_mode: FlushMode,
36 pending: Vec<Entry>,
38}
39
40impl Store {
41 pub fn open(path: &Path, genesis: Option<Entry>) -> Result<Self, StoreError> {
46 let db = Database::create(path).map_err(|e| StoreError::Io(e.to_string()))?;
47
48 #[cfg(unix)]
50 {
51 use std::os::unix::fs::PermissionsExt;
52 let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
53 }
54
55 {
57 let txn = db
58 .begin_write()
59 .map_err(|e| StoreError::Io(e.to_string()))?;
60 {
61 let _t = txn
62 .open_table(ENTRIES_TABLE)
63 .map_err(|e| StoreError::Io(e.to_string()))?;
64 let _m = txn
65 .open_table(META_TABLE)
66 .map_err(|e| StoreError::Io(e.to_string()))?;
67 }
68 txn.commit().map_err(|e| StoreError::Io(e.to_string()))?;
69 }
70
71 let existing_entries = Self::load_entries(&db)?;
73
74 if !existing_entries.is_empty() {
75 let oplog = Self::reconstruct_oplog(existing_entries)?;
77 return Ok(Self {
78 db,
79 oplog,
80 flush_mode: FlushMode::Immediate,
81 pending: Vec::new(),
82 });
83 }
84
85 let genesis = genesis.ok_or(StoreError::NoGenesis)?;
87 let oplog = OpLog::new(genesis.clone());
88
89 let store = Self {
91 db,
92 oplog,
93 flush_mode: FlushMode::Immediate,
94 pending: Vec::new(),
95 };
96 store.persist_entry_and_heads(&genesis)?;
97
98 Ok(store)
99 }
100
101 pub fn append(&mut self, entry: Entry) -> Result<bool, StoreError> {
103 let inserted = self
104 .oplog
105 .append(entry.clone())
106 .map_err(StoreError::OpLog)?;
107 if inserted {
108 match self.flush_mode {
109 FlushMode::Immediate => self.persist_entry_and_heads(&entry)?,
110 FlushMode::Deferred => self.pending.push(entry),
111 }
112 }
113 Ok(inserted)
114 }
115
116 pub fn set_flush_mode(&mut self, mode: FlushMode) {
118 self.flush_mode = mode;
119 }
120
121 pub fn flush(&mut self) -> Result<usize, StoreError> {
124 if self.pending.is_empty() {
125 return Ok(0);
126 }
127 let count = self.pending.len();
128 let entries: Vec<Entry> = self.pending.drain(..).collect();
129 self.persist_entries_and_heads(&entries)?;
130 Ok(count)
131 }
132
133 pub fn pending_count(&self) -> usize {
135 self.pending.len()
136 }
137
138 pub fn merge(&mut self, entries: &[Entry]) -> Result<usize, StoreError> {
144 let mut inserted = 0;
145 let mut new_entries: Vec<Entry> = Vec::new();
146 let mut remaining: Vec<&Entry> = entries.iter().collect();
147 let mut max_passes = remaining.len() + 1;
148
149 while !remaining.is_empty() && max_passes > 0 {
150 let mut next_remaining = Vec::new();
151 for entry in &remaining {
152 match self.oplog.append((*entry).clone()) {
153 Ok(true) => {
154 new_entries.push((*entry).clone());
155 inserted += 1;
156 }
157 Ok(false) => {
158 }
160 Err(crate::oplog::OpLogError::MissingParent(_)) => {
161 next_remaining.push(*entry);
162 }
163 Err(crate::oplog::OpLogError::InvalidHash) => {
164 return Err(StoreError::Io(format!(
165 "invalid hash for entry {}",
166 hex::encode(entry.hash)
167 )));
168 }
169 }
170 }
171 if next_remaining.len() == remaining.len() {
172 return Err(StoreError::Io(format!(
173 "{} entries have unresolvable parents",
174 remaining.len()
175 )));
176 }
177 remaining = next_remaining;
178 max_passes -= 1;
179 }
180
181 if !new_entries.is_empty() {
182 match self.flush_mode {
183 FlushMode::Immediate => self.persist_entries_and_heads(&new_entries)?,
184 FlushMode::Deferred => self.pending.extend(new_entries),
185 }
186 }
187
188 Ok(inserted)
189 }
190
191 pub fn replace_with_checkpoint(&mut self, checkpoint: Entry) -> Result<(), StoreError> {
193 let txn = self
194 .db
195 .begin_write()
196 .map_err(|e| StoreError::Io(e.to_string()))?;
197 {
198 let mut table = txn
199 .open_table(ENTRIES_TABLE)
200 .map_err(|e| StoreError::Io(e.to_string()))?;
201 let keys: Vec<Vec<u8>> = table
203 .iter()
204 .map_err(|e| StoreError::Io(e.to_string()))?
205 .filter_map(|r| r.ok().map(|(k, _)| k.value().to_vec()))
206 .collect();
207 for key in keys {
208 table
209 .remove(key.as_slice())
210 .map_err(|e| StoreError::Io(e.to_string()))?;
211 }
212 let entry_bytes = checkpoint.to_bytes();
214 table
215 .insert(checkpoint.hash.as_slice(), entry_bytes.as_slice())
216 .map_err(|e| StoreError::Io(e.to_string()))?;
217 }
218 {
219 let mut meta = txn
220 .open_table(META_TABLE)
221 .map_err(|e| StoreError::Io(e.to_string()))?;
222 let heads_bytes = rmp_serde::to_vec(&vec![checkpoint.hash])
223 .map_err(|e| StoreError::Io(e.to_string()))?;
224 meta.insert("heads", heads_bytes.as_slice())
225 .map_err(|e| StoreError::Io(e.to_string()))?;
226 }
227 txn.commit().map_err(|e| StoreError::Io(e.to_string()))?;
228
229 self.oplog.replace_with_checkpoint(checkpoint);
231
232 Ok(())
233 }
234
235 pub fn reclaim_disk(&mut self) -> Result<bool, StoreError> {
239 self.db.compact().map_err(|e| StoreError::Io(e.to_string()))
240 }
241
242 fn persist_entry_and_heads(&self, entry: &Entry) -> Result<(), StoreError> {
244 let txn = self
245 .db
246 .begin_write()
247 .map_err(|e| StoreError::Io(e.to_string()))?;
248 {
249 let mut entries_table = txn
250 .open_table(ENTRIES_TABLE)
251 .map_err(|e| StoreError::Io(e.to_string()))?;
252 let bytes = entry.to_bytes();
253 entries_table
254 .insert(entry.hash.as_slice(), bytes.as_slice())
255 .map_err(|e| StoreError::Io(e.to_string()))?;
256 }
257 {
258 let mut meta_table = txn
259 .open_table(META_TABLE)
260 .map_err(|e| StoreError::Io(e.to_string()))?;
261 let heads = self.oplog.heads();
262 let heads_bytes =
263 rmp_serde::to_vec(&heads).map_err(|e| StoreError::Io(e.to_string()))?;
264 meta_table
265 .insert("heads", heads_bytes.as_slice())
266 .map_err(|e| StoreError::Io(e.to_string()))?;
267 }
268 txn.commit().map_err(|e| StoreError::Io(e.to_string()))?;
269 Ok(())
270 }
271
272 fn persist_entries_and_heads(&self, entries: &[Entry]) -> Result<(), StoreError> {
274 let txn = self
275 .db
276 .begin_write()
277 .map_err(|e| StoreError::Io(e.to_string()))?;
278 {
279 let mut entries_table = txn
280 .open_table(ENTRIES_TABLE)
281 .map_err(|e| StoreError::Io(e.to_string()))?;
282 for entry in entries {
283 let bytes = entry.to_bytes();
284 entries_table
285 .insert(entry.hash.as_slice(), bytes.as_slice())
286 .map_err(|e| StoreError::Io(e.to_string()))?;
287 }
288 }
289 {
290 let mut meta_table = txn
291 .open_table(META_TABLE)
292 .map_err(|e| StoreError::Io(e.to_string()))?;
293 let heads = self.oplog.heads();
294 let heads_bytes =
295 rmp_serde::to_vec(&heads).map_err(|e| StoreError::Io(e.to_string()))?;
296 meta_table
297 .insert("heads", heads_bytes.as_slice())
298 .map_err(|e| StoreError::Io(e.to_string()))?;
299 }
300 txn.commit().map_err(|e| StoreError::Io(e.to_string()))?;
301 Ok(())
302 }
303
304 fn load_entries(db: &Database) -> Result<Vec<Entry>, StoreError> {
306 let txn = db.begin_read().map_err(|e| StoreError::Io(e.to_string()))?;
307 let table = match txn.open_table(ENTRIES_TABLE) {
308 Ok(t) => t,
309 Err(_) => return Ok(vec![]),
310 };
311
312 let mut entries = Vec::new();
313 let iter = table.iter().map_err(|e| StoreError::Io(e.to_string()))?;
314 for result in iter {
315 let (_, value) = result.map_err(|e| StoreError::Io(e.to_string()))?;
316 let entry = Entry::from_bytes(value.value())
317 .map_err(|e| StoreError::Io(format!("corrupt entry: {e}")))?;
318 entries.push(entry);
319 }
320 Ok(entries)
321 }
322
323 fn reconstruct_oplog(entries: Vec<Entry>) -> Result<OpLog, StoreError> {
329 use std::collections::{HashMap, HashSet, VecDeque};
330
331 if entries.is_empty() {
332 return Err(StoreError::Io("no entries to reconstruct".into()));
333 }
334
335 let mut by_hash: HashMap<crate::entry::Hash, Entry> = HashMap::new();
337 let mut roots: Vec<Entry> = Vec::new();
338 for entry in entries {
339 if entry.next.is_empty() {
340 roots.push(entry.clone());
341 }
342 by_hash.insert(entry.hash, entry);
343 }
344
345 if roots.is_empty() {
346 return Err(StoreError::Io("no genesis entry found".into()));
347 }
348
349 let genesis = roots[0].clone();
352 let genesis_hash = genesis.hash;
353 let mut oplog = OpLog::new(genesis);
354
355 let mut resolved: HashSet<crate::entry::Hash> = HashSet::new();
357 resolved.insert(genesis_hash);
358
359 for root in &roots[1..] {
363 resolved.insert(root.hash);
364 let _ = oplog.append(root.clone());
366 }
367
368 let mut children_of: HashMap<crate::entry::Hash, Vec<crate::entry::Hash>> = HashMap::new();
370 let mut pending_parents: HashMap<crate::entry::Hash, HashSet<crate::entry::Hash>> =
371 HashMap::new();
372
373 for (hash, entry) in &by_hash {
374 if resolved.contains(hash) {
375 continue;
376 }
377 let parents: HashSet<_> = entry.next.iter().copied().collect();
378 pending_parents.insert(*hash, parents.clone());
379 for parent in &parents {
380 children_of.entry(*parent).or_default().push(*hash);
381 }
382 }
383
384 let mut ready: VecDeque<crate::entry::Hash> = VecDeque::new();
386
387 for root_hash in &resolved {
388 if let Some(kids) = children_of.get(root_hash) {
389 for kid in kids {
390 if let Some(pp) = pending_parents.get_mut(kid) {
391 pp.remove(root_hash);
392 if pp.is_empty() {
393 ready.push_back(*kid);
394 }
395 }
396 }
397 }
398 }
399
400 while let Some(hash) = ready.pop_front() {
401 if let Some(entry) = by_hash.get(&hash) {
402 match oplog.append(entry.clone()) {
403 Ok(_) => {}
404 Err(e) => {
405 return Err(StoreError::Io(format!("reconstruct failed: {e}")));
406 }
407 }
408 if let Some(kids) = children_of.get(&hash) {
410 for kid in kids {
411 if let Some(pp) = pending_parents.get_mut(kid) {
412 pp.remove(&hash);
413 if pp.is_empty() {
414 ready.push_back(*kid);
415 }
416 }
417 }
418 }
419 }
420 }
421
422 let unresolved: Vec<_> = pending_parents
424 .iter()
425 .filter(|(_, parents)| !parents.is_empty())
426 .collect();
427 if !unresolved.is_empty() {
428 return Err(StoreError::Io(format!(
429 "could not reconstruct oplog: {} entries with unresolvable parents",
430 unresolved.len()
431 )));
432 }
433
434 Ok(oplog)
435 }
436}
437
438#[derive(Debug)]
439pub enum StoreError {
440 Io(String),
441 NoGenesis,
442 OpLog(OpLogError),
443}
444
445impl std::fmt::Display for StoreError {
446 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447 match self {
448 StoreError::Io(msg) => write!(f, "store I/O error: {msg}"),
449 StoreError::NoGenesis => write!(f, "no genesis entry provided for new store"),
450 StoreError::OpLog(e) => write!(f, "oplog error: {e}"),
451 }
452 }
453}
454
455impl std::error::Error for StoreError {}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460 use crate::clock::LamportClock;
461 use crate::entry::{GraphOp, Hash};
462 use crate::ontology::{NodeTypeDef, Ontology};
463 use std::collections::BTreeMap;
464
465 fn test_ontology() -> Ontology {
466 Ontology {
467 node_types: BTreeMap::from([(
468 "entity".into(),
469 NodeTypeDef {
470 description: None,
471 properties: BTreeMap::new(),
472 subtypes: None,
473 parent_type: None,
474 },
475 )]),
476 edge_types: BTreeMap::new(),
477 }
478 }
479
480 fn genesis() -> Entry {
481 Entry::new(
482 GraphOp::DefineOntology {
483 ontology: test_ontology(),
484 },
485 vec![],
486 vec![],
487 LamportClock::new("test"),
488 "test",
489 )
490 }
491
492 fn add_node_op(id: &str) -> GraphOp {
493 GraphOp::AddNode {
494 node_id: id.into(),
495 node_type: "entity".into(),
496 label: id.into(),
497 properties: BTreeMap::new(),
498 subtype: None,
499 }
500 }
501
502 fn make_entry(op: GraphOp, next: Vec<Hash>, clock_time: u64) -> Entry {
503 Entry::new(
504 op,
505 next,
506 vec![],
507 LamportClock::with_values("test", clock_time, 0),
508 "test",
509 )
510 }
511
512 #[test]
513 fn open_creates_file() {
514 let dir = tempfile::tempdir().unwrap();
515 let path = dir.path().join("test.redb");
516 assert!(!path.exists());
517
518 let store = Store::open(&path, Some(genesis())).unwrap();
519 assert!(path.exists());
520 assert_eq!(store.oplog.len(), 1);
521 }
522
523 #[test]
524 fn open_existing_loads_state() {
525 let dir = tempfile::tempdir().unwrap();
526 let path = dir.path().join("test.redb");
527 let g = genesis();
528
529 {
531 let mut store = Store::open(&path, Some(g.clone())).unwrap();
532 let e1 = make_entry(add_node_op("n1"), vec![g.hash], 2);
533 let e2 = make_entry(add_node_op("n2"), vec![e1.hash], 3);
534 store.append(e1).unwrap();
535 store.append(e2).unwrap();
536 assert_eq!(store.oplog.len(), 3);
537 }
538
539 {
541 let store = Store::open(&path, None).unwrap();
542 assert_eq!(store.oplog.len(), 3);
543 let heads = store.oplog.heads();
544 assert_eq!(heads.len(), 1);
545 }
546 }
547
548 #[test]
549 fn new_store_without_genesis_fails() {
550 let dir = tempfile::tempdir().unwrap();
551 let path = dir.path().join("test.redb");
552 match Store::open(&path, None) {
553 Err(StoreError::NoGenesis) => {} Ok(_) => panic!("expected NoGenesis error, got Ok"),
555 Err(e) => panic!("expected NoGenesis, got {e}"),
556 }
557 }
558
559 #[test]
560 fn append_persists_across_reopen() {
561 let dir = tempfile::tempdir().unwrap();
562 let path = dir.path().join("test.redb");
563 let g = genesis();
564
565 let e1 = make_entry(add_node_op("n1"), vec![g.hash], 2);
566 let e1_hash = e1.hash;
567
568 {
569 let mut store = Store::open(&path, Some(g.clone())).unwrap();
570 store.append(e1).unwrap();
571 }
572
573 {
574 let store = Store::open(&path, None).unwrap();
575 assert_eq!(store.oplog.len(), 2);
576 assert!(store.oplog.get(&e1_hash).is_some());
577 }
578 }
579
580 #[test]
581 fn concurrent_readers_ok() {
582 use std::thread;
583
584 let dir = tempfile::tempdir().unwrap();
585 let path = dir.path().join("test.redb");
586 let g = genesis();
587
588 let mut store = Store::open(&path, Some(g.clone())).unwrap();
589 for i in 0..10 {
590 let next = store.oplog.heads();
591 let e = make_entry(add_node_op(&format!("n{i}")), next, (i + 2) as u64);
592 store.append(e).unwrap();
593 }
594
595 thread::scope(|s| {
597 for _ in 0..4 {
598 s.spawn(|| {
599 let txn = store.db.begin_read().unwrap();
600 let table = txn.open_table(ENTRIES_TABLE).unwrap();
601 let count = table.iter().unwrap().count();
602 assert_eq!(count, 11); });
604 }
605 });
606 }
607}