use std::collections::HashSet;
#[cfg(test)]
use super::DEFAULT_MAX_VISITED;
use crate::csr::{CsrIndex, Direction};
use crate::overlay_delta::GraphOverlayDelta;
use crate::traversal_overlay::{OverlaySubgraphParams, subgraph_overlay};
impl CsrIndex {
pub fn subgraph(
&self,
start_nodes: &[&str],
label_filter: &[&str],
direction: Direction,
max_depth: usize,
max_visited: usize,
overlay: Option<&GraphOverlayDelta>,
) -> Vec<(String, String, String)> {
Self::subgraph_on(
Some(self),
start_nodes,
label_filter,
direction,
max_depth,
max_visited,
overlay,
)
}
pub fn subgraph_on(
partition: Option<&Self>,
start_nodes: &[&str],
label_filter: &[&str],
direction: Direction,
max_depth: usize,
max_visited: usize,
overlay: Option<&GraphOverlayDelta>,
) -> Vec<(String, String, String)> {
match (partition, overlay) {
(_, Some(ov)) if !ov.is_empty() => subgraph_overlay(
partition,
OverlaySubgraphParams {
start_nodes,
label_filter,
direction,
max_depth,
max_visited,
},
ov,
),
(Some(csr), _) => {
csr.subgraph_dense(start_nodes, label_filter, direction, max_depth, max_visited)
}
(None, _) => Vec::new(),
}
}
fn subgraph_dense(
&self,
start_nodes: &[&str],
label_filter: &[&str],
direction: Direction,
max_depth: usize,
max_visited: usize,
) -> Vec<(String, String, String)> {
let labels = self.label_filter(label_filter);
let mut visited: HashSet<u32> = HashSet::new();
let mut frontier: Vec<u32> = Vec::new();
let mut edges = Vec::new();
let mut seen: HashSet<(u32, u32, u32)> = HashSet::new();
for &node in start_nodes {
if let Some(&id) = self.node_to_id.get(node)
&& visited.insert(id)
{
frontier.push(id);
}
}
for _depth in 0..max_depth {
if frontier.is_empty() || visited.len() >= max_visited {
break;
}
let mut candidates: Vec<u32> = Vec::new();
for &node_id in &frontier {
self.record_access(node_id);
if matches!(direction, Direction::Out | Direction::Both) {
for (lid, dst) in self.dense_iter_out(node_id) {
if labels.keeps(lid) {
if seen.insert((node_id, lid, dst)) {
edges.push((
self.id_to_node[node_id as usize].clone(),
self.label_name(lid).to_string(),
self.id_to_node[dst as usize].clone(),
));
}
if !visited.contains(&dst) {
candidates.push(dst);
}
}
}
}
if matches!(direction, Direction::In | Direction::Both) {
for (lid, src) in self.dense_iter_in(node_id) {
if labels.keeps(lid) {
if seen.insert((src, lid, node_id)) {
edges.push((
self.id_to_node[src as usize].clone(),
self.label_name(lid).to_string(),
self.id_to_node[node_id as usize].clone(),
));
}
if !visited.contains(&src) {
candidates.push(src);
}
}
}
}
}
frontier = self.admit_by_name(candidates, &mut visited, max_visited);
}
for &node_id in &frontier {
if matches!(direction, Direction::Out | Direction::Both) {
for (lid, dst) in self.dense_iter_out(node_id) {
if labels.keeps(lid)
&& visited.contains(&dst)
&& seen.insert((node_id, lid, dst))
{
edges.push((
self.id_to_node[node_id as usize].clone(),
self.label_name(lid).to_string(),
self.id_to_node[dst as usize].clone(),
));
}
}
}
if matches!(direction, Direction::In | Direction::Both) {
for (lid, src) in self.dense_iter_in(node_id) {
if labels.keeps(lid)
&& visited.contains(&src)
&& seen.insert((src, lid, node_id))
{
edges.push((
self.id_to_node[src as usize].clone(),
self.label_name(lid).to_string(),
self.id_to_node[node_id as usize].clone(),
));
}
}
}
}
edges
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{chain_csr, test_memory};
#[test]
fn subgraph_materialization() {
let csr = chain_csr();
let edges = csr.subgraph(&["a"], &[], Direction::Out, 2, DEFAULT_MAX_VISITED, None);
assert_eq!(edges.len(), 3);
assert!(edges.contains(&("a".into(), "KNOWS".into(), "b".into())));
assert!(edges.contains(&("a".into(), "WORKS".into(), "e".into())));
assert!(edges.contains(&("b".into(), "KNOWS".into(), "c".into())));
}
#[test]
fn a_capped_subgraph_expands_only_admitted_levels() {
let mut csr = CsrIndex::new(test_memory());
csr.add_edge("a", "L", "z").unwrap();
csr.add_edge("a", "L", "m").unwrap();
csr.add_edge("a", "L", "b").unwrap();
csr.add_edge("z", "L", "y").unwrap();
csr.add_edge("b", "L", "c").unwrap();
let mut edges = csr.subgraph(&["a"], &[], Direction::Out, 3, 3, None);
edges.sort();
let expected: Vec<(String, String, String)> = ["b", "m", "z"]
.iter()
.map(|dst| ("a".to_string(), "L".to_string(), dst.to_string()))
.collect();
assert_eq!(edges, expected);
}
#[test]
fn both_directions_return_each_physical_edge_once() {
let mut csr = CsrIndex::new(test_memory());
csr.add_edge("a", "L", "b").unwrap();
csr.add_edge("b", "L", "a").unwrap();
csr.add_edge("a", "SELF", "a").unwrap();
csr.add_edge_in_collection("a", "L", "c", "first").unwrap();
csr.add_edge_in_collection("a", "L", "c", "second").unwrap();
let mut edges = csr.subgraph(&["a"], &[], Direction::Both, 3, DEFAULT_MAX_VISITED, None);
edges.sort();
let expected: Vec<(String, String, String)> = [
("a", "L", "b"),
("a", "L", "c"),
("a", "SELF", "a"),
("b", "L", "a"),
]
.iter()
.map(|(s, l, d)| (s.to_string(), l.to_string(), d.to_string()))
.collect();
assert_eq!(edges, expected);
}
fn triples(edges: &[(&str, &str, &str)]) -> Vec<(String, String, String)> {
let mut out: Vec<(String, String, String)> = edges
.iter()
.map(|(s, l, d)| (s.to_string(), l.to_string(), d.to_string()))
.collect();
out.sort();
out
}
fn boundary_csr() -> CsrIndex {
let mut csr = CsrIndex::new(test_memory());
for (src, dst) in [
("a", "b"),
("a", "c"),
("b", "c"),
("c", "b"),
("c", "a"),
("c", "x"),
] {
csr.add_edge(src, "L", dst).unwrap();
}
csr
}
#[test]
fn boundary_nodes_record_edges_among_admitted_nodes() {
let csr = boundary_csr();
let mut staged = GraphOverlayDelta::new();
staged.stage_tombstone("q", "L", "r");
for overlay in [None, Some(&staged)] {
let mut edges =
csr.subgraph(&["a"], &[], Direction::Out, 1, DEFAULT_MAX_VISITED, overlay);
edges.sort();
assert_eq!(
edges,
triples(&[
("a", "L", "b"),
("a", "L", "c"),
("b", "L", "c"),
("c", "L", "a"),
("c", "L", "b"),
]),
"c -> x leaves the admitted set"
);
}
}
#[test]
fn boundary_edges_follow_the_walk_direction() {
let mut csr = CsrIndex::new(test_memory());
for (src, dst) in [("b", "a"), ("c", "a"), ("b", "c"), ("a", "z")] {
csr.add_edge(src, "L", dst).unwrap();
}
let mut edges = csr.subgraph(&["a"], &[], Direction::In, 1, DEFAULT_MAX_VISITED, None);
edges.sort();
assert_eq!(
edges,
triples(&[("b", "L", "a"), ("b", "L", "c"), ("c", "L", "a")])
);
}
#[test]
fn depth_zero_records_only_the_start_nodes_edges_among_themselves() {
let mut csr = CsrIndex::new(test_memory());
csr.add_edge("a", "L", "a").unwrap();
csr.add_edge("a", "L", "b").unwrap();
let edges = csr.subgraph(&["a"], &[], Direction::Out, 0, DEFAULT_MAX_VISITED, None);
assert_eq!(edges, triples(&[("a", "L", "a")]));
}
#[test]
fn a_label_set_returns_parallel_edges_of_both_labels() {
let mut csr = CsrIndex::new(test_memory());
csr.add_edge("a", "K", "b").unwrap();
csr.add_edge("a", "W", "b").unwrap();
csr.add_edge("a", "X", "c").unwrap();
let mut edges = csr.subgraph(
&["a"],
&["K", "W"],
Direction::Out,
2,
DEFAULT_MAX_VISITED,
None,
);
edges.sort();
let expected: Vec<(String, String, String)> = [("a", "K", "b"), ("a", "W", "b")]
.iter()
.map(|(s, l, d)| (s.to_string(), l.to_string(), d.to_string()))
.collect();
assert_eq!(edges, expected);
}
}