use std::collections::{HashMap, HashSet};
use crate::csr::index::LabelFilter;
#[derive(Debug, Default, Clone)]
pub struct GraphOverlayDelta {
out_edges: HashMap<String, Vec<(String, String)>>,
in_edges: HashMap<String, Vec<(String, String)>>,
tombstones: HashSet<(String, String, String)>,
}
impl GraphOverlayDelta {
pub fn new() -> Self {
Self::default()
}
pub fn stage_edge(&mut self, src: &str, label: &str, dst: &str) {
self.out_edges
.entry(src.to_string())
.or_default()
.push((label.to_string(), dst.to_string()));
self.in_edges
.entry(dst.to_string())
.or_default()
.push((label.to_string(), src.to_string()));
}
pub fn stage_tombstone(&mut self, src: &str, label: &str, dst: &str) {
self.tombstones
.insert((src.to_string(), label.to_string(), dst.to_string()));
}
pub fn is_empty(&self) -> bool {
self.out_edges.is_empty() && self.in_edges.is_empty() && self.tombstones.is_empty()
}
pub fn out_neighbors<'a>(
&'a self,
src: &str,
label_filter: &'a [&'a str],
) -> impl Iterator<Item = (&'a str, &'a str)> {
self.out_edges.get(src).into_iter().flat_map(move |edges| {
edges
.iter()
.filter(move |(label, _)| LabelFilter::keeps_name(label_filter, label))
.map(|(label, dst)| (label.as_str(), dst.as_str()))
})
}
pub fn in_neighbors<'a>(
&'a self,
dst: &str,
label_filter: &'a [&'a str],
) -> impl Iterator<Item = (&'a str, &'a str)> {
self.in_edges.get(dst).into_iter().flat_map(move |edges| {
edges
.iter()
.filter(move |(label, _)| LabelFilter::keeps_name(label_filter, label))
.map(|(label, src)| (label.as_str(), src.as_str()))
})
}
pub fn staged_endpoint_names(&self) -> impl Iterator<Item = &str> {
self.out_edges
.keys()
.chain(self.in_edges.keys())
.map(String::as_str)
}
pub fn names_node(&self, node: &str) -> bool {
self.out_edges.contains_key(node) || self.in_edges.contains_key(node)
}
pub fn is_tombstoned(&self, src: &str, label: &str, dst: &str) -> bool {
self.tombstones
.contains(&(src.to_string(), label.to_string(), dst.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_by_default() {
assert!(GraphOverlayDelta::new().is_empty());
}
#[test]
fn staged_edge_visible_both_directions() {
let mut d = GraphOverlayDelta::new();
d.stage_edge("a", "knows", "b");
assert!(!d.is_empty());
let out: Vec<_> = d.out_neighbors("a", &[]).collect();
assert_eq!(out, vec![("knows", "b")]);
let inn: Vec<_> = d.in_neighbors("b", &[]).collect();
assert_eq!(inn, vec![("knows", "a")]);
}
#[test]
fn label_filter_applied() {
let mut d = GraphOverlayDelta::new();
d.stage_edge("a", "knows", "b");
d.stage_edge("a", "likes", "c");
let out: Vec<_> = d.out_neighbors("a", &["likes"]).collect();
assert_eq!(out, vec![("likes", "c")]);
}
#[test]
fn a_label_set_keeps_every_listed_label() {
let mut d = GraphOverlayDelta::new();
d.stage_edge("a", "knows", "b");
d.stage_edge("a", "likes", "c");
d.stage_edge("a", "hates", "d");
let out: Vec<_> = d.out_neighbors("a", &["likes", "knows", "nope"]).collect();
assert_eq!(out, vec![("knows", "b"), ("likes", "c")]);
let inn: Vec<_> = d.in_neighbors("d", &["knows", "likes"]).collect();
assert!(inn.is_empty());
let none: Vec<_> = d.out_neighbors("a", &["nope"]).collect();
assert!(none.is_empty());
}
#[test]
fn tombstone_only_is_not_empty() {
let mut d = GraphOverlayDelta::new();
d.stage_tombstone("a", "knows", "b");
assert!(!d.is_empty());
assert!(d.is_tombstoned("a", "knows", "b"));
assert!(!d.is_tombstoned("a", "knows", "z"));
}
#[test]
fn names_node_covers_staged_endpoints_only() {
let mut d = GraphOverlayDelta::new();
d.stage_edge("a", "knows", "b");
d.stage_tombstone("x", "knows", "y");
assert!(d.names_node("a"));
assert!(d.names_node("b"));
assert!(!d.names_node("x"));
assert!(!d.names_node("z"));
}
}