use yo_common::{Code, Error, Result};
use yo_doc::{Doc, IndexKind, Key};
use crate::{Adjacency, Dir, Props};
const EMPTY_OBJECT: [u8; 4] = EMPTY_OBJECT_HEAD.to_le_bytes();
const EMPTY_OBJECT_HEAD: u32 = 7 | (1 << 4) | (1 << 5);
pub const NO_PROPS: &[u8] = &EMPTY_OBJECT;
#[derive(Debug, Default)]
pub struct Graph {
adj: Adjacency,
nodes: Props,
edges: Props,
next: u32,
free: Vec<u32>,
labels: Vec<u32>,
}
impl Graph {
#[must_use]
pub fn new() -> Graph {
Graph {
adj: Adjacency::new(),
..Graph::default()
}
}
#[must_use]
pub fn out_only() -> Graph {
Graph {
adj: Adjacency::out_only(),
..Graph::default()
}
}
pub fn put_node(&mut self, id: u64, props: &[u8]) -> Result<bool> {
self.nodes.put(id, props)
}
pub fn add_node(&mut self, id: u64) -> Result<bool> {
if self.nodes.contains(id) {
return Ok(false);
}
self.nodes.put(id, &EMPTY_OBJECT)
}
#[must_use]
pub fn node(&self, id: u64) -> Option<Doc<'_>> {
self.nodes.get(id)
}
#[must_use]
pub fn has_node(&self, id: u64) -> bool {
self.nodes.contains(id)
}
#[must_use]
pub fn nodes(&self) -> usize {
self.nodes.len()
}
#[must_use]
pub fn edges(&self) -> usize {
self.adj.edges()
}
#[must_use]
pub fn labels(&self) -> &[u32] {
&self.labels
}
pub fn link(&mut self, src: u64, dst: u64, label: u32, props: &[u8]) -> Result<u32> {
self.add_node(src)?;
self.add_node(dst)?;
let slot = self.take_slot()?;
if let Err(e) = self.edges.put(u64::from(slot), props) {
self.free.push(slot);
return Err(e);
}
self.adj.link(src, dst, label, slot);
if let Err(at) = self.labels.binary_search(&label) {
self.labels.insert(at, label);
}
Ok(slot)
}
pub fn unlink(&mut self, src: u64, dst: u64, label: u32) -> Option<u32> {
let slot = self.adj.unlink(src, dst, label)?;
self.release(slot);
Some(slot)
}
#[must_use]
pub fn edge(&self, slot: u32) -> Option<Doc<'_>> {
self.edges.get(u64::from(slot))
}
pub fn put_edge(&mut self, slot: u32, props: &[u8]) -> Result<()> {
if !self.edges.contains(u64::from(slot)) {
return Err(Error::new(Code::NotFound, "no edge is under that slot")
.with_detail(format!("slot={slot}")));
}
self.edges.put(u64::from(slot), props)?;
Ok(())
}
pub fn remove_node(&mut self, id: u64) -> Result<bool> {
if !self.nodes.contains(id) {
return Ok(false);
}
if !self.adj.indexes_incoming() {
let any = self
.labels
.iter()
.any(|&l| !self.adj.neighbours(id, l, Dir::Out).is_empty());
if any {
return Err(Error::new(
Code::Unsupported,
"this graph does not index incoming edges, so a node with edges cannot be removed",
)
.with_detail(format!("node={id}")));
}
}
let labels = self.labels.clone();
for label in labels {
let out: Vec<u64> = self.adj.neighbours(id, label, Dir::Out).to_vec();
for dst in out {
if let Some(slot) = self.adj.unlink(id, dst, label) {
self.release(slot);
}
}
let into: Vec<u64> = self.adj.neighbours(id, label, Dir::In).to_vec();
for src in into {
if let Some(slot) = self.adj.unlink(src, id, label) {
self.release(slot);
}
}
}
Ok(self.nodes.remove(id))
}
#[must_use]
pub fn neighbours(&self, node: u64, label: u32, dir: Dir) -> &[u64] {
self.adj.neighbours(node, label, dir)
}
#[must_use]
pub fn edge_slots(&self, node: u64, label: u32, dir: Dir) -> &[u32] {
self.adj.edge_slots(node, label, dir)
}
pub fn hop(&self, node: u64, label: u32, dir: Dir) -> impl Iterator<Item = (u64, u32)> {
self.adj
.neighbours(node, label, dir)
.iter()
.copied()
.zip(self.adj.edge_slots(node, label, dir).iter().copied())
}
#[must_use]
pub fn degree(&self, node: u64, label: u32, dir: Dir) -> usize {
self.adj.degree(node, label, dir)
}
pub fn prefetch(&self, node: u64, label: u32, dir: Dir) {
self.adj.prefetch(node, label, dir);
}
pub fn index_nodes(&mut self, path: &str, kind: IndexKind) -> Result<()> {
self.nodes.create_index(path, kind)
}
pub fn index_edges(&mut self, path: &str, kind: IndexKind) -> Result<()> {
self.edges.create_index(path, kind)
}
pub fn find_nodes(&self, path: &str, key: &Key, f: impl FnMut(u64, Doc<'_>)) -> Result<usize> {
self.nodes.find(path, key, f)
}
pub fn count_nodes(&self, path: &str, key: &Key) -> Result<usize> {
self.nodes.count(path, key)
}
pub fn count_edges(&self, path: &str, key: &Key) -> Result<usize> {
self.edges.count(path, key)
}
pub fn find_edges(
&self,
path: &str,
key: &Key,
mut f: impl FnMut(u32, Doc<'_>),
) -> Result<usize> {
self.edges.find(path, key, |slot, doc| {
if let Ok(slot) = u32::try_from(slot) {
f(slot, doc);
}
})
}
#[must_use]
pub fn node_props(&self) -> &Props {
&self.nodes
}
#[must_use]
pub fn edge_props(&self) -> &Props {
&self.edges
}
#[must_use]
pub fn adjacency(&self) -> &Adjacency {
&self.adj
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.adj.bytes()
+ self.nodes.memory_bytes()
+ self.edges.memory_bytes()
+ self.free.capacity() * size_of::<u32>()
+ self.labels.capacity() * size_of::<u32>()
}
fn take_slot(&mut self) -> Result<u32> {
if let Some(slot) = self.free.pop() {
return Ok(slot);
}
if self.next == u32::MAX {
return Err(Error::new(Code::Full, "this graph has no edge slots left"));
}
let slot = self.next;
self.next += 1;
Ok(slot)
}
fn release(&mut self, slot: u32) {
self.edges.remove(u64::from(slot));
self.free.push(slot);
}
}
#[cfg(test)]
mod tests {
use super::*;
use yo_doc::{Builder, Value};
const FOLLOWS: u32 = 1;
const BLOCKS: u32 = 2;
fn doc(f: impl FnOnce(&mut Builder) -> Result<()>) -> Vec<u8> {
let mut b = Builder::new();
f(&mut b).expect("built");
b.finish().expect("finished").to_vec()
}
fn named(name: &str) -> Vec<u8> {
doc(|b| {
b.begin_object()?;
b.key(b"name")?;
b.text(name)?;
b.end_object()
})
}
fn since(year: i64) -> Vec<u8> {
doc(|b| {
b.begin_object()?;
b.key(b"since")?;
b.int(year)?;
b.end_object()
})
}
#[test]
fn the_empty_object_constant_is_an_empty_object() {
let built = doc(|b| {
b.begin_object()?;
b.end_object()
});
assert_eq!(&EMPTY_OBJECT[..], &built[..]);
let v = Value::new(&EMPTY_OBJECT).expect("readable");
assert!(v.validate());
assert!(v.is_empty());
}
#[test]
fn a_node_exists_once_it_has_properties() {
let mut g = Graph::new();
assert!(!g.has_node(1));
assert!(g.add_node(1).unwrap());
assert!(!g.add_node(1).unwrap(), "adding twice is not two nodes");
assert!(g.has_node(1));
assert_eq!(g.nodes(), 1);
assert_eq!(g.edges(), 0);
}
#[test]
fn linking_creates_the_endpoints() {
let mut g = Graph::new();
let e = g.link(1, 2, FOLLOWS, &since(2026)).unwrap();
assert!(g.has_node(1) && g.has_node(2));
assert_eq!(g.nodes(), 2);
assert_eq!(g.edges(), 1);
assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), [2]);
assert_eq!(g.neighbours(2, FOLLOWS, Dir::In), [1]);
assert_eq!(
g.edge(e)
.and_then(|d| d.get(b"since"))
.and_then(|v| v.as_int()),
Some(2026)
);
assert_eq!(g.labels(), [FOLLOWS]);
}
#[test]
fn a_hop_gives_the_neighbour_and_the_edge_together() {
let mut g = Graph::new();
g.link(1, 2, FOLLOWS, &since(2024)).unwrap();
g.link(1, 3, FOLLOWS, &since(2025)).unwrap();
let mut seen: Vec<(u64, i64)> = g
.hop(1, FOLLOWS, Dir::Out)
.map(|(n, slot)| {
let year = g
.edge(slot)
.and_then(|d| d.get(b"since"))
.and_then(|v| v.as_int())
.expect("an edge has its year");
(n, year)
})
.collect();
seen.sort_unstable();
assert_eq!(seen, vec![(2, 2024), (3, 2025)]);
}
#[test]
fn parallel_edges_each_keep_their_own_properties() {
let mut g = Graph::new();
let a = g.link(1, 2, FOLLOWS, &since(2024)).unwrap();
let b = g.link(1, 2, FOLLOWS, &since(2026)).unwrap();
assert_ne!(a, b);
assert_eq!(g.edges(), 2);
assert_eq!(g.degree(1, FOLLOWS, Dir::Out), 2);
assert_eq!(
g.edge(a)
.and_then(|d| d.get(b"since"))
.and_then(|v| v.as_int()),
Some(2024)
);
assert_eq!(
g.edge(b)
.and_then(|d| d.get(b"since"))
.and_then(|v| v.as_int()),
Some(2026)
);
}
#[test]
fn a_freed_slot_is_reused_without_its_old_properties() {
let mut g = Graph::new();
let a = g.link(1, 2, FOLLOWS, &since(2024)).unwrap();
assert_eq!(g.unlink(1, 2, FOLLOWS), Some(a));
assert!(g.edge(a).is_none(), "an unlinked edge keeps nothing");
let b = g.link(3, 4, FOLLOWS, &since(2026)).unwrap();
assert_eq!(a, b, "a freed slot is handed out again");
assert_eq!(
g.edge(b)
.and_then(|d| d.get(b"since"))
.and_then(|v| v.as_int()),
Some(2026)
);
}
#[test]
fn removing_a_node_takes_its_edges_at_both_ends() {
let mut g = Graph::new();
g.link(1, 2, FOLLOWS, &since(2024)).unwrap();
g.link(3, 2, FOLLOWS, &since(2025)).unwrap();
g.link(2, 4, BLOCKS, &since(2026)).unwrap();
g.link(1, 3, FOLLOWS, &since(2023)).unwrap();
assert_eq!(g.edges(), 4);
assert!(g.remove_node(2).unwrap());
assert!(!g.has_node(2));
assert_eq!(g.edges(), 1);
assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), [3]);
assert!(g.neighbours(3, FOLLOWS, Dir::Out).is_empty());
assert!(g.neighbours(4, BLOCKS, Dir::In).is_empty());
assert_eq!(g.edge_props().len(), 1);
let left = g.edge_slots(1, FOLLOWS, Dir::Out)[0];
assert_eq!(
g.edge(left)
.and_then(|d| d.get(b"since"))
.and_then(|v| v.as_int()),
Some(2023)
);
}
#[test]
fn removing_a_node_with_parallel_edges_takes_all_of_them() {
let mut g = Graph::new();
for year in 2020..2030 {
g.link(1, 2, FOLLOWS, &since(year)).unwrap();
}
assert_eq!(g.edges(), 10);
assert!(g.remove_node(2).unwrap());
assert_eq!(g.edges(), 0);
assert!(g.neighbours(1, FOLLOWS, Dir::Out).is_empty());
assert!(g.edge_props().is_empty());
}
#[test]
fn removing_a_node_that_is_not_there_says_so() {
let mut g = Graph::new();
g.add_node(1).unwrap();
assert!(!g.remove_node(2).unwrap());
assert!(g.remove_node(1).unwrap());
assert_eq!(g.nodes(), 0);
}
#[test]
fn an_out_only_graph_refuses_to_remove_a_linked_node() {
let mut g = Graph::out_only();
g.link(1, 2, FOLLOWS, &since(2026)).unwrap();
assert!(g.remove_node(1).is_err());
assert!(g.remove_node(2).unwrap());
}
#[test]
fn an_edge_slot_that_is_gone_refuses_a_write() {
let mut g = Graph::new();
let e = g.link(1, 2, FOLLOWS, &since(2024)).unwrap();
assert!(g.put_edge(e, &since(2025)).is_ok());
g.unlink(1, 2, FOLLOWS);
assert!(
g.put_edge(e, &since(2026)).is_err(),
"a slot held across a removal is not an edge"
);
}
#[test]
fn nodes_are_found_by_an_indexed_property() {
let mut g = Graph::new();
g.index_nodes("$.name", IndexKind::Equality).unwrap();
g.put_node(1, &named("ada")).unwrap();
g.put_node(2, &named("grace")).unwrap();
g.put_node(3, &named("ada")).unwrap();
let mut found = Vec::new();
let n = g
.find_nodes("$.name", &Key::text("ada"), |id, _| found.push(id))
.unwrap();
assert_eq!(n, 2);
found.sort_unstable();
assert_eq!(found, vec![1, 3]);
}
#[test]
fn edges_are_found_by_an_indexed_property() {
let mut g = Graph::new();
g.index_edges("$.since", IndexKind::Equality).unwrap();
let a = g.link(1, 2, FOLLOWS, &since(2026)).unwrap();
g.link(1, 3, FOLLOWS, &since(2024)).unwrap();
let c = g.link(2, 3, BLOCKS, &since(2026)).unwrap();
let mut found = Vec::new();
let n = g
.find_edges("$.since", &Key::int(2026), |slot, _| found.push(slot))
.unwrap();
assert_eq!(n, 2);
found.sort_unstable();
let mut want = vec![a, c];
want.sort_unstable();
assert_eq!(found, want);
}
#[test]
fn labels_are_the_ones_with_edges_in_order() {
let mut g = Graph::new();
g.link(1, 2, BLOCKS, &since(2026)).unwrap();
g.link(1, 3, FOLLOWS, &since(2026)).unwrap();
g.link(1, 4, FOLLOWS, &since(2026)).unwrap();
assert_eq!(g.labels(), [FOLLOWS, BLOCKS]);
}
#[test]
fn a_graph_that_churns_does_not_grow_forever() {
let rounds = if cfg!(miri) { 200i64 } else { 10_000 };
let mut g = Graph::new();
for year in 0..rounds {
g.link(1, 2, FOLLOWS, &since(year)).unwrap();
assert!(g.unlink(1, 2, FOLLOWS).is_some());
}
g.link(1, 2, FOLLOWS, &since(2026)).unwrap();
assert_eq!(g.edges(), 1);
assert_eq!(g.edge_props().len(), 1);
}
#[test]
fn the_names_of_edge_properties_are_stored_once() {
let edges = if cfg!(miri) { 50u64 } else { 1000 };
let mut g = Graph::new();
for dst in 2..2 + edges {
g.link(1, dst, FOLLOWS, &since(2026)).unwrap();
}
assert_eq!(g.edges() as u64, edges);
assert_eq!(g.edge_props().keys().len(), 1);
}
}