use std::path::Path;
use byteorder::BE;
use heed::{
Database, DatabaseFlags, Env, EnvOpenOptions,
types::{Bytes, Str, U64, Unit},
};
use crate::error::Error;
pub struct Storage {
pub env: Env,
pub nodes: Database<U64<BE>, Bytes>, pub edges: Database<U64<BE>, Bytes>,
pub out_adj: Database<U64<BE>, Bytes>, pub in_adj: Database<U64<BE>, Bytes>,
pub label_idx: Database<Bytes, Unit>, pub type_idx: Database<Bytes, Unit>,
pub node_prop_idx: Database<Bytes, Unit>,
pub edge_prop_idx: Database<Bytes, Unit>,
pub fts_postings: Database<Bytes, Bytes>, pub fts_docs: Database<Bytes, Bytes>,
pub vectors: Database<U64<BE>, Bytes>,
pub meta: Database<Str, Bytes>,
pub db_id: [u8; 16],
}
pub type RoTxn<'a> = heed::RoTxn<'a>;
pub type OwnedRoTxn<'a> = heed::RoTxn<'a, heed::WithTls>;
pub type RwTxn<'a> = heed::RwTxn<'a>;
pub type StorageError = heed::Error;
impl Storage {
pub fn copy_to_file(&self, destination: &Path, compact: bool) -> Result<(), Error> {
let option = if compact {
heed::CompactionOption::Enabled
} else {
heed::CompactionOption::Disabled
};
self.env
.copy_to_path(destination, option)
.map(|_| ())
.map_err(Error::Storage)
}
pub fn restore_from_file(snapshot_file: &Path, dst_dir: &Path) -> Result<(), Error> {
let dst_file = dst_dir.join("data.mdb");
if dst_file.exists() {
return Err(Error::InvalidArgument(format!(
"{} already contains a database (data.mdb); restore into a new or \
empty directory rather than overwriting it",
dst_dir.display()
)));
}
std::fs::create_dir_all(dst_dir)?;
for entry in std::fs::read_dir(dst_dir)? {
let path = entry?.path();
if path.extension().is_some_and(|ext| ext == "cache") {
std::fs::remove_file(&path)?;
}
}
std::fs::copy(snapshot_file, &dst_file)?;
Ok(())
}
pub fn open(path: &Path, map_size_gb: usize) -> Result<Self, Error> {
std::fs::create_dir_all(path)?;
let env = unsafe {
EnvOpenOptions::new()
.map_size(map_size_gb * 1024 * 1024 * 1024)
.max_dbs(12)
.open(path)?
};
let mut wtxn = env.write_txn()?;
let nodes = env.create_database(&mut wtxn, Some("nodes"))?;
let edges = env.create_database(&mut wtxn, Some("edges"))?;
let out_adj = env
.database_options()
.types::<U64<BE>, Bytes>()
.name("out_adj")
.flags(DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED)
.create(&mut wtxn)?;
let in_adj = env
.database_options()
.types::<U64<BE>, Bytes>()
.name("in_adj")
.flags(DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED)
.create(&mut wtxn)?;
let label_idx = env.create_database(&mut wtxn, Some("label_idx"))?;
let type_idx = env.create_database(&mut wtxn, Some("type_idx"))?;
let node_prop_idx = env.create_database(&mut wtxn, Some("node_prop_idx"))?;
let edge_prop_idx = env.create_database(&mut wtxn, Some("edge_prop_idx"))?;
let fts_postings = env
.database_options()
.types::<Bytes, Bytes>()
.name("fts_postings")
.flags(DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED)
.create(&mut wtxn)?;
let fts_docs = env.create_database(&mut wtxn, Some("fts_docs"))?;
let vectors = env.create_database(&mut wtxn, Some("vectors"))?;
let meta: Database<Str, Bytes> = env.create_database(&mut wtxn, Some("meta"))?;
let db_id = match meta.get(&wtxn, DB_ID_KEY)? {
Some(bytes) => bytes
.try_into()
.map_err(|_| Error::Corrupt("db_id must be 16 bytes"))?,
None => {
let id = generate_db_id();
meta.put(&mut wtxn, DB_ID_KEY, &id)?;
id
}
};
wtxn.commit()?;
Ok(Self {
env,
nodes,
edges,
out_adj,
in_adj,
label_idx,
type_idx,
node_prop_idx,
edge_prop_idx,
fts_postings,
fts_docs,
vectors,
meta,
db_id,
})
}
}
const DB_ID_KEY: &str = "db_id";
fn generate_db_id() -> [u8; 16] {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut id = [0u8; 16];
for (i, chunk) in id.chunks_mut(8).enumerate() {
let mut hasher = RandomState::new().build_hasher();
hasher.write_u128(nanos);
hasher.write_usize(i);
chunk.copy_from_slice(&hasher.finish().to_le_bytes());
}
id
}