use datafusion::error::Result as DataFusionResult;
use std::collections::{HashMap, HashSet, VecDeque};
pub struct RecursiveCteProcessor;
#[derive(Debug, Clone, PartialEq)]
pub struct GraphPath {
pub nodes: Vec<String>,
pub total_weight: f64,
pub depth: usize,
}
#[derive(Debug, Clone)]
pub struct RecursiveQueryConfig {
pub max_depth: usize,
pub include_cycles: bool,
pub weight_threshold: Option<f64>,
pub max_paths: Option<usize>,
}
impl Default for RecursiveQueryConfig {
fn default() -> Self {
Self {
max_depth: 10,
include_cycles: false,
weight_threshold: None,
max_paths: Some(1000),
}
}
}
impl RecursiveCteProcessor {
pub fn new() -> Self {
Self
}
pub fn find_recursive_paths(
&self,
edges: &[(String, String, f64)], start_node: &str,
end_node: Option<&str>,
config: &RecursiveQueryConfig,
) -> DataFusionResult<Vec<GraphPath>> {
let mut paths = Vec::new();
let mut queue = VecDeque::new();
let visited_in_path = HashSet::new();
let mut adjacency_map: HashMap<String, Vec<(String, f64)>> = HashMap::new();
for (source, target, weight) in edges {
adjacency_map
.entry(source.clone())
.or_insert_with(Vec::new)
.push((target.clone(), *weight));
}
let initial_path = GraphPath {
nodes: vec![start_node.to_string()],
total_weight: 0.0,
depth: 0,
};
queue.push_back((initial_path, visited_in_path.clone()));
while let Some((current_path, path_visited)) = queue.pop_front() {
if let Some(target) = end_node {
if current_path.nodes.last() == Some(&target.to_string()) {
paths.push(current_path.clone());
if let Some(max_paths) = config.max_paths {
if paths.len() >= max_paths {
break;
}
}
continue;
}
} else {
paths.push(current_path.clone());
if let Some(max_paths) = config.max_paths {
if paths.len() >= max_paths {
break;
}
}
}
if current_path.depth >= config.max_depth {
continue;
}
let current_node = current_path.nodes.last().unwrap();
if let Some(neighbors) = adjacency_map.get(current_node) {
for (neighbor, weight) in neighbors {
if !config.include_cycles && current_path.nodes.contains(neighbor) {
continue;
}
if let Some(threshold) = config.weight_threshold {
if current_path.total_weight + weight > threshold {
continue;
}
}
let mut new_nodes = current_path.nodes.clone();
new_nodes.push(neighbor.clone());
let new_path = GraphPath {
nodes: new_nodes,
total_weight: current_path.total_weight + weight,
depth: current_path.depth + 1,
};
let mut new_visited = path_visited.clone();
new_visited.insert(neighbor.clone());
queue.push_back((new_path, new_visited));
}
}
}
Ok(paths)
}
pub fn find_shortest_paths(
&self,
edges: &[(String, String, f64)],
start_node: &str,
end_node: &str,
max_depth: usize,
) -> DataFusionResult<Vec<GraphPath>> {
let config = RecursiveQueryConfig {
max_depth,
include_cycles: false,
weight_threshold: None,
max_paths: Some(10), };
let mut all_paths = self.find_recursive_paths(edges, start_node, Some(end_node), &config)?;
all_paths.sort_by(|a, b| a.total_weight.partial_cmp(&b.total_weight).unwrap_or(std::cmp::Ordering::Equal));
Ok(all_paths)
}
pub fn find_reachable_nodes(
&self,
edges: &[(String, String, f64)],
start_node: &str,
max_depth: usize,
) -> DataFusionResult<Vec<String>> {
let config = RecursiveQueryConfig {
max_depth,
include_cycles: false,
weight_threshold: None,
max_paths: None, };
let paths = self.find_recursive_paths(edges, start_node, None, &config)?;
let mut reachable: HashSet<String> = HashSet::new();
for path in paths {
for node in path.nodes {
reachable.insert(node);
}
}
reachable.remove(start_node);
Ok(reachable.into_iter().collect())
}
pub fn parse_and_execute_recursive_cte(
&self,
_cte_query: &str,
edges: &[(String, String, f64)],
) -> DataFusionResult<Vec<GraphPath>> {
let config = RecursiveQueryConfig::default();
if let Some((start_node, _, _)) = edges.first() {
self.find_recursive_paths(edges, start_node, None, &config)
} else {
Ok(Vec::new())
}
}
}
impl Default for RecursiveCteProcessor {
fn default() -> Self {
Self::new()
}
}
pub struct RecursiveGraphQueries;
impl RecursiveGraphQueries {
pub fn all_paths_query(
processor: &RecursiveCteProcessor,
edges: &[(String, String, f64)],
start: &str,
end: &str,
max_depth: usize,
) -> DataFusionResult<Vec<GraphPath>> {
let config = RecursiveQueryConfig {
max_depth,
include_cycles: false,
weight_threshold: None,
max_paths: Some(100),
};
processor.find_recursive_paths(edges, start, Some(end), &config)
}
pub fn connected_component_query(
processor: &RecursiveCteProcessor,
edges: &[(String, String, f64)],
start: &str,
) -> DataFusionResult<Vec<String>> {
processor.find_reachable_nodes(edges, start, 50) }
pub fn shortest_path_query(
processor: &RecursiveCteProcessor,
edges: &[(String, String, f64)],
start: &str,
end: &str,
) -> DataFusionResult<Option<GraphPath>> {
let paths = processor.find_shortest_paths(edges, start, end, 20)?;
Ok(paths.into_iter().next())
}
pub fn paths_within_distance_query(
processor: &RecursiveCteProcessor,
edges: &[(String, String, f64)],
start: &str,
max_distance: f64,
) -> DataFusionResult<Vec<GraphPath>> {
let config = RecursiveQueryConfig {
max_depth: 20,
include_cycles: false,
weight_threshold: Some(max_distance),
max_paths: Some(1000),
};
processor.find_recursive_paths(edges, start, None, &config)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_edges() -> Vec<(String, String, f64)> {
vec![
("A".to_string(), "B".to_string(), 1.0),
("B".to_string(), "C".to_string(), 2.0),
("C".to_string(), "D".to_string(), 1.0),
("A".to_string(), "C".to_string(), 4.0),
("B".to_string(), "D".to_string(), 3.0),
]
}
#[test]
fn test_find_recursive_paths() {
let processor = RecursiveCteProcessor::new();
let edges = create_test_edges();
let config = RecursiveQueryConfig::default();
let paths = processor.find_recursive_paths(&edges, "A", Some("D"), &config).unwrap();
assert!(!paths.is_empty());
let path_endpoints: Vec<_> = paths.iter()
.map(|p| (p.nodes.first().unwrap(), p.nodes.last().unwrap()))
.collect();
for (start, end) in path_endpoints {
assert_eq!(start, "A");
assert_eq!(end, "D");
}
}
#[test]
fn test_find_shortest_paths() {
let processor = RecursiveCteProcessor::new();
let edges = create_test_edges();
let paths = processor.find_shortest_paths(&edges, "A", "D", 5).unwrap();
assert!(!paths.is_empty());
for i in 1..paths.len() {
assert!(paths[i-1].total_weight <= paths[i].total_weight);
}
}
#[test]
fn test_find_reachable_nodes() {
let processor = RecursiveCteProcessor::new();
let edges = create_test_edges();
let reachable = processor.find_reachable_nodes(&edges, "A", 5).unwrap();
assert!(reachable.contains(&"B".to_string()));
assert!(reachable.contains(&"C".to_string()));
assert!(reachable.contains(&"D".to_string()));
assert!(!reachable.contains(&"A".to_string())); }
#[test]
fn test_recursive_query_utilities() {
let processor = RecursiveCteProcessor::new();
let edges = create_test_edges();
let all_paths = RecursiveGraphQueries::all_paths_query(&processor, &edges, "A", "D", 5).unwrap();
assert!(!all_paths.is_empty());
let component = RecursiveGraphQueries::connected_component_query(&processor, &edges, "A").unwrap();
assert!(component.len() >= 3);
let shortest = RecursiveGraphQueries::shortest_path_query(&processor, &edges, "A", "D").unwrap();
assert!(shortest.is_some());
let within_distance = RecursiveGraphQueries::paths_within_distance_query(&processor, &edges, "A", 5.0).unwrap();
assert!(!within_distance.is_empty());
}
#[test]
fn test_cycle_detection() {
let processor = RecursiveCteProcessor::new();
let edges = vec![
("A".to_string(), "B".to_string(), 1.0),
("B".to_string(), "C".to_string(), 1.0),
("C".to_string(), "A".to_string(), 1.0), ];
let config = RecursiveQueryConfig {
max_depth: 5,
include_cycles: false,
weight_threshold: None,
max_paths: Some(100),
};
let paths = processor.find_recursive_paths(&edges, "A", None, &config).unwrap();
assert!(paths.len() < 100);
for path in paths {
let mut unique_nodes = HashSet::new();
for node in &path.nodes {
assert!(unique_nodes.insert(node.clone()), "Found cycle in path: {:?}", path.nodes);
}
}
}
#[test]
fn test_max_depth_limit() {
let processor = RecursiveCteProcessor::new();
let edges = create_test_edges();
let config = RecursiveQueryConfig {
max_depth: 2,
include_cycles: false,
weight_threshold: None,
max_paths: None,
};
let paths = processor.find_recursive_paths(&edges, "A", None, &config).unwrap();
for path in paths {
assert!(path.depth <= 2, "Path exceeded max depth: {:?}", path);
}
}
}