#![forbid(unsafe_code)]
#![deny(missing_docs)]
pub mod blockstore;
pub mod knn_edges_store;
pub mod op_heads;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use mnem_core::error::{Error, StoreError};
use mnem_core::store::{Blockstore, OpHeadsStore};
use redb::{Database, TableDefinition};
pub use blockstore::RedbBlockstore;
pub use knn_edges_store::{KNN_EDGES_TABLE, load_knn_edges, store_knn_edges};
pub use op_heads::RedbOpHeadsStore;
pub(crate) const OBJECTS_TABLE: TableDefinition<'_, &[u8], &[u8]> =
TableDefinition::new("mnem_objects");
pub(crate) const OP_HEADS_TABLE: TableDefinition<'_, &[u8], ()> =
TableDefinition::new("mnem_op_heads");
pub(crate) fn redb_err<E: std::fmt::Display>(e: E) -> StoreError {
StoreError::Io(format!("redb: {e}"))
}
pub fn open_or_init(
path: impl AsRef<Path>,
) -> Result<(Arc<dyn Blockstore>, Arc<dyn OpHeadsStore>, PathBuf), Error> {
let path = path.as_ref().to_owned();
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)
.map_err(|e| StoreError::Io(format!("create parent dir: {e}")))?;
}
let db = Database::create(&path).map_err(redb_err)?;
let tx = db.begin_write().map_err(redb_err)?;
{
let _ = tx.open_table(OBJECTS_TABLE).map_err(redb_err)?;
let _ = tx.open_table(OP_HEADS_TABLE).map_err(redb_err)?;
}
tx.commit().map_err(redb_err)?;
let db = Arc::new(db);
let bs: Arc<dyn Blockstore> = Arc::new(RedbBlockstore::new(db.clone()));
let ohs: Arc<dyn OpHeadsStore> = Arc::new(RedbOpHeadsStore::new(db));
Ok((bs, ohs, path))
}
#[cfg(test)]
mod tests {
use super::*;
use mnem_core::repo::ReadonlyRepo;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn tmp_file(name: &str) -> PathBuf {
let path = std::env::temp_dir().join(format!(
"mnem-redb-{name}-{}-{}.redb",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
));
let _ = std::fs::remove_file(&path);
path
}
#[test]
fn init_creates_file() {
let p = tmp_file("init");
let (_, _, file) = open_or_init(&p).unwrap();
assert!(file.exists());
}
#[test]
fn init_is_idempotent() {
let p = tmp_file("idem");
let _ = open_or_init(&p).unwrap();
let _ = open_or_init(&p).unwrap();
let _ = open_or_init(&p).unwrap();
}
#[test]
fn full_repo_persists_across_reopens() {
let p = tmp_file("persist");
let op_at_close = {
let (bs, ohs, _) = open_or_init(&p).unwrap();
let repo = ReadonlyRepo::init(bs.clone(), ohs.clone()).unwrap();
let mut tx = repo.start_transaction();
let alice = mnem_core::objects::Node::new(mnem_core::id::NodeId::new_v7(), "Person");
let alice_id = alice.id;
tx.add_node(&alice).unwrap();
let r1 = tx.commit("a@example.org", "add Alice").unwrap();
assert!(r1.lookup_node(&alice_id).unwrap().is_some());
r1.op_id().clone()
};
{
let (bs, ohs, _) = open_or_init(&p).unwrap();
let repo = ReadonlyRepo::open(bs, ohs).unwrap();
assert_eq!(*repo.op_id(), op_at_close);
}
}
}