Skip to main content

leyline_engine/
graph.rs

1use std::collections::{BTreeSet, HashMap};
2use std::path::Path;
3
4use crate::ids::{EdgeLabel, NodeId};
5use crate::manifest::{Manifest, SegmentMeta};
6use crate::memtable::Memtable;
7use crate::segment::Segment;
8use crate::store::LocalStore;
9use crate::{Error, Result};
10
11/// Top-level graph handle: memtable over immutable segments, LSM-style.
12///
13/// Reads merge the memtable with every open segment; `flush` drains the
14/// memtable into a new segment and commits a new manifest generation.
15/// `Graph::new` is in-memory only (no flush); `Graph::open` is the embedded
16/// persistent mode.
17pub struct Graph {
18    memtable: Memtable,
19    segments: Vec<Segment>,
20    store: Option<LocalStore>,
21    /// All keys ever, loaded from segments on open. Fits memory until it
22    /// doesn't — see docs/SEGMENTS.md, planned evolution.
23    key_index: HashMap<String, NodeId>,
24    /// Keys created since the last flush; what the next segment's key
25    /// section will contain.
26    pending_keys: Vec<(String, NodeId)>,
27    labels: Vec<String>,
28    next_id: u64,
29    generation: u64,
30    committed_edges: u64,
31}
32
33impl Graph {
34    /// In-memory only; `flush` returns `Error::NoStore`.
35    pub fn new() -> Self {
36        Self {
37            memtable: Memtable::new(),
38            segments: Vec::new(),
39            store: None,
40            key_index: HashMap::new(),
41            pending_keys: Vec::new(),
42            labels: Vec::new(),
43            next_id: 0,
44            generation: 0,
45            committed_edges: 0,
46        }
47    }
48
49    /// Open (or create) a graph rooted at `dir`.
50    pub fn open(dir: impl AsRef<Path>) -> Result<Self> {
51        let store = LocalStore::open(dir)?;
52        let manifest = store.load_manifest()?.unwrap_or_else(Manifest::empty);
53
54        let mut segments = Vec::with_capacity(manifest.segments.len());
55        let mut key_index = HashMap::new();
56        for meta in &manifest.segments {
57            let segment = Segment::decode(&store.read_segment(&meta.name)?)?;
58            for (key, node) in segment.keys() {
59                key_index.insert(key.to_owned(), node);
60            }
61            segments.push(segment);
62        }
63
64        Ok(Self {
65            memtable: Memtable::new(),
66            segments,
67            store: Some(store),
68            key_index,
69            pending_keys: Vec::new(),
70            labels: manifest.labels,
71            next_id: manifest.next_node_id,
72            generation: manifest.generation,
73            committed_edges: manifest.edge_count,
74        })
75    }
76
77    /// Create a node with a user-facing key, or return the existing id.
78    pub fn upsert_node(&mut self, key: &str) -> NodeId {
79        if let Some(&id) = self.key_index.get(key) {
80            return id;
81        }
82        let id = NodeId(self.next_id);
83        self.next_id += 1;
84        self.key_index.insert(key.to_owned(), id);
85        self.pending_keys.push((key.to_owned(), id));
86        self.memtable.insert_node(id);
87        id
88    }
89
90    pub fn node(&self, key: &str) -> Option<NodeId> {
91        self.key_index.get(key).copied()
92    }
93
94    /// Intern a label string, returning its id. Label indices are identity
95    /// forever — the manifest's label list is append-only.
96    pub fn label(&mut self, name: &str) -> EdgeLabel {
97        if let Some(pos) = self.labels.iter().position(|l| l == name) {
98            return EdgeLabel(pos as u32);
99        }
100        self.labels.push(name.to_owned());
101        EdgeLabel((self.labels.len() - 1) as u32)
102    }
103
104    pub fn add_edge(&mut self, src: NodeId, label: EdgeLabel, dst: NodeId) -> Result<()> {
105        // Edges are a set. The memtable dedups itself; this check keeps a
106        // re-added committed edge from inflating counters or segments.
107        if self
108            .segments
109            .iter()
110            .any(|s| s.out(src, label).any(|n| n == dst))
111        {
112            return Ok(());
113        }
114        self.memtable.insert_edge(src, label, dst)
115    }
116
117    /// Outbound neighbors of `src` under `label`, merged across memtable
118    /// and segments, ascending and deduplicated.
119    pub fn out(&self, src: NodeId, label: EdgeLabel) -> impl Iterator<Item = NodeId> {
120        let mut merged: BTreeSet<NodeId> = self.memtable.out(src, label).collect();
121        for segment in &self.segments {
122            merged.extend(segment.out(src, label));
123        }
124        merged.into_iter()
125    }
126
127    /// Inbound neighbors of `dst` under `label`, merged the same way.
128    pub fn r#in(&self, dst: NodeId, label: EdgeLabel) -> impl Iterator<Item = NodeId> {
129        let mut merged: BTreeSet<NodeId> = self.memtable.r#in(dst, label).collect();
130        for segment in &self.segments {
131            merged.extend(segment.r#in(dst, label));
132        }
133        merged.into_iter()
134    }
135
136    /// Drain the memtable into a new segment and commit the next manifest
137    /// generation. No-op (returns the current generation) when there is
138    /// nothing to write.
139    pub fn flush(&mut self) -> Result<u64> {
140        let store = self.store.as_ref().ok_or(Error::NoStore)?;
141        if self.memtable.is_empty() && self.pending_keys.is_empty() {
142            return Ok(self.generation);
143        }
144
145        // Segment key sections are sorted by key bytes (docs/SEGMENTS.md).
146        self.pending_keys.sort_by(|(a, _), (b, _)| a.cmp(b));
147        let bytes = Segment::encode(
148            self.memtable.iter_fwd(),
149            self.memtable.iter_rev(),
150            &self.pending_keys,
151        );
152
153        let generation = self.generation + 1;
154        let name = format!("seg-{generation:08}.seg");
155        // Segment before manifest: a crash here leaves an orphan file for
156        // the sweep, never a manifest naming a segment that isn't there.
157        store.write_segment(&name, &bytes)?;
158
159        let edges = self.memtable.edge_count() as u64;
160        let mut segments_meta: Vec<SegmentMeta> = self
161            .manifest_segments()
162            .unwrap_or_default();
163        segments_meta.push(SegmentMeta {
164            name: name.clone(),
165            level: 0,
166            edges,
167            bytes: bytes.len() as u64,
168        });
169
170        let manifest = Manifest {
171            format_version: crate::manifest::FORMAT_VERSION,
172            generation,
173            labels: self.labels.clone(),
174            next_node_id: self.next_id,
175            node_count: self.key_index.len() as u64,
176            edge_count: self.committed_edges + edges,
177            segments: segments_meta,
178        };
179        store.commit_manifest(&manifest)?;
180
181        self.segments.push(Segment::decode(&bytes)?);
182        self.memtable.clear();
183        self.pending_keys.clear();
184        self.generation = generation;
185        self.committed_edges += edges;
186        Ok(generation)
187    }
188
189    /// Re-read the committed segment list; the manifest is the truth for
190    /// what was durable before this process.
191    fn manifest_segments(&self) -> Option<Vec<SegmentMeta>> {
192        let store = self.store.as_ref()?;
193        store.load_manifest().ok().flatten().map(|m| m.segments)
194    }
195
196    pub fn generation(&self) -> u64 {
197        self.generation
198    }
199
200    pub fn node_count(&self) -> u64 {
201        self.key_index.len() as u64
202    }
203
204    pub fn edge_count(&self) -> u64 {
205        self.committed_edges + self.memtable.edge_count() as u64
206    }
207}
208
209impl Default for Graph {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn upsert_is_idempotent() {
221        let mut g = Graph::new();
222        let a = g.upsert_node("alice");
223        assert_eq!(a, g.upsert_node("alice"));
224        assert_ne!(a, g.upsert_node("bob"));
225    }
226
227    #[test]
228    fn out_and_in_traversal() {
229        let mut g = Graph::new();
230        let follows = g.label("follows");
231        let a = g.upsert_node("alice");
232        let b = g.upsert_node("bob");
233        let c = g.upsert_node("carol");
234        g.add_edge(a, follows, b).unwrap();
235        g.add_edge(a, follows, c).unwrap();
236        g.add_edge(b, follows, c).unwrap();
237
238        assert_eq!(g.out(a, follows).collect::<Vec<_>>(), vec![b, c]);
239        assert_eq!(g.r#in(c, follows).collect::<Vec<_>>(), vec![a, b]);
240    }
241
242    #[test]
243    fn in_memory_graph_cannot_flush() {
244        let mut g = Graph::new();
245        assert!(matches!(g.flush(), Err(Error::NoStore)));
246    }
247
248    #[test]
249    fn flush_and_reopen_roundtrip() {
250        let dir = tempfile::tempdir().unwrap();
251
252        let (a, b, follows_idx) = {
253            let mut g = Graph::open(dir.path()).unwrap();
254            let follows = g.label("follows");
255            let a = g.upsert_node("alice");
256            let b = g.upsert_node("bob");
257            g.add_edge(a, follows, b).unwrap();
258            assert_eq!(g.flush().unwrap(), 1);
259            (a, b, follows)
260        };
261
262        let mut g = Graph::open(dir.path()).unwrap();
263        assert_eq!(g.generation(), 1);
264        assert_eq!(g.node_count(), 2);
265        assert_eq!(g.edge_count(), 1);
266        // Same keys resolve to the same ids after reopen.
267        assert_eq!(g.node("alice"), Some(a));
268        assert_eq!(g.upsert_node("bob"), b);
269        // Label interning must agree with what segments were written with.
270        assert_eq!(g.label("follows"), follows_idx);
271        assert_eq!(g.out(a, follows_idx).collect::<Vec<_>>(), vec![b]);
272        assert_eq!(g.r#in(b, follows_idx).collect::<Vec<_>>(), vec![a]);
273    }
274
275    #[test]
276    fn reads_merge_across_segments_and_memtable() {
277        let dir = tempfile::tempdir().unwrap();
278        let mut g = Graph::open(dir.path()).unwrap();
279        let follows = g.label("follows");
280        let a = g.upsert_node("alice");
281        let b = g.upsert_node("bob");
282        g.add_edge(a, follows, b).unwrap();
283        g.flush().unwrap();
284
285        let c = g.upsert_node("carol");
286        g.add_edge(a, follows, c).unwrap();
287        g.flush().unwrap();
288
289        let d = g.upsert_node("dave");
290        g.add_edge(a, follows, d).unwrap(); // memtable only
291
292        // One neighborhood spread over two segments plus the memtable.
293        assert_eq!(g.out(a, follows).collect::<Vec<_>>(), vec![b, c, d]);
294        assert_eq!(g.generation(), 2);
295        assert_eq!(g.edge_count(), 3);
296    }
297
298    #[test]
299    fn flush_is_noop_when_clean() {
300        let dir = tempfile::tempdir().unwrap();
301        let mut g = Graph::open(dir.path()).unwrap();
302        let follows = g.label("follows");
303        let a = g.upsert_node("alice");
304        let b = g.upsert_node("bob");
305        g.add_edge(a, follows, b).unwrap();
306        assert_eq!(g.flush().unwrap(), 1);
307        assert_eq!(g.flush().unwrap(), 1); // nothing new to write
308
309        // Re-adding a committed edge is absorbed, not re-counted.
310        g.add_edge(a, follows, b).unwrap();
311        assert_eq!(g.edge_count(), 1);
312        assert_eq!(g.flush().unwrap(), 1);
313    }
314}