1#![forbid(unsafe_code)]
46#![deny(missing_docs)]
47
48pub mod blockstore;
49pub mod knn_edges_store;
50pub mod op_heads;
51
52use std::path::{Path, PathBuf};
53use std::sync::Arc;
54
55use mnem_core::error::{Error, StoreError};
56use mnem_core::store::{Blockstore, OpHeadsStore};
57use redb::{Database, TableDefinition};
58
59pub use blockstore::RedbBlockstore;
60pub use knn_edges_store::{KNN_EDGES_TABLE, load_knn_edges, store_knn_edges};
61pub use op_heads::RedbOpHeadsStore;
62
63pub(crate) const OBJECTS_TABLE: TableDefinition<'_, &[u8], &[u8]> =
65 TableDefinition::new("mnem_objects");
66
67pub(crate) const OP_HEADS_TABLE: TableDefinition<'_, &[u8], ()> =
69 TableDefinition::new("mnem_op_heads");
70
71pub(crate) fn redb_err<E: std::fmt::Display>(e: E) -> StoreError {
73 StoreError::Io(format!("redb: {e}"))
74}
75
76pub fn open_or_init(
87 path: impl AsRef<Path>,
88) -> Result<(Arc<dyn Blockstore>, Arc<dyn OpHeadsStore>, PathBuf), Error> {
89 let path = path.as_ref().to_owned();
90 if let Some(parent) = path.parent()
91 && !parent.as_os_str().is_empty()
92 {
93 std::fs::create_dir_all(parent)
94 .map_err(|e| StoreError::Io(format!("create parent dir: {e}")))?;
95 }
96
97 let db = Database::create(&path).map_err(redb_err)?;
98 let tx = db.begin_write().map_err(redb_err)?;
100 {
101 let _ = tx.open_table(OBJECTS_TABLE).map_err(redb_err)?;
102 let _ = tx.open_table(OP_HEADS_TABLE).map_err(redb_err)?;
103 }
104 tx.commit().map_err(redb_err)?;
105
106 let db = Arc::new(db);
107 let bs: Arc<dyn Blockstore> = Arc::new(RedbBlockstore::new(db.clone()));
108 let ohs: Arc<dyn OpHeadsStore> = Arc::new(RedbOpHeadsStore::new(db));
109 Ok((bs, ohs, path))
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use mnem_core::repo::ReadonlyRepo;
116 use std::sync::atomic::{AtomicU64, Ordering};
117
118 static COUNTER: AtomicU64 = AtomicU64::new(0);
119 fn tmp_file(name: &str) -> PathBuf {
120 let path = std::env::temp_dir().join(format!(
121 "mnem-redb-{name}-{}-{}.redb",
122 std::process::id(),
123 COUNTER.fetch_add(1, Ordering::Relaxed)
124 ));
125 let _ = std::fs::remove_file(&path);
126 path
127 }
128
129 #[test]
130 fn init_creates_file() {
131 let p = tmp_file("init");
132 let (_, _, file) = open_or_init(&p).unwrap();
133 assert!(file.exists());
134 }
135
136 #[test]
137 fn init_is_idempotent() {
138 let p = tmp_file("idem");
139 let _ = open_or_init(&p).unwrap();
140 let _ = open_or_init(&p).unwrap();
141 let _ = open_or_init(&p).unwrap();
142 }
143
144 #[test]
145 fn full_repo_persists_across_reopens() {
146 let p = tmp_file("persist");
147 let op_at_close = {
148 let (bs, ohs, _) = open_or_init(&p).unwrap();
149 let repo = ReadonlyRepo::init(bs.clone(), ohs.clone()).unwrap();
150 let mut tx = repo.start_transaction();
151 let alice = mnem_core::objects::Node::new(mnem_core::id::NodeId::new_v7(), "Person");
152 let alice_id = alice.id;
153 tx.add_node(&alice).unwrap();
154 let r1 = tx.commit("a@example.org", "add Alice").unwrap();
155 assert!(r1.lookup_node(&alice_id).unwrap().is_some());
156 r1.op_id().clone()
157 };
158
159 {
161 let (bs, ohs, _) = open_or_init(&p).unwrap();
162 let repo = ReadonlyRepo::open(bs, ohs).unwrap();
163 assert_eq!(*repo.op_id(), op_at_close);
164 }
165 }
166}