Skip to main content

selene_graph/
adjacency.rs

1//! Adjacency entries per spec 03 section 3.1.
2
3use smallvec::SmallVec;
4
5use selene_core::{DbString, EdgeId, NodeId};
6
7/// One edge recorded in a node's incoming or outgoing adjacency list.
8#[derive(Clone, Debug, Eq, Hash, PartialEq)]
9pub struct AdjacencyEdge {
10    /// Edge label.
11    pub label: DbString,
12    /// Opposite endpoint reached through this edge.
13    pub neighbor: NodeId,
14    /// Stable edge ID.
15    pub edge_id: EdgeId,
16}
17
18/// Sorted adjacency list for one node.
19#[derive(Clone, Debug, Eq, Hash, PartialEq)]
20pub struct AdjacencyEntry {
21    /// Edges sorted by `(label, neighbor, edge_id)`.
22    pub edges: SmallVec<[AdjacencyEdge; 4]>,
23}
24
25impl AdjacencyEntry {
26    /// Construct an empty adjacency entry.
27    #[must_use]
28    pub fn new() -> Self {
29        Self {
30            edges: SmallVec::new(),
31        }
32    }
33
34    /// Number of adjacent edges.
35    #[must_use]
36    pub fn len(&self) -> usize {
37        self.edges.len()
38    }
39
40    /// Return true when no adjacent edges are recorded.
41    #[must_use]
42    pub fn is_empty(&self) -> bool {
43        self.edges.is_empty()
44    }
45
46    /// Iterate adjacent edges in sorted order.
47    pub fn iter(&self) -> impl Iterator<Item = &AdjacencyEdge> {
48        self.edges.iter()
49    }
50
51    /// Iterate adjacent edges with exactly `label`.
52    ///
53    /// Entries are sorted by `(label, neighbor, edge_id)`, so this uses a
54    /// bounded label range rather than scanning unrelated edge labels.
55    pub fn iter_label<'a>(&'a self, label: &DbString) -> impl Iterator<Item = &'a AdjacencyEdge> {
56        let range = self.label_range(label);
57        self.edges[range].iter()
58    }
59
60    /// Insert `edge` while maintaining sorted order.
61    pub fn add(&mut self, edge: AdjacencyEdge) {
62        let key = adjacency_key(&edge);
63        let index = self
64            .edges
65            .binary_search_by_key(&key, adjacency_key)
66            .unwrap_or_else(|index| index);
67        self.edges.insert(index, edge);
68    }
69
70    /// Remove the edge with `edge_id`, returning it when present.
71    pub fn remove(&mut self, edge_id: EdgeId) -> Option<AdjacencyEdge> {
72        self.edges
73            .iter()
74            .position(|edge| edge.edge_id == edge_id)
75            .map(|index| self.edges.remove(index))
76    }
77
78    fn label_range(&self, label: &DbString) -> std::ops::Range<usize> {
79        let start = self
80            .edges
81            .partition_point(|edge| edge.label.as_str() < label.as_str());
82        let len = self.edges[start..].partition_point(|edge| edge.label.as_str() == label.as_str());
83        start..start + len
84    }
85
86    #[cfg(test)]
87    fn spilled(&self) -> bool {
88        self.edges.spilled()
89    }
90}
91
92impl Default for AdjacencyEntry {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98fn adjacency_key(edge: &AdjacencyEdge) -> (DbString, NodeId, EdgeId) {
99    (edge.label.clone(), edge.neighbor, edge.edge_id)
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use selene_core::db_string;
106
107    fn label(name: &str) -> DbString {
108        db_string(name).unwrap()
109    }
110
111    fn edge(label_name: &str, neighbor: u64, edge_id: u64) -> AdjacencyEdge {
112        AdjacencyEdge {
113            label: label(label_name),
114            neighbor: NodeId::new(neighbor),
115            edge_id: EdgeId::new(edge_id),
116        }
117    }
118
119    #[test]
120    fn add_inserts_in_sorted_order() {
121        let mut entry = AdjacencyEntry::new();
122        let a = edge("adj.a", 2, 2);
123        let b = edge("adj.a", 2, 1);
124        let c = edge("adj.b", 1, 3);
125        entry.add(c.clone());
126        entry.add(a.clone());
127        entry.add(b.clone());
128        assert_eq!(entry.iter().cloned().collect::<Vec<_>>(), vec![b, a, c]);
129    }
130
131    #[test]
132    fn add_handles_inline_path_under_4_edges() {
133        let mut entry = AdjacencyEntry::new();
134        for id in 1..=4 {
135            entry.add(edge("adj.inline", id, id));
136        }
137        assert_eq!(entry.len(), 4);
138        assert!(!entry.spilled());
139    }
140
141    #[test]
142    fn add_spills_after_4_edges() {
143        let mut entry = AdjacencyEntry::new();
144        for id in 1..=5 {
145            entry.add(edge("adj.spill", id, id));
146        }
147        assert!(entry.spilled());
148    }
149
150    #[test]
151    fn remove_returns_removed_edge() {
152        let mut entry = AdjacencyEntry::new();
153        let e = edge("adj.remove", 2, 1);
154        entry.add(e.clone());
155        assert_eq!(entry.remove(EdgeId::new(1)), Some(e));
156        assert!(entry.is_empty());
157    }
158
159    #[test]
160    fn remove_returns_none_when_absent() {
161        let mut entry = AdjacencyEntry::new();
162        assert_eq!(entry.remove(EdgeId::new(1)), None);
163    }
164
165    #[test]
166    fn iter_label_returns_only_matching_label_range() {
167        let mut entry = AdjacencyEntry::new();
168        let a1 = edge("adj.a", 3, 1);
169        let a2 = edge("adj.a", 4, 2);
170        let b = edge("adj.b", 5, 3);
171        let c = edge("adj.c", 6, 4);
172        entry.add(c);
173        entry.add(a2.clone());
174        entry.add(b);
175        entry.add(a1.clone());
176
177        assert_eq!(
178            entry
179                .iter_label(&label("adj.a"))
180                .cloned()
181                .collect::<Vec<_>>(),
182            vec![a1, a2],
183        );
184    }
185
186    #[test]
187    fn iter_label_returns_empty_for_absent_labels() {
188        let mut entry = AdjacencyEntry::new();
189        entry.add(edge("adj.a", 2, 1));
190        entry.add(edge("adj.c", 3, 2));
191
192        assert_eq!(entry.iter_label(&label("adj.b")).count(), 0);
193    }
194
195    #[test]
196    fn parallel_edges_sort_by_edge_id() {
197        let mut entry = AdjacencyEntry::new();
198        let first = edge("adj.parallel", 2, 1);
199        let second = edge("adj.parallel", 2, 2);
200        entry.add(second.clone());
201        entry.add(first.clone());
202        assert_eq!(
203            entry.iter().cloned().collect::<Vec<_>>(),
204            vec![first, second]
205        );
206    }
207}