use crate::graph::schema::{ConnectivityTriple, DirGraph, GraphBackend, InternedKey};
use crate::graph::storage::disk::graph::DiskGraph;
use crate::graph::storage::GraphRead;
use std::collections::{HashMap, HashSet};
use super::{NeighborConnection, NeighborsSchema};
type CountMap = HashMap<(InternedKey, InternedKey, InternedKey), usize>;
pub fn compute_type_connectivity(graph: &DirGraph) -> Vec<ConnectivityTriple> {
let backend = &graph.graph;
let counts: CountMap = match disk_scan_target(backend) {
Some(dg) => compute_disk_parallel(dg),
None => compute_serial(backend),
};
let mut triples: Vec<ConnectivityTriple> = counts
.into_iter()
.map(|((sk, ck, tk), count)| ConnectivityTriple {
src: graph.interner.resolve(sk).to_string(),
conn: graph.interner.resolve(ck).to_string(),
tgt: graph.interner.resolve(tk).to_string(),
count,
})
.collect();
triples.sort_by_key(|t| std::cmp::Reverse(t.count));
triples
}
fn disk_scan_target(backend: &GraphBackend) -> Option<&DiskGraph> {
backend.as_disk()
}
fn compute_disk_parallel(dg: &DiskGraph) -> CountMap {
use crate::graph::storage::disk::csr::TOMBSTONE_EDGE;
use petgraph::graph::NodeIndex;
use rayon::prelude::*;
let total = (dg.next_edge_idx as usize).min(dg.edge_endpoint_len());
if total == 0 {
return HashMap::new();
}
dg.edge_endpoints.advise_sequential();
let chunk = (total / rayon::current_num_threads().max(1)).max(1 << 20);
let ranges: Vec<(usize, usize)> = (0..total)
.step_by(chunk)
.map(|lo| (lo, (lo + chunk).min(total)))
.collect();
let shard_maps: Vec<CountMap> = ranges
.into_par_iter()
.map(|(lo, hi)| {
let mut acc: CountMap = HashMap::new();
for i in lo..hi {
let ep = dg.edge_endpoint(i);
if ep.source == TOMBSTONE_EDGE {
continue;
}
let src = NodeIndex::new(ep.source as usize);
let tgt = NodeIndex::new(ep.target as usize);
if let (Some(sk), Some(tk)) = (dg.node_type_of(src), dg.node_type_of(tgt)) {
let conn = InternedKey::from_u64(ep.connection_type);
*acc.entry((sk, conn, tk)).or_insert(0) += 1;
}
}
acc
})
.collect();
dg.edge_endpoints.advise_dontneed();
let mut combined: CountMap = HashMap::new();
for shard in shard_maps {
for (k, v) in shard {
*combined.entry(k).or_insert(0) += v;
}
}
combined
}
fn compute_serial(backend: &GraphBackend) -> CountMap {
let mut counts: CountMap = HashMap::new();
backend.for_each_edge_endpoint_key(|src_idx, tgt_idx, conn_key| {
let src_key = backend.node_type_of(src_idx);
let tgt_key = backend.node_type_of(tgt_idx);
if let (Some(sk), Some(tk)) = (src_key, tgt_key) {
*counts.entry((sk, conn_key, tk)).or_insert(0) += 1;
}
});
counts
}
pub fn neighbors_from_triples(triples: &[ConnectivityTriple], node_type: &str) -> NeighborsSchema {
let mut outgoing: Vec<NeighborConnection> = Vec::new();
let mut incoming: Vec<NeighborConnection> = Vec::new();
for t in triples {
if t.src == node_type {
outgoing.push(NeighborConnection {
connection_type: t.conn.clone(),
other_type: t.tgt.clone(),
count: t.count,
});
}
if t.tgt == node_type {
incoming.push(NeighborConnection {
connection_type: t.conn.clone(),
other_type: t.src.clone(),
count: t.count,
});
}
}
outgoing.sort_by_key(|o| std::cmp::Reverse(o.count));
incoming.sort_by_key(|i| std::cmp::Reverse(i.count));
NeighborsSchema { outgoing, incoming }
}
pub struct TypeConnectivityIndex {
index: HashMap<String, NeighborsSchema>,
}
impl TypeConnectivityIndex {
pub fn from_triples(triples: &[ConnectivityTriple]) -> Self {
let mut out_map: HashMap<String, Vec<NeighborConnection>> = HashMap::new();
let mut in_map: HashMap<String, Vec<NeighborConnection>> = HashMap::new();
for t in triples {
out_map
.entry(t.src.clone())
.or_default()
.push(NeighborConnection {
connection_type: t.conn.clone(),
other_type: t.tgt.clone(),
count: t.count,
});
in_map
.entry(t.tgt.clone())
.or_default()
.push(NeighborConnection {
connection_type: t.conn.clone(),
other_type: t.src.clone(),
count: t.count,
});
}
let all_types: HashSet<String> = out_map.keys().chain(in_map.keys()).cloned().collect();
let mut index = HashMap::with_capacity(all_types.len());
for nt in all_types {
let mut outgoing = out_map.remove(&nt).unwrap_or_default();
outgoing.sort_by_key(|o| std::cmp::Reverse(o.count));
let mut incoming = in_map.remove(&nt).unwrap_or_default();
incoming.sort_by_key(|i| std::cmp::Reverse(i.count));
index.insert(nt, NeighborsSchema { outgoing, incoming });
}
TypeConnectivityIndex { index }
}
pub fn get(&self, node_type: &str) -> NeighborsSchema {
self.index
.get(node_type)
.cloned()
.unwrap_or(NeighborsSchema {
outgoing: Vec::new(),
incoming: Vec::new(),
})
}
}
pub struct DerivedEdgeStats {
pub counts: HashMap<String, usize>,
pub endpoints: HashMap<String, (HashSet<String>, HashSet<String>)>,
}
pub fn derive_edge_counts_from_triples(triples: &[ConnectivityTriple]) -> DerivedEdgeStats {
let mut counts: HashMap<String, usize> = HashMap::new();
let mut endpoints: HashMap<String, (HashSet<String>, HashSet<String>)> = HashMap::new();
for t in triples {
*counts.entry(t.conn.clone()).or_insert(0) += t.count;
let entry = endpoints
.entry(t.conn.clone())
.or_insert_with(|| (HashSet::new(), HashSet::new()));
entry.0.insert(t.src.clone());
entry.1.insert(t.tgt.clone());
}
DerivedEdgeStats { counts, endpoints }
}
#[cfg(test)]
mod capture_wrapped_routing_tests {
use super::*;
use crate::datatypes::{DataFrame, Value};
use tempfile::TempDir;
fn disk_graph(dir: &TempDir) -> DirGraph {
let people = DataFrame::from_cypher_rows(
vec!["id".into(), "title".into()],
vec![
vec![Value::Int64(1), Value::String("p1".into())],
vec![Value::Int64(2), Value::String("p2".into())],
],
)
.unwrap();
let cities = DataFrame::from_cypher_rows(
vec!["id".into(), "title".into()],
vec![vec![Value::Int64(10), Value::String("Oslo".into())]],
)
.unwrap();
let visits = DataFrame::from_cypher_rows(
vec!["src".into(), "tgt".into()],
vec![
vec![Value::Int64(1), Value::Int64(10)],
vec![Value::Int64(2), Value::Int64(10)],
],
)
.unwrap();
let mut graph = DirGraph::new();
crate::graph::mutation::maintain::add_nodes(
&mut graph,
people,
"Person".to_string(),
"id".to_string(),
Some("title".to_string()),
None,
)
.unwrap();
crate::graph::mutation::maintain::add_nodes(
&mut graph,
cities,
"City".to_string(),
"id".to_string(),
Some("title".to_string()),
None,
)
.unwrap();
crate::graph::mutation::maintain::add_connections(
&mut graph,
visits,
"VISITED".to_string(),
"Person".to_string(),
"src".to_string(),
"City".to_string(),
"tgt".to_string(),
None,
None,
None,
)
.unwrap();
graph.enable_disk_mode().unwrap();
graph.save_disk(dir.path().to_str().unwrap()).unwrap();
graph
}
fn triples_of(graph: &DirGraph) -> Vec<(String, String, String, usize)> {
compute_type_connectivity(graph)
.into_iter()
.map(|t| (t.src, t.conn, t.tgt, t.count))
.collect()
}
#[test]
fn a_capture_wrapped_disk_graph_still_routes_to_the_parallel_scan() {
let dir = TempDir::new().unwrap();
let mut graph = disk_graph(&dir);
assert!(
disk_scan_target(&graph.graph).is_some(),
"fixture must be a disk graph before wrapping"
);
let bare = triples_of(&graph);
graph.graph.wrap_for_durability();
assert!(
matches!(&graph.graph, GraphBackend::Recording(_)),
"the wrap must have taken effect, or this test asserts nothing"
);
assert!(
disk_scan_target(&graph.graph).is_some(),
"a durability-wrapped disk graph must still reach compute_disk_parallel"
);
let wrapped = triples_of(&graph);
assert_eq!(bare, wrapped, "both scans must agree on the triples");
assert_eq!(
wrapped,
vec![(
"Person".to_string(),
"VISITED".to_string(),
"City".to_string(),
2
)]
);
}
}