nodedb_graph/
overlay_delta.rs1use std::collections::{HashMap, HashSet};
17
18use crate::csr::index::LabelFilter;
19
20#[derive(Debug, Default, Clone)]
25pub struct GraphOverlayDelta {
26 out_edges: HashMap<String, Vec<(String, String)>>,
28 in_edges: HashMap<String, Vec<(String, String)>>,
30 tombstones: HashSet<(String, String, String)>,
32}
33
34impl GraphOverlayDelta {
35 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn stage_edge(&mut self, src: &str, label: &str, dst: &str) {
43 self.out_edges
44 .entry(src.to_string())
45 .or_default()
46 .push((label.to_string(), dst.to_string()));
47 self.in_edges
48 .entry(dst.to_string())
49 .or_default()
50 .push((label.to_string(), src.to_string()));
51 }
52
53 pub fn stage_tombstone(&mut self, src: &str, label: &str, dst: &str) {
55 self.tombstones
56 .insert((src.to_string(), label.to_string(), dst.to_string()));
57 }
58
59 pub fn is_empty(&self) -> bool {
62 self.out_edges.is_empty() && self.in_edges.is_empty() && self.tombstones.is_empty()
63 }
64
65 pub fn out_neighbors<'a>(
69 &'a self,
70 src: &str,
71 label_filter: &'a [&'a str],
72 ) -> impl Iterator<Item = (&'a str, &'a str)> {
73 self.out_edges.get(src).into_iter().flat_map(move |edges| {
74 edges
75 .iter()
76 .filter(move |(label, _)| LabelFilter::keeps_name(label_filter, label))
77 .map(|(label, dst)| (label.as_str(), dst.as_str()))
78 })
79 }
80
81 pub fn in_neighbors<'a>(
85 &'a self,
86 dst: &str,
87 label_filter: &'a [&'a str],
88 ) -> impl Iterator<Item = (&'a str, &'a str)> {
89 self.in_edges.get(dst).into_iter().flat_map(move |edges| {
90 edges
91 .iter()
92 .filter(move |(label, _)| LabelFilter::keeps_name(label_filter, label))
93 .map(|(label, src)| (label.as_str(), src.as_str()))
94 })
95 }
96
97 pub fn staged_endpoint_names(&self) -> impl Iterator<Item = &str> {
106 self.out_edges
107 .keys()
108 .chain(self.in_edges.keys())
109 .map(String::as_str)
110 }
111
112 pub fn names_node(&self, node: &str) -> bool {
114 self.out_edges.contains_key(node) || self.in_edges.contains_key(node)
115 }
116
117 pub fn is_tombstoned(&self, src: &str, label: &str, dst: &str) -> bool {
119 self.tombstones
120 .contains(&(src.to_string(), label.to_string(), dst.to_string()))
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn empty_by_default() {
130 assert!(GraphOverlayDelta::new().is_empty());
131 }
132
133 #[test]
134 fn staged_edge_visible_both_directions() {
135 let mut d = GraphOverlayDelta::new();
136 d.stage_edge("a", "knows", "b");
137 assert!(!d.is_empty());
138 let out: Vec<_> = d.out_neighbors("a", &[]).collect();
139 assert_eq!(out, vec![("knows", "b")]);
140 let inn: Vec<_> = d.in_neighbors("b", &[]).collect();
141 assert_eq!(inn, vec![("knows", "a")]);
142 }
143
144 #[test]
145 fn label_filter_applied() {
146 let mut d = GraphOverlayDelta::new();
147 d.stage_edge("a", "knows", "b");
148 d.stage_edge("a", "likes", "c");
149 let out: Vec<_> = d.out_neighbors("a", &["likes"]).collect();
150 assert_eq!(out, vec![("likes", "c")]);
151 }
152
153 #[test]
154 fn a_label_set_keeps_every_listed_label() {
155 let mut d = GraphOverlayDelta::new();
156 d.stage_edge("a", "knows", "b");
157 d.stage_edge("a", "likes", "c");
158 d.stage_edge("a", "hates", "d");
159 let out: Vec<_> = d.out_neighbors("a", &["likes", "knows", "nope"]).collect();
160 assert_eq!(out, vec![("knows", "b"), ("likes", "c")]);
161 let inn: Vec<_> = d.in_neighbors("d", &["knows", "likes"]).collect();
162 assert!(inn.is_empty());
163 let none: Vec<_> = d.out_neighbors("a", &["nope"]).collect();
164 assert!(none.is_empty());
165 }
166
167 #[test]
168 fn tombstone_only_is_not_empty() {
169 let mut d = GraphOverlayDelta::new();
170 d.stage_tombstone("a", "knows", "b");
171 assert!(!d.is_empty());
172 assert!(d.is_tombstoned("a", "knows", "b"));
173 assert!(!d.is_tombstoned("a", "knows", "z"));
174 }
175
176 #[test]
177 fn names_node_covers_staged_endpoints_only() {
178 let mut d = GraphOverlayDelta::new();
179 d.stage_edge("a", "knows", "b");
180 d.stage_tombstone("x", "knows", "y");
181 assert!(d.names_node("a"));
182 assert!(d.names_node("b"));
183 assert!(!d.names_node("x"));
184 assert!(!d.names_node("z"));
185 }
186}