use std::collections::VecDeque;
use crate::progress::ProgressCallback;
use crate::{errors::SqliteGraphError, graph::SqliteGraph};
pub fn weakly_connected_components(graph: &SqliteGraph) -> Result<Vec<Vec<i64>>, SqliteGraphError> {
let mut components = Vec::new();
let mut visited = ahash::AHashSet::new();
let all_ids = graph.all_entity_ids()?;
for id in all_ids {
if !visited.insert(id) {
continue; }
let mut queue = VecDeque::new();
queue.push_back(id);
let mut component = Vec::new();
while let Some(node) = queue.pop_front() {
component.push(node);
for next in graph.fetch_outgoing(node)? {
if visited.insert(next) {
queue.push_back(next);
}
}
for prev in graph.fetch_incoming(node)? {
if visited.insert(prev) {
queue.push_back(prev);
}
}
}
component.sort();
components.push(component);
}
components.sort_by(|a, b| a.first().cmp(&b.first()));
Ok(components)
}
pub fn weakly_connected_components_with_progress<F>(
graph: &SqliteGraph,
progress: &F,
) -> Result<Vec<Vec<i64>>, SqliteGraphError>
where
F: ProgressCallback,
{
let mut components = Vec::new();
let mut visited = ahash::AHashSet::new();
let all_ids = graph.all_entity_ids()?;
let total = all_ids.len();
for (idx, id) in all_ids.iter().enumerate() {
if !visited.insert(*id) {
continue; }
progress.on_progress(idx + 1, Some(total), "Finding weakly connected components");
let mut queue = VecDeque::new();
queue.push_back(*id);
let mut component = Vec::new();
while let Some(node) = queue.pop_front() {
component.push(node);
for next in graph.fetch_outgoing(node)? {
if visited.insert(next) {
queue.push_back(next);
}
}
for prev in graph.fetch_incoming(node)? {
if visited.insert(prev) {
queue.push_back(prev);
}
}
}
component.sort();
components.push(component);
}
components.sort_by(|a, b| a.first().cmp(&b.first()));
progress.on_complete();
Ok(components)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::GraphEntity;
fn create_linear_chain_graph() -> SqliteGraph {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..4 {
let entity = GraphEntity {
id: 0,
kind: "node".to_string(),
name: format!("node_{}", i),
file_path: Some(format!("node_{}.rs", i)),
data: serde_json::json!({"index": i}),
};
graph
.insert_entity(&entity)
.expect("Failed to insert entity");
}
let entity_ids = graph.list_entity_ids().expect("Failed to get IDs");
for i in 0..entity_ids.len().saturating_sub(1) {
let edge = crate::GraphEdge {
id: 0,
from_id: entity_ids[i],
to_id: entity_ids[i + 1],
edge_type: "next".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
graph
}
fn create_disconnected_graph() -> SqliteGraph {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..4 {
let entity = GraphEntity {
id: 0,
kind: "node".to_string(),
name: format!("node_{}", i),
file_path: Some(format!("node_{}.rs", i)),
data: serde_json::json!({"index": i}),
};
graph
.insert_entity(&entity)
.expect("Failed to insert entity");
}
let entity_ids = graph.list_entity_ids().expect("Failed to get IDs");
let edge1 = crate::GraphEdge {
id: 0,
from_id: entity_ids[0],
to_id: entity_ids[1],
edge_type: "next".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge1).ok();
let edge2 = crate::GraphEdge {
id: 0,
from_id: entity_ids[2],
to_id: entity_ids[3],
edge_type: "next".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge2).ok();
graph
}
#[test]
fn test_wcc_empty_graph() {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
let result = weakly_connected_components(&graph);
assert!(result.is_ok(), "WCC failed on empty graph");
let components = result.unwrap();
assert_eq!(components.len(), 0, "Expected 0 components in empty graph");
}
#[test]
fn test_wcc_single_node() {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
let entity = GraphEntity {
id: 0,
kind: "node".to_string(),
name: "single_node".to_string(),
file_path: Some("single_node.rs".to_string()),
data: serde_json::json!({}),
};
graph
.insert_entity(&entity)
.expect("Failed to insert entity");
let result = weakly_connected_components(&graph);
assert!(result.is_ok(), "WCC failed on single node");
let components = result.unwrap();
assert_eq!(components.len(), 1, "Expected 1 component");
assert_eq!(components[0].len(), 1, "Expected 1 node in component");
}
#[test]
fn test_wcc_linear_chain() {
let graph = create_linear_chain_graph();
let result = weakly_connected_components(&graph);
assert!(result.is_ok(), "WCC failed on linear chain");
let components = result.unwrap();
assert_eq!(components.len(), 1, "Expected 1 component in linear chain");
assert_eq!(
components[0].len(),
4,
"Expected all 4 nodes in single component"
);
let all_nodes: Vec<i64> = graph.list_entity_ids().expect("Failed to get IDs");
let component_nodes: Vec<i64> = components[0].clone();
assert_eq!(
all_nodes.len(),
component_nodes.len(),
"Mismatch in node count"
);
}
#[test]
fn test_wcc_disconnected() {
let graph = create_disconnected_graph();
let result = weakly_connected_components(&graph);
assert!(result.is_ok(), "WCC failed on disconnected graph");
let components = result.unwrap();
assert_eq!(
components.len(),
2,
"Expected 2 components in disconnected graph"
);
assert_eq!(
components[0].len(),
2,
"First component should have 2 nodes"
);
assert_eq!(
components[1].len(),
2,
"Second component should have 2 nodes"
);
let all_nodes: i64 = graph.list_entity_ids().expect("Failed to get IDs").len() as i64;
let component_nodes: i64 = components.iter().map(|c| c.len() as i64).sum();
assert_eq!(all_nodes, component_nodes, "Not all nodes accounted for");
}
#[test]
fn test_wcc_with_progress() {
use crate::progress::NoProgress;
let graph = create_linear_chain_graph();
let progress = NoProgress;
let result =
weakly_connected_components_with_progress(&graph, &progress).expect("WCC failed");
let result_no_progress =
weakly_connected_components(&graph).expect("WCC without progress failed");
assert_eq!(
result.len(),
result_no_progress.len(),
"Component count mismatch"
);
for (comp_with, comp_without) in result.iter().zip(result_no_progress.iter()) {
assert_eq!(comp_with, comp_without, "Component mismatch");
}
}
#[test]
fn test_wcc_deterministic_ordering() {
let graph = create_disconnected_graph();
let result1 = weakly_connected_components(&graph).expect("First WCC failed");
let result2 = weakly_connected_components(&graph).expect("Second WCC failed");
assert_eq!(result1, result2, "WCC results are non-deterministic");
}
}