arrow_graph/sql/
multi_graph.rs

1use datafusion::error::Result as DataFusionResult;
2use std::collections::HashMap;
3
4/// Multi-graph processor for handling multiple graphs in a single query
5/// Supports queries like: SELECT * FROM graph1.edges UNION SELECT * FROM graph2.edges
6pub struct MultiGraphProcessor;
7
8/// Represents a named graph with its metadata
9#[derive(Debug, Clone, PartialEq)]
10pub struct NamedGraph {
11    pub name: String,
12    pub edges: Vec<MultiGraphEdge>,
13    pub properties: HashMap<String, String>,
14    pub creation_time: Option<i64>,
15}
16
17/// Represents an edge in a multi-graph context
18#[derive(Debug, Clone, PartialEq)]
19pub struct MultiGraphEdge {
20    pub source: String,
21    pub target: String,
22    pub weight: f64,
23    pub graph_name: String, // Which graph this edge belongs to
24    pub edge_type: Option<String>, // Optional edge type/label
25    pub properties: HashMap<String, String>,
26}
27
28/// Configuration for multi-graph queries
29#[derive(Debug, Clone)]
30pub struct MultiGraphConfig {
31    pub include_graph_metadata: bool,
32    pub merge_duplicate_edges: bool,
33    pub edge_weight_combination: EdgeWeightCombination,
34}
35
36/// How to combine edge weights when merging graphs
37#[derive(Debug, Clone)]
38pub enum EdgeWeightCombination {
39    Sum,
40    Average,
41    Maximum,
42    Minimum,
43    KeepFirst,
44    KeepLast,
45}
46
47impl Default for MultiGraphConfig {
48    fn default() -> Self {
49        Self {
50            include_graph_metadata: true,
51            merge_duplicate_edges: false,
52            edge_weight_combination: EdgeWeightCombination::Average,
53        }
54    }
55}
56
57impl MultiGraphProcessor {
58    pub fn new() -> Self {
59        Self
60    }
61
62    /// Create a new named graph
63    pub fn create_graph(
64        &self,
65        name: &str,
66        edges: Vec<MultiGraphEdge>,
67        properties: Option<HashMap<String, String>>,
68    ) -> DataFusionResult<NamedGraph> {
69        let graph = NamedGraph {
70            name: name.to_string(),
71            edges,
72            properties: properties.unwrap_or_default(),
73            creation_time: Some(std::time::SystemTime::now()
74                .duration_since(std::time::UNIX_EPOCH)
75                .unwrap()
76                .as_secs() as i64),
77        };
78
79        Ok(graph)
80    }
81
82    /// Merge multiple graphs into a single graph
83    pub fn merge_graphs(
84        &self,
85        graphs: &[NamedGraph],
86        config: &MultiGraphConfig,
87    ) -> DataFusionResult<NamedGraph> {
88        let mut merged_edges = Vec::new();
89        let mut merged_properties = HashMap::new();
90
91        // Collect all edges from all graphs
92        for graph in graphs {
93            merged_edges.extend(graph.edges.clone());
94            
95            // Merge properties with graph name prefix
96            for (key, value) in &graph.properties {
97                let prefixed_key = format!("{}_{}", graph.name, key);
98                merged_properties.insert(prefixed_key, value.clone());
99            }
100        }
101
102        // Handle duplicate edge merging if requested
103        if config.merge_duplicate_edges {
104            merged_edges = self.merge_duplicate_edges(merged_edges, &config.edge_weight_combination)?;
105        }
106
107        let merged_graph_name = graphs
108            .iter()
109            .map(|g| g.name.as_str())
110            .collect::<Vec<_>>()
111            .join("_merged_");
112
113        Ok(NamedGraph {
114            name: merged_graph_name,
115            edges: merged_edges,
116            properties: merged_properties,
117            creation_time: Some(std::time::SystemTime::now()
118                .duration_since(std::time::UNIX_EPOCH)
119                .unwrap()
120                .as_secs() as i64),
121        })
122    }
123
124    /// Merge duplicate edges according to the specified combination strategy
125    fn merge_duplicate_edges(
126        &self,
127        edges: Vec<MultiGraphEdge>,
128        combination: &EdgeWeightCombination,
129    ) -> DataFusionResult<Vec<MultiGraphEdge>> {
130        let mut edge_groups: HashMap<(String, String), Vec<MultiGraphEdge>> = HashMap::new();
131
132        // Group edges by (source, target) pair
133        for edge in edges {
134            let key = (edge.source.clone(), edge.target.clone());
135            edge_groups.entry(key).or_insert_with(Vec::new).push(edge);
136        }
137
138        let mut merged_edges = Vec::new();
139
140        for ((source, target), group) in edge_groups {
141            if group.len() == 1 {
142                // No duplicates, keep as is
143                merged_edges.push(group.into_iter().next().unwrap());
144            } else {
145                // Merge duplicates
146                let combined_weight = match combination {
147                    EdgeWeightCombination::Sum => group.iter().map(|e| e.weight).sum(),
148                    EdgeWeightCombination::Average => {
149                        group.iter().map(|e| e.weight).sum::<f64>() / group.len() as f64
150                    }
151                    EdgeWeightCombination::Maximum => {
152                        group.iter().map(|e| e.weight).fold(f64::NEG_INFINITY, f64::max)
153                    }
154                    EdgeWeightCombination::Minimum => {
155                        group.iter().map(|e| e.weight).fold(f64::INFINITY, f64::min)
156                    }
157                    EdgeWeightCombination::KeepFirst => group.first().unwrap().weight,
158                    EdgeWeightCombination::KeepLast => group.last().unwrap().weight,
159                };
160
161                // Combine graph names
162                let combined_graph_names = group
163                    .iter()
164                    .map(|e| e.graph_name.as_str())
165                    .collect::<Vec<_>>()
166                    .join(",");
167
168                // Merge properties
169                let mut combined_properties = HashMap::new();
170                for edge in &group {
171                    for (key, value) in &edge.properties {
172                        let prefixed_key = format!("{}_{}", edge.graph_name, key);
173                        combined_properties.insert(prefixed_key, value.clone());
174                    }
175                }
176
177                let merged_edge = MultiGraphEdge {
178                    source,
179                    target,
180                    weight: combined_weight,
181                    graph_name: combined_graph_names,
182                    edge_type: group.first().unwrap().edge_type.clone(),
183                    properties: combined_properties,
184                };
185
186                merged_edges.push(merged_edge);
187            }
188        }
189
190        Ok(merged_edges)
191    }
192
193    /// Query edges across multiple graphs
194    pub fn query_multi_graph(
195        &self,
196        graphs: &[NamedGraph],
197        graph_names: Option<&[String]>,
198        edge_filter: Option<Box<dyn Fn(&MultiGraphEdge) -> bool>>,
199    ) -> DataFusionResult<Vec<MultiGraphEdge>> {
200        let mut result_edges = Vec::new();
201
202        for graph in graphs {
203            // Filter by graph names if specified
204            if let Some(names) = graph_names {
205                if !names.contains(&graph.name) {
206                    continue;
207                }
208            }
209
210            // Apply edge filter if provided
211            let filtered_edges = if let Some(ref filter) = edge_filter {
212                graph.edges.iter().filter(|e| filter(e)).cloned().collect()
213            } else {
214                graph.edges.clone()
215            };
216
217            result_edges.extend(filtered_edges);
218        }
219
220        Ok(result_edges)
221    }
222
223    /// Find cross-graph connections (edges that span multiple graphs)
224    pub fn find_cross_graph_connections(
225        &self,
226        graphs: &[NamedGraph],
227    ) -> DataFusionResult<Vec<(String, String, Vec<String>)>> {
228        let mut node_to_graphs: HashMap<String, Vec<String>> = HashMap::new();
229
230        // Map each node to the graphs it appears in
231        for graph in graphs {
232            for edge in &graph.edges {
233                node_to_graphs
234                    .entry(edge.source.clone())
235                    .or_insert_with(Vec::new)
236                    .push(graph.name.clone());
237                node_to_graphs
238                    .entry(edge.target.clone())
239                    .or_insert_with(Vec::new)
240                    .push(graph.name.clone());
241            }
242        }
243
244        // Find nodes that appear in multiple graphs
245        let mut cross_graph_connections = Vec::new();
246        for (node, graph_list) in node_to_graphs {
247            if graph_list.len() > 1 {
248                // Remove duplicates and sort
249                let mut unique_graphs = graph_list;
250                unique_graphs.sort();
251                unique_graphs.dedup();
252                
253                if unique_graphs.len() > 1 {
254                    cross_graph_connections.push((
255                        "cross_graph_node".to_string(),
256                        node,
257                        unique_graphs,
258                    ));
259                }
260            }
261        }
262
263        Ok(cross_graph_connections)
264    }
265
266    /// Calculate graph similarity between two graphs
267    pub fn calculate_graph_similarity(
268        &self,
269        graph1: &NamedGraph,
270        graph2: &NamedGraph,
271    ) -> DataFusionResult<f64> {
272        // Simple Jaccard similarity based on shared edges
273        let edges1: std::collections::HashSet<(String, String)> = graph1
274            .edges
275            .iter()
276            .map(|e| (e.source.clone(), e.target.clone()))
277            .collect();
278
279        let edges2: std::collections::HashSet<(String, String)> = graph2
280            .edges
281            .iter()
282            .map(|e| (e.source.clone(), e.target.clone()))
283            .collect();
284
285        let intersection_size = edges1.intersection(&edges2).count();
286        let union_size = edges1.union(&edges2).count();
287
288        if union_size == 0 {
289            Ok(0.0)
290        } else {
291            Ok(intersection_size as f64 / union_size as f64)
292        }
293    }
294
295    /// Get graph statistics for multiple graphs
296    pub fn get_multi_graph_statistics(
297        &self,
298        graphs: &[NamedGraph],
299    ) -> DataFusionResult<HashMap<String, GraphStatistics>> {
300        let mut stats = HashMap::new();
301
302        for graph in graphs {
303            let graph_stats = self.calculate_graph_statistics(graph)?;
304            stats.insert(graph.name.clone(), graph_stats);
305        }
306
307        Ok(stats)
308    }
309
310    /// Calculate statistics for a single graph
311    fn calculate_graph_statistics(&self, graph: &NamedGraph) -> DataFusionResult<GraphStatistics> {
312        let edge_count = graph.edges.len();
313        
314        let mut nodes = std::collections::HashSet::new();
315        let mut total_weight = 0.0;
316        
317        for edge in &graph.edges {
318            nodes.insert(edge.source.clone());
319            nodes.insert(edge.target.clone());
320            total_weight += edge.weight;
321        }
322
323        let node_count = nodes.len();
324        let average_weight = if edge_count > 0 {
325            total_weight / edge_count as f64
326        } else {
327            0.0
328        };
329
330        let density = if node_count > 1 {
331            edge_count as f64 / (node_count * (node_count - 1)) as f64
332        } else {
333            0.0
334        };
335
336        Ok(GraphStatistics {
337            node_count,
338            edge_count,
339            average_edge_weight: average_weight,
340            density,
341            total_weight,
342        })
343    }
344}
345
346/// Statistics for a graph
347#[derive(Debug, Clone, PartialEq)]
348pub struct GraphStatistics {
349    pub node_count: usize,
350    pub edge_count: usize,
351    pub average_edge_weight: f64,
352    pub density: f64,
353    pub total_weight: f64,
354}
355
356impl Default for MultiGraphProcessor {
357    fn default() -> Self {
358        Self::new()
359    }
360}
361
362/// Utility functions for common multi-graph operations
363pub struct MultiGraphQueries;
364
365impl MultiGraphQueries {
366    /// Find the largest graph by edge count
367    pub fn find_largest_graph(graphs: &[NamedGraph]) -> Option<&NamedGraph> {
368        graphs.iter().max_by_key(|g| g.edges.len())
369    }
370
371    /// Find graphs containing a specific node
372    pub fn find_graphs_with_node<'a>(graphs: &'a [NamedGraph], node_id: &str) -> Vec<&'a NamedGraph> {
373        graphs
374            .iter()
375            .filter(|graph| {
376                graph.edges.iter().any(|edge| 
377                    edge.source == node_id || edge.target == node_id
378                )
379            })
380            .collect()
381    }
382
383    /// Get all unique nodes across multiple graphs
384    pub fn get_all_unique_nodes(graphs: &[NamedGraph]) -> Vec<String> {
385        let mut nodes = std::collections::HashSet::new();
386        
387        for graph in graphs {
388            for edge in &graph.edges {
389                nodes.insert(edge.source.clone());
390                nodes.insert(edge.target.clone());
391            }
392        }
393
394        nodes.into_iter().collect()
395    }
396
397    /// Find overlapping nodes between graphs
398    pub fn find_overlapping_nodes(graph1: &NamedGraph, graph2: &NamedGraph) -> Vec<String> {
399        let nodes1: std::collections::HashSet<String> = graph1
400            .edges
401            .iter()
402            .flat_map(|e| vec![e.source.clone(), e.target.clone()])
403            .collect();
404
405        let nodes2: std::collections::HashSet<String> = graph2
406            .edges
407            .iter()
408            .flat_map(|e| vec![e.source.clone(), e.target.clone()])
409            .collect();
410
411        nodes1.intersection(&nodes2).cloned().collect()
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    fn create_test_graph(name: &str, edges: Vec<(String, String, f64)>) -> NamedGraph {
420        let multi_edges = edges
421            .into_iter()
422            .map(|(source, target, weight)| MultiGraphEdge {
423                source,
424                target,
425                weight,
426                graph_name: name.to_string(),
427                edge_type: None,
428                properties: HashMap::new(),
429            })
430            .collect();
431
432        NamedGraph {
433            name: name.to_string(),
434            edges: multi_edges,
435            properties: HashMap::new(),
436            creation_time: Some(1704067200),
437        }
438    }
439
440    #[test]
441    fn test_create_graph() {
442        let processor = MultiGraphProcessor::new();
443        
444        let edges = vec![
445            MultiGraphEdge {
446                source: "A".to_string(),
447                target: "B".to_string(),
448                weight: 1.0,
449                graph_name: "test_graph".to_string(),
450                edge_type: None,
451                properties: HashMap::new(),
452            }
453        ];
454
455        let graph = processor.create_graph("test_graph", edges, None).unwrap();
456        assert_eq!(graph.name, "test_graph");
457        assert_eq!(graph.edges.len(), 1);
458    }
459
460    #[test]
461    fn test_merge_graphs() {
462        let processor = MultiGraphProcessor::new();
463        
464        let graph1 = create_test_graph("graph1", vec![
465            ("A".to_string(), "B".to_string(), 1.0),
466            ("B".to_string(), "C".to_string(), 2.0),
467        ]);
468
469        let graph2 = create_test_graph("graph2", vec![
470            ("C".to_string(), "D".to_string(), 3.0),
471            ("D".to_string(), "A".to_string(), 4.0),
472        ]);
473
474        let config = MultiGraphConfig::default();
475        let merged = processor.merge_graphs(&[graph1, graph2], &config).unwrap();
476        
477        assert_eq!(merged.edges.len(), 4);
478        assert!(merged.name.contains("merged"));
479    }
480
481    #[test]
482    fn test_merge_duplicate_edges() {
483        let processor = MultiGraphProcessor::new();
484        
485        let edges = vec![
486            MultiGraphEdge {
487                source: "A".to_string(),
488                target: "B".to_string(),
489                weight: 1.0,
490                graph_name: "graph1".to_string(),
491                edge_type: None,
492                properties: HashMap::new(),
493            },
494            MultiGraphEdge {
495                source: "A".to_string(),
496                target: "B".to_string(),
497                weight: 3.0,
498                graph_name: "graph2".to_string(),
499                edge_type: None,
500                properties: HashMap::new(),
501            },
502        ];
503
504        let merged = processor.merge_duplicate_edges(edges, &EdgeWeightCombination::Average).unwrap();
505        
506        assert_eq!(merged.len(), 1);
507        assert_eq!(merged[0].weight, 2.0); // Average of 1.0 and 3.0
508        assert!(merged[0].graph_name.contains("graph1") && merged[0].graph_name.contains("graph2"));
509    }
510
511    #[test]
512    fn test_query_multi_graph() {
513        let processor = MultiGraphProcessor::new();
514        
515        let graph1 = create_test_graph("graph1", vec![
516            ("A".to_string(), "B".to_string(), 1.0),
517            ("B".to_string(), "C".to_string(), 2.0),
518        ]);
519
520        let graph2 = create_test_graph("graph2", vec![
521            ("C".to_string(), "D".to_string(), 3.0),
522            ("D".to_string(), "A".to_string(), 4.0),
523        ]);
524
525        // Query specific graphs
526        let result = processor.query_multi_graph(
527            &[graph1.clone(), graph2.clone()],
528            Some(&vec!["graph1".to_string()]),
529            None,
530        ).unwrap();
531        
532        assert_eq!(result.len(), 2); // Only edges from graph1
533
534        // Query with edge filter
535        let result_filtered = processor.query_multi_graph(
536            &[graph1, graph2],
537            None,
538            Some(Box::new(|edge| edge.weight > 2.0)),
539        ).unwrap();
540        
541        assert_eq!(result_filtered.len(), 2); // Edges with weight > 2.0
542    }
543
544    #[test]
545    fn test_find_cross_graph_connections() {
546        let processor = MultiGraphProcessor::new();
547        
548        let graph1 = create_test_graph("graph1", vec![
549            ("A".to_string(), "B".to_string(), 1.0),
550            ("B".to_string(), "C".to_string(), 2.0),
551        ]);
552
553        let graph2 = create_test_graph("graph2", vec![
554            ("C".to_string(), "D".to_string(), 3.0), // C appears in both graphs
555            ("D".to_string(), "E".to_string(), 4.0),
556        ]);
557
558        let connections = processor.find_cross_graph_connections(&[graph1, graph2]).unwrap();
559        
560        assert!(!connections.is_empty());
561        // Should find node C as it appears in both graphs
562        assert!(connections.iter().any(|(_, node, _)| node == "C"));
563    }
564
565    #[test]
566    fn test_calculate_graph_similarity() {
567        let processor = MultiGraphProcessor::new();
568        
569        let graph1 = create_test_graph("graph1", vec![
570            ("A".to_string(), "B".to_string(), 1.0),
571            ("B".to_string(), "C".to_string(), 2.0),
572        ]);
573
574        let graph2 = create_test_graph("graph2", vec![
575            ("A".to_string(), "B".to_string(), 1.0), // Same edge
576            ("C".to_string(), "D".to_string(), 3.0), // Different edge
577        ]);
578
579        let similarity = processor.calculate_graph_similarity(&graph1, &graph2).unwrap();
580        
581        // Jaccard similarity: 1 shared edge / 3 total unique edges = 1/3 ≈ 0.33
582        assert!(similarity > 0.0 && similarity < 1.0);
583    }
584
585    #[test]
586    fn test_get_multi_graph_statistics() {
587        let processor = MultiGraphProcessor::new();
588        
589        let graph1 = create_test_graph("graph1", vec![
590            ("A".to_string(), "B".to_string(), 1.0),
591            ("B".to_string(), "C".to_string(), 2.0),
592        ]);
593
594        let graph2 = create_test_graph("graph2", vec![
595            ("C".to_string(), "D".to_string(), 3.0),
596        ]);
597
598        let stats = processor.get_multi_graph_statistics(&[graph1, graph2]).unwrap();
599        
600        assert_eq!(stats.len(), 2);
601        assert!(stats.contains_key("graph1"));
602        assert!(stats.contains_key("graph2"));
603        
604        let graph1_stats = &stats["graph1"];
605        assert_eq!(graph1_stats.edge_count, 2);
606        assert_eq!(graph1_stats.node_count, 3); // A, B, C
607    }
608
609    #[test]
610    fn test_multi_graph_queries() {
611        let graph1 = create_test_graph("graph1", vec![
612            ("A".to_string(), "B".to_string(), 1.0),
613            ("B".to_string(), "C".to_string(), 2.0),
614        ]);
615
616        let graph2 = create_test_graph("graph2", vec![
617            ("C".to_string(), "D".to_string(), 3.0),
618            ("D".to_string(), "E".to_string(), 4.0),
619        ]);
620
621        let graphs = vec![graph1, graph2];
622
623        // Test largest graph
624        let largest = MultiGraphQueries::find_largest_graph(&graphs);
625        assert!(largest.is_some());
626        assert_eq!(largest.unwrap().edges.len(), 2);
627
628        // Test find graphs with node
629        let graphs_with_c = MultiGraphQueries::find_graphs_with_node(&graphs, "C");
630        assert_eq!(graphs_with_c.len(), 2); // C appears in both graphs
631
632        // Test all unique nodes
633        let all_nodes = MultiGraphQueries::get_all_unique_nodes(&graphs);
634        assert!(all_nodes.len() >= 5); // A, B, C, D, E
635
636        // Test overlapping nodes
637        let overlapping = MultiGraphQueries::find_overlapping_nodes(&graphs[0], &graphs[1]);
638        assert!(overlapping.contains(&"C".to_string()));
639    }
640
641    #[test]
642    fn test_edge_weight_combinations() {
643        let processor = MultiGraphProcessor::new();
644        
645        let edges = vec![
646            MultiGraphEdge {
647                source: "A".to_string(),
648                target: "B".to_string(),
649                weight: 2.0,
650                graph_name: "graph1".to_string(),
651                edge_type: None,
652                properties: HashMap::new(),
653            },
654            MultiGraphEdge {
655                source: "A".to_string(),
656                target: "B".to_string(),
657                weight: 4.0,
658                graph_name: "graph2".to_string(),
659                edge_type: None,
660                properties: HashMap::new(),
661            },
662        ];
663
664        // Test different combination strategies
665        let sum_result = processor.merge_duplicate_edges(edges.clone(), &EdgeWeightCombination::Sum).unwrap();
666        assert_eq!(sum_result[0].weight, 6.0);
667
668        let max_result = processor.merge_duplicate_edges(edges.clone(), &EdgeWeightCombination::Maximum).unwrap();
669        assert_eq!(max_result[0].weight, 4.0);
670
671        let min_result = processor.merge_duplicate_edges(edges.clone(), &EdgeWeightCombination::Minimum).unwrap();
672        assert_eq!(min_result[0].weight, 2.0);
673
674        let first_result = processor.merge_duplicate_edges(edges.clone(), &EdgeWeightCombination::KeepFirst).unwrap();
675        assert_eq!(first_result[0].weight, 2.0);
676
677        let last_result = processor.merge_duplicate_edges(edges, &EdgeWeightCombination::KeepLast).unwrap();
678        assert_eq!(last_result[0].weight, 4.0);
679    }
680}