1use std::marker::PhantomData;
27use std::path::{Path, PathBuf};
28
29use crate::shared_region::{OffsetPtr, RegionError, SharedRegion};
30
31pub const NIL_INDEX: u32 = u32::MAX;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34#[repr(C)]
35pub struct NodeIndex<N> {
36 pub index: u32,
37 _phantom: PhantomData<N>,
38}
39
40impl<N> NodeIndex<N> {
41 pub const NIL: Self = Self { index: NIL_INDEX, _phantom: PhantomData };
42 #[inline]
43 pub fn new(index: u32) -> Self { Self { index, _phantom: PhantomData } }
44 #[inline]
45 pub fn is_nil(self) -> bool { self.index == NIL_INDEX }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49#[repr(C)]
50pub struct EdgeIndex<E> {
51 pub index: u32,
52 _phantom: PhantomData<E>,
53}
54
55impl<E> EdgeIndex<E> {
56 pub const NIL: Self = Self { index: NIL_INDEX, _phantom: PhantomData };
57 #[inline]
58 pub fn new(index: u32) -> Self { Self { index, _phantom: PhantomData } }
59 #[inline]
60 pub fn is_nil(self) -> bool { self.index == NIL_INDEX }
61}
62
63#[repr(C)]
64#[derive(Clone, Copy)]
65pub struct GraphNode<N: Copy + Default + 'static> {
66 pub value: N,
67 pub first_out_edge: u32,
68 pub n_out_edges: u32,
69}
70
71#[repr(C)]
72#[derive(Clone, Copy)]
73pub struct GraphEdge<E: Copy + Default + 'static> {
74 pub value: E,
75 pub dst: u32,
76 pub next_in_src_list: u32,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum GraphError {
81 Region(RegionError),
82 InvalidNode,
83 InvalidEdge,
84 IoError(std::io::ErrorKind),
85}
86
87impl From<RegionError> for GraphError {
88 fn from(e: RegionError) -> Self { Self::Region(e) }
89}
90impl From<std::io::Error> for GraphError {
91 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
92}
93
94fn nodes_path(base: &Path) -> PathBuf {
95 let mut p = base.to_path_buf();
96 let stem = p.file_name().unwrap().to_string_lossy().to_string();
97 p.set_file_name(format!("{stem}.nodes.bin"));
98 p
99}
100fn edges_path(base: &Path) -> PathBuf {
101 let mut p = base.to_path_buf();
102 let stem = p.file_name().unwrap().to_string_lossy().to_string();
103 p.set_file_name(format!("{stem}.edges.bin"));
104 p
105}
106
107pub struct SharedGraph<
108 N: Copy + Default + 'static,
109 E: Copy + Default + 'static,
110> {
111 nodes: SharedRegion<GraphNode<N>>,
112 edges: SharedRegion<GraphEdge<E>>,
113 _phantom: PhantomData<(N, E)>,
114 header_sidecar: subetha_core::HandshakeHeader,
115 ring_sidecar: Box<subetha_core::ObservationRing>,
116}
117
118impl<
119 N: Copy + Default + Send + Sync + 'static,
120 E: Copy + Default + Send + Sync + 'static,
121> subetha_sidecar::AdaptiveInstance for SharedGraph<N, E> {
122 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
123 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
124 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
125 Box::new(subetha_sidecar::NoMigrationPolicy)
126 }
127}
128
129impl<N: Copy + Default + 'static, E: Copy + Default + 'static>
130 SharedGraph<N, E>
131{
132 pub fn create(
133 base_path: impl AsRef<Path>,
134 max_nodes: usize,
135 max_edges: usize,
136 ) -> Result<Self, GraphError> {
137 let base = base_path.as_ref();
138 let nodes = SharedRegion::create(nodes_path(base), max_nodes)?;
139 let edges = SharedRegion::create(edges_path(base), max_edges)?;
140 Ok(Self {
141 nodes, edges, _phantom: PhantomData,
142 header_sidecar: subetha_core::HandshakeHeader::new(),
143 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
144 })
145 }
146
147 pub fn open(
148 base_path: impl AsRef<Path>,
149 max_nodes: usize,
150 max_edges: usize,
151 ) -> Result<Self, GraphError> {
152 let base = base_path.as_ref();
153 let nodes = SharedRegion::open(nodes_path(base), max_nodes)?;
154 let edges = SharedRegion::open(edges_path(base), max_edges)?;
155 Ok(Self {
156 nodes, edges, _phantom: PhantomData,
157 header_sidecar: subetha_core::HandshakeHeader::new(),
158 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
159 })
160 }
161
162 pub fn node_count(&self) -> usize { self.nodes.len() }
163 pub fn edge_count(&self) -> usize { self.edges.len() }
164 pub fn max_nodes(&self) -> usize { self.nodes.capacity() }
165 pub fn max_edges(&self) -> usize { self.edges.capacity() }
166
167 pub fn add_node(&self, value: N) -> Result<NodeIndex<N>, GraphError> {
169 let r = self.nodes.allocate(GraphNode {
170 value,
171 first_out_edge: NIL_INDEX,
172 n_out_edges: 0,
173 });
174 self.ring_sidecar.push_op(
175 crate::sidecar_ops::graph::OP_ADD_NODE,
176 if r.is_err() { 1 } else { 0 },
177 );
178 Ok(NodeIndex::new(r?.index))
179 }
180
181 pub fn add_edge(
185 &self, src: NodeIndex<N>, dst: NodeIndex<N>, value: E,
186 ) -> Result<EdgeIndex<E>, GraphError> {
187 let r = self.add_edge_inner(src, dst, value);
188 self.ring_sidecar.push_op(
189 crate::sidecar_ops::graph::OP_ADD_EDGE,
190 if r.is_err() { 1 } else { 0 },
191 );
192 r
193 }
194
195 fn add_edge_inner(
196 &self, src: NodeIndex<N>, dst: NodeIndex<N>, value: E,
197 ) -> Result<EdgeIndex<E>, GraphError> {
198 if src.is_nil() || dst.is_nil()
199 || src.index as usize >= self.nodes.capacity()
200 || dst.index as usize >= self.nodes.capacity()
201 {
202 return Err(GraphError::InvalidNode);
203 }
204 let mut src_node = self.nodes.get(OffsetPtr::new(src.index))?;
205 let edge_ptr = self.edges.allocate(GraphEdge {
206 value,
207 dst: dst.index,
208 next_in_src_list: src_node.first_out_edge,
209 })?;
210 src_node.first_out_edge = edge_ptr.index;
211 src_node.n_out_edges = src_node.n_out_edges.wrapping_add(1);
212 self.nodes.set(OffsetPtr::new(src.index), src_node)?;
213 Ok(EdgeIndex::new(edge_ptr.index))
214 }
215
216 pub fn node_value(&self, idx: NodeIndex<N>) -> Option<N> {
218 if idx.is_nil() { return None; }
219 self.nodes.get(OffsetPtr::new(idx.index)).ok().map(|n| n.value)
220 }
221
222 pub fn edge_endpoints(&self, idx: EdgeIndex<E>) -> Option<(E, NodeIndex<N>)> {
224 if idx.is_nil() { return None; }
225 let e = self.edges.get(OffsetPtr::new(idx.index)).ok()?;
226 Some((e.value, NodeIndex::new(e.dst)))
227 }
228
229 pub fn out_degree(&self, src: NodeIndex<N>) -> Option<u32> {
231 if src.is_nil() { return None; }
232 self.nodes.get(OffsetPtr::new(src.index)).ok().map(|n| n.n_out_edges)
233 }
234
235 pub fn neighbors(&self, src: NodeIndex<N>) -> Vec<(EdgeIndex<E>, NodeIndex<N>, E)> {
238 if src.is_nil() {
239 self.ring_sidecar
240 .push_op(crate::sidecar_ops::graph::OP_NEIGHBORS, 2);
241 return Vec::new();
242 }
243 let src_node = match self.nodes.get(OffsetPtr::new(src.index)) {
244 Ok(n) => n,
245 Err(_) => {
246 self.ring_sidecar
247 .push_op(crate::sidecar_ops::graph::OP_NEIGHBORS, 2);
248 return Vec::new();
249 }
250 };
251 let mut out = Vec::with_capacity(src_node.n_out_edges as usize);
252 let mut cur = src_node.first_out_edge;
253 let mut visited = 0u32;
254 let max_iter = self.edges.capacity() as u32;
255 while cur != NIL_INDEX && visited < max_iter {
256 let e = match self.edges.get(OffsetPtr::new(cur)) {
257 Ok(e) => e, Err(_) => break,
258 };
259 out.push((EdgeIndex::new(cur), NodeIndex::new(e.dst), e.value));
260 cur = e.next_in_src_list;
261 visited += 1;
262 }
263 self.ring_sidecar
264 .push_op(crate::sidecar_ops::graph::OP_NEIGHBORS, 0);
265 out
266 }
267
268 pub fn remove_edge(
271 &self, src: NodeIndex<N>, edge_idx: EdgeIndex<E>,
272 ) -> Option<E> {
273 let r = self.remove_edge_inner(src, edge_idx);
274 self.ring_sidecar.push_op(
275 crate::sidecar_ops::graph::OP_REMOVE_EDGE,
276 if r.is_none() { 2 } else { 0 },
277 );
278 r
279 }
280
281 fn remove_edge_inner(
282 &self, src: NodeIndex<N>, edge_idx: EdgeIndex<E>,
283 ) -> Option<E> {
284 if src.is_nil() || edge_idx.is_nil() { return None; }
285 let mut src_node = self.nodes.get(OffsetPtr::new(src.index)).ok()?;
286 let target_value;
287 let target_next;
288 if src_node.first_out_edge == edge_idx.index {
290 let e = self.edges.get(OffsetPtr::new(edge_idx.index)).ok()?;
291 target_value = e.value;
292 target_next = e.next_in_src_list;
293 src_node.first_out_edge = target_next;
294 } else {
295 let mut prev_idx = src_node.first_out_edge;
296 loop {
297 if prev_idx == NIL_INDEX { return None; }
298 let mut prev_edge = self.edges.get(OffsetPtr::new(prev_idx)).ok()?;
299 if prev_edge.next_in_src_list == edge_idx.index {
300 let removed = self.edges.get(OffsetPtr::new(edge_idx.index)).ok()?;
301 target_value = removed.value;
302 target_next = removed.next_in_src_list;
303 prev_edge.next_in_src_list = target_next;
304 self.edges.set(OffsetPtr::new(prev_idx), prev_edge).ok()?;
305 break;
306 }
307 prev_idx = prev_edge.next_in_src_list;
308 }
309 }
310 src_node.n_out_edges = src_node.n_out_edges.saturating_sub(1);
311 self.nodes.set(OffsetPtr::new(src.index), src_node).ok()?;
312 self.edges.free(OffsetPtr::new(edge_idx.index)).ok();
314 Some(target_value)
315 }
316
317 pub fn flush(&self) -> Result<(), GraphError> {
318 self.nodes.flush()?;
319 self.edges.flush()?;
320 Ok(())
321 }
322 pub fn flush_async(&self) -> Result<(), GraphError> {
323 self.nodes.flush_async()?;
324 self.edges.flush_async()?;
325 Ok(())
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 fn tmp_base(name: &str) -> PathBuf {
334 let mut p = std::env::temp_dir();
335 let pid = std::process::id();
336 p.push(format!("subetha-graph-{name}-{pid}"));
337 p
338 }
339
340 fn cleanup(base: &Path) {
341 std::fs::remove_file(nodes_path(base)).ok();
342 std::fs::remove_file(edges_path(base)).ok();
343 }
344
345 #[test]
346 fn create_initial_state_is_empty() {
347 let base = tmp_base("init");
348 let g: SharedGraph<u32, u32> = SharedGraph::create(&base, 16, 32).unwrap();
349 assert_eq!(g.node_count(), 0);
350 assert_eq!(g.edge_count(), 0);
351 cleanup(&base);
352 }
353
354 #[test]
355 fn add_node_returns_distinct_indices() {
356 let base = tmp_base("add-node");
357 let g: SharedGraph<u32, u32> = SharedGraph::create(&base, 16, 32).unwrap();
358 let a = g.add_node(10).unwrap();
359 let b = g.add_node(20).unwrap();
360 let c = g.add_node(30).unwrap();
361 assert_ne!(a, b);
362 assert_ne!(b, c);
363 assert_eq!(g.node_value(a), Some(10));
364 assert_eq!(g.node_value(b), Some(20));
365 assert_eq!(g.node_value(c), Some(30));
366 assert_eq!(g.node_count(), 3);
367 cleanup(&base);
368 }
369
370 #[test]
371 fn add_edge_links_correctly() {
372 let base = tmp_base("add-edge");
373 let g: SharedGraph<u32, u32> = SharedGraph::create(&base, 16, 32).unwrap();
374 let a = g.add_node(1).unwrap();
375 let b = g.add_node(2).unwrap();
376 let c = g.add_node(3).unwrap();
377 let _e1 = g.add_edge(a, b, 100).unwrap();
378 let _e2 = g.add_edge(a, c, 200).unwrap();
379 assert_eq!(g.out_degree(a), Some(2));
380 assert_eq!(g.out_degree(b), Some(0));
381 let nbrs = g.neighbors(a);
382 let mut dsts: Vec<u32> = nbrs.iter().map(|(_, d, _)| d.index).collect();
383 let mut vals: Vec<u32> = nbrs.iter().map(|(_, _, v)| *v).collect();
384 dsts.sort();
385 vals.sort();
386 assert_eq!(dsts, vec![b.index, c.index]);
387 assert_eq!(vals, vec![100, 200]);
388 cleanup(&base);
389 }
390
391 #[test]
392 fn multiple_edges_from_one_source() {
393 let base = tmp_base("multi-edge");
394 let g: SharedGraph<u32, u32> = SharedGraph::create(&base, 16, 32).unwrap();
395 let src = g.add_node(0).unwrap();
396 let dsts: Vec<NodeIndex<u32>> = (1..=5).map(|i| g.add_node(i).unwrap()).collect();
397 for (i, &d) in dsts.iter().enumerate() {
398 g.add_edge(src, d, (i as u32) * 10).unwrap();
399 }
400 assert_eq!(g.out_degree(src), Some(5));
401 let nbrs = g.neighbors(src);
402 assert_eq!(nbrs.len(), 5);
403 cleanup(&base);
404 }
405
406 #[test]
407 fn remove_edge_unlinks_head() {
408 let base = tmp_base("remove-head");
409 let g: SharedGraph<u32, u32> = SharedGraph::create(&base, 16, 32).unwrap();
410 let a = g.add_node(0).unwrap();
411 let b = g.add_node(1).unwrap();
412 let c = g.add_node(2).unwrap();
413 let e1 = g.add_edge(a, b, 100).unwrap();
414 let e2 = g.add_edge(a, c, 200).unwrap();
415 let removed = g.remove_edge(a, e2);
417 assert_eq!(removed, Some(200));
418 assert_eq!(g.out_degree(a), Some(1));
419 let nbrs = g.neighbors(a);
420 assert_eq!(nbrs.len(), 1);
421 assert_eq!(nbrs[0].0, e1);
422 cleanup(&base);
423 }
424
425 #[test]
426 fn remove_edge_unlinks_middle() {
427 let base = tmp_base("remove-middle");
428 let g: SharedGraph<u32, u32> = SharedGraph::create(&base, 16, 32).unwrap();
429 let src = g.add_node(0).unwrap();
430 let dsts: Vec<NodeIndex<u32>> = (1..=4).map(|i| g.add_node(i).unwrap()).collect();
431 let edges: Vec<EdgeIndex<u32>> = dsts.iter().enumerate()
432 .map(|(i, &d)| g.add_edge(src, d, (i as u32) * 10).unwrap())
433 .collect();
434 let removed = g.remove_edge(src, edges[1]);
436 assert_eq!(removed, Some(10));
437 assert_eq!(g.out_degree(src), Some(3));
438 let nbrs = g.neighbors(src);
439 assert_eq!(nbrs.len(), 3);
440 let edge_idxs: Vec<EdgeIndex<u32>> = nbrs.iter().map(|(e, _, _)| *e).collect();
442 assert!(!edge_idxs.contains(&edges[1]));
443 cleanup(&base);
444 }
445
446 #[test]
447 fn invalid_node_index_rejected_on_add_edge() {
448 let base = tmp_base("invalid-node");
449 let g: SharedGraph<u32, u32> = SharedGraph::create(&base, 4, 8).unwrap();
450 let a = g.add_node(0).unwrap();
451 let bogus = NodeIndex::<u32>::new(999);
452 assert_eq!(
453 g.add_edge(a, bogus, 1).err(),
454 Some(GraphError::InvalidNode)
455 );
456 assert_eq!(g.add_edge(NodeIndex::NIL, a, 1).err(), Some(GraphError::InvalidNode));
457 cleanup(&base);
458 }
459
460 #[test]
461 fn cross_handle_visibility() {
462 let base = tmp_base("cross-handle");
463 let w: SharedGraph<u32, u32> = SharedGraph::create(&base, 8, 16).unwrap();
464 let r: SharedGraph<u32, u32> = SharedGraph::open(&base, 8, 16).unwrap();
465 let a = w.add_node(1).unwrap();
466 let b = w.add_node(2).unwrap();
467 let e = w.add_edge(a, b, 42).unwrap();
468 assert_eq!(r.node_value(a), Some(1));
470 assert_eq!(r.node_value(b), Some(2));
471 let nbrs = r.neighbors(a);
472 assert_eq!(nbrs.len(), 1);
473 assert_eq!(nbrs[0].0, e);
474 cleanup(&base);
475 }
476
477 #[test]
478 fn struct_payload_round_trip() {
479 #[derive(Clone, Copy, Debug, PartialEq, Default)]
480 #[repr(C)]
481 struct Page { url_hash: u64, depth: u32 }
482 #[derive(Clone, Copy, Debug, PartialEq, Default)]
483 #[repr(C)]
484 struct Link { weight: f32, rel: u32 }
485 let base = tmp_base("struct");
486 let g: SharedGraph<Page, Link> = SharedGraph::create(&base, 8, 16).unwrap();
487 let p1 = Page { url_hash: 0xAAAA, depth: 0 };
488 let p2 = Page { url_hash: 0xBBBB, depth: 1 };
489 let a = g.add_node(p1).unwrap();
490 let b = g.add_node(p2).unwrap();
491 let _e = g.add_edge(a, b, Link { weight: 0.75, rel: 1 }).unwrap();
492 assert_eq!(g.node_value(a), Some(p1));
493 let nbrs = g.neighbors(a);
494 assert_eq!(nbrs.len(), 1);
495 assert_eq!(nbrs[0].1, b);
496 assert_eq!(nbrs[0].2.weight, 0.75);
497 cleanup(&base);
498 }
499
500 #[test]
501 fn capacity_exhaustion_returns_error() {
502 let base = tmp_base("exhaust");
503 let g: SharedGraph<u32, u32> = SharedGraph::create(&base, 3, 2).unwrap();
504 let _a = g.add_node(0).unwrap();
505 let _b = g.add_node(1).unwrap();
506 let _c = g.add_node(2).unwrap();
507 assert!(g.add_node(3).is_err());
509 cleanup(&base);
510 }
511
512 #[test]
513 fn disk_persistence_survives_reopen() {
514 let base = tmp_base("disk");
515 let saved_a;
516 let saved_b;
517 {
518 let g: SharedGraph<u32, u32> = SharedGraph::create(&base, 8, 16).unwrap();
519 saved_a = g.add_node(100).unwrap();
520 saved_b = g.add_node(200).unwrap();
521 g.add_edge(saved_a, saved_b, 42).unwrap();
522 g.flush().unwrap();
523 }
524 let g2: SharedGraph<u32, u32> = SharedGraph::open(&base, 8, 16).unwrap();
525 assert_eq!(g2.node_count(), 2);
526 assert_eq!(g2.node_value(saved_a), Some(100));
527 let nbrs = g2.neighbors(saved_a);
528 assert_eq!(nbrs.len(), 1);
529 assert_eq!(nbrs[0].2, 42);
530 cleanup(&base);
531 }
532}