use std::collections::{BTreeSet, HashMap};
use std::path::Path;
use crate::ids::{EdgeLabel, NodeId};
use crate::manifest::{Manifest, SegmentMeta};
use crate::memtable::Memtable;
use crate::segment::Segment;
use crate::store::LocalStore;
use crate::{Error, Result};
pub struct Graph {
memtable: Memtable,
segments: Vec<Segment>,
store: Option<LocalStore>,
key_index: HashMap<String, NodeId>,
pending_keys: Vec<(String, NodeId)>,
labels: Vec<String>,
next_id: u64,
generation: u64,
committed_edges: u64,
}
impl Graph {
pub fn new() -> Self {
Self {
memtable: Memtable::new(),
segments: Vec::new(),
store: None,
key_index: HashMap::new(),
pending_keys: Vec::new(),
labels: Vec::new(),
next_id: 0,
generation: 0,
committed_edges: 0,
}
}
pub fn open(dir: impl AsRef<Path>) -> Result<Self> {
let store = LocalStore::open(dir)?;
let manifest = store.load_manifest()?.unwrap_or_else(Manifest::empty);
let mut segments = Vec::with_capacity(manifest.segments.len());
let mut key_index = HashMap::new();
for meta in &manifest.segments {
let segment = Segment::decode(&store.read_segment(&meta.name)?)?;
for (key, node) in segment.keys() {
key_index.insert(key.to_owned(), node);
}
segments.push(segment);
}
Ok(Self {
memtable: Memtable::new(),
segments,
store: Some(store),
key_index,
pending_keys: Vec::new(),
labels: manifest.labels,
next_id: manifest.next_node_id,
generation: manifest.generation,
committed_edges: manifest.edge_count,
})
}
pub fn upsert_node(&mut self, key: &str) -> NodeId {
if let Some(&id) = self.key_index.get(key) {
return id;
}
let id = NodeId(self.next_id);
self.next_id += 1;
self.key_index.insert(key.to_owned(), id);
self.pending_keys.push((key.to_owned(), id));
self.memtable.insert_node(id);
id
}
pub fn node(&self, key: &str) -> Option<NodeId> {
self.key_index.get(key).copied()
}
pub fn label(&mut self, name: &str) -> EdgeLabel {
if let Some(pos) = self.labels.iter().position(|l| l == name) {
return EdgeLabel(pos as u32);
}
self.labels.push(name.to_owned());
EdgeLabel((self.labels.len() - 1) as u32)
}
pub fn add_edge(&mut self, src: NodeId, label: EdgeLabel, dst: NodeId) -> Result<()> {
if self
.segments
.iter()
.any(|s| s.out(src, label).any(|n| n == dst))
{
return Ok(());
}
self.memtable.insert_edge(src, label, dst)
}
pub fn out(&self, src: NodeId, label: EdgeLabel) -> impl Iterator<Item = NodeId> {
let mut merged: BTreeSet<NodeId> = self.memtable.out(src, label).collect();
for segment in &self.segments {
merged.extend(segment.out(src, label));
}
merged.into_iter()
}
pub fn r#in(&self, dst: NodeId, label: EdgeLabel) -> impl Iterator<Item = NodeId> {
let mut merged: BTreeSet<NodeId> = self.memtable.r#in(dst, label).collect();
for segment in &self.segments {
merged.extend(segment.r#in(dst, label));
}
merged.into_iter()
}
pub fn flush(&mut self) -> Result<u64> {
let store = self.store.as_ref().ok_or(Error::NoStore)?;
if self.memtable.is_empty() && self.pending_keys.is_empty() {
return Ok(self.generation);
}
self.pending_keys.sort_by(|(a, _), (b, _)| a.cmp(b));
let bytes = Segment::encode(
self.memtable.iter_fwd(),
self.memtable.iter_rev(),
&self.pending_keys,
);
let generation = self.generation + 1;
let name = format!("seg-{generation:08}.seg");
store.write_segment(&name, &bytes)?;
let edges = self.memtable.edge_count() as u64;
let mut segments_meta: Vec<SegmentMeta> = self
.manifest_segments()
.unwrap_or_default();
segments_meta.push(SegmentMeta {
name: name.clone(),
level: 0,
edges,
bytes: bytes.len() as u64,
});
let manifest = Manifest {
format_version: crate::manifest::FORMAT_VERSION,
generation,
labels: self.labels.clone(),
next_node_id: self.next_id,
node_count: self.key_index.len() as u64,
edge_count: self.committed_edges + edges,
segments: segments_meta,
};
store.commit_manifest(&manifest)?;
self.segments.push(Segment::decode(&bytes)?);
self.memtable.clear();
self.pending_keys.clear();
self.generation = generation;
self.committed_edges += edges;
Ok(generation)
}
fn manifest_segments(&self) -> Option<Vec<SegmentMeta>> {
let store = self.store.as_ref()?;
store.load_manifest().ok().flatten().map(|m| m.segments)
}
pub fn generation(&self) -> u64 {
self.generation
}
pub fn node_count(&self) -> u64 {
self.key_index.len() as u64
}
pub fn edge_count(&self) -> u64 {
self.committed_edges + self.memtable.edge_count() as u64
}
}
impl Default for Graph {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn upsert_is_idempotent() {
let mut g = Graph::new();
let a = g.upsert_node("alice");
assert_eq!(a, g.upsert_node("alice"));
assert_ne!(a, g.upsert_node("bob"));
}
#[test]
fn out_and_in_traversal() {
let mut g = Graph::new();
let follows = g.label("follows");
let a = g.upsert_node("alice");
let b = g.upsert_node("bob");
let c = g.upsert_node("carol");
g.add_edge(a, follows, b).unwrap();
g.add_edge(a, follows, c).unwrap();
g.add_edge(b, follows, c).unwrap();
assert_eq!(g.out(a, follows).collect::<Vec<_>>(), vec![b, c]);
assert_eq!(g.r#in(c, follows).collect::<Vec<_>>(), vec![a, b]);
}
#[test]
fn in_memory_graph_cannot_flush() {
let mut g = Graph::new();
assert!(matches!(g.flush(), Err(Error::NoStore)));
}
#[test]
fn flush_and_reopen_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let (a, b, follows_idx) = {
let mut g = Graph::open(dir.path()).unwrap();
let follows = g.label("follows");
let a = g.upsert_node("alice");
let b = g.upsert_node("bob");
g.add_edge(a, follows, b).unwrap();
assert_eq!(g.flush().unwrap(), 1);
(a, b, follows)
};
let mut g = Graph::open(dir.path()).unwrap();
assert_eq!(g.generation(), 1);
assert_eq!(g.node_count(), 2);
assert_eq!(g.edge_count(), 1);
assert_eq!(g.node("alice"), Some(a));
assert_eq!(g.upsert_node("bob"), b);
assert_eq!(g.label("follows"), follows_idx);
assert_eq!(g.out(a, follows_idx).collect::<Vec<_>>(), vec![b]);
assert_eq!(g.r#in(b, follows_idx).collect::<Vec<_>>(), vec![a]);
}
#[test]
fn reads_merge_across_segments_and_memtable() {
let dir = tempfile::tempdir().unwrap();
let mut g = Graph::open(dir.path()).unwrap();
let follows = g.label("follows");
let a = g.upsert_node("alice");
let b = g.upsert_node("bob");
g.add_edge(a, follows, b).unwrap();
g.flush().unwrap();
let c = g.upsert_node("carol");
g.add_edge(a, follows, c).unwrap();
g.flush().unwrap();
let d = g.upsert_node("dave");
g.add_edge(a, follows, d).unwrap();
assert_eq!(g.out(a, follows).collect::<Vec<_>>(), vec![b, c, d]);
assert_eq!(g.generation(), 2);
assert_eq!(g.edge_count(), 3);
}
#[test]
fn flush_is_noop_when_clean() {
let dir = tempfile::tempdir().unwrap();
let mut g = Graph::open(dir.path()).unwrap();
let follows = g.label("follows");
let a = g.upsert_node("alice");
let b = g.upsert_node("bob");
g.add_edge(a, follows, b).unwrap();
assert_eq!(g.flush().unwrap(), 1);
assert_eq!(g.flush().unwrap(), 1);
g.add_edge(a, follows, b).unwrap();
assert_eq!(g.edge_count(), 1);
assert_eq!(g.flush().unwrap(), 1);
}
}