1use datafusion::error::Result as DataFusionResult;
2use std::collections::HashMap;
3
4pub struct MultiGraphProcessor;
7
8#[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#[derive(Debug, Clone, PartialEq)]
19pub struct MultiGraphEdge {
20 pub source: String,
21 pub target: String,
22 pub weight: f64,
23 pub graph_name: String, pub edge_type: Option<String>, pub properties: HashMap<String, String>,
26}
27
28#[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#[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 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 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 for graph in graphs {
93 merged_edges.extend(graph.edges.clone());
94
95 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 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 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 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 merged_edges.push(group.into_iter().next().unwrap());
144 } else {
145 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 let combined_graph_names = group
163 .iter()
164 .map(|e| e.graph_name.as_str())
165 .collect::<Vec<_>>()
166 .join(",");
167
168 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 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 if let Some(names) = graph_names {
205 if !names.contains(&graph.name) {
206 continue;
207 }
208 }
209
210 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 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 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 let mut cross_graph_connections = Vec::new();
246 for (node, graph_list) in node_to_graphs {
247 if graph_list.len() > 1 {
248 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 pub fn calculate_graph_similarity(
268 &self,
269 graph1: &NamedGraph,
270 graph2: &NamedGraph,
271 ) -> DataFusionResult<f64> {
272 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 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 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#[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
362pub struct MultiGraphQueries;
364
365impl MultiGraphQueries {
366 pub fn find_largest_graph(graphs: &[NamedGraph]) -> Option<&NamedGraph> {
368 graphs.iter().max_by_key(|g| g.edges.len())
369 }
370
371 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 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 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); 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 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); 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); }
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), ("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 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), ("C".to_string(), "D".to_string(), 3.0), ]);
578
579 let similarity = processor.calculate_graph_similarity(&graph1, &graph2).unwrap();
580
581 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); }
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 let largest = MultiGraphQueries::find_largest_graph(&graphs);
625 assert!(largest.is_some());
626 assert_eq!(largest.unwrap().edges.len(), 2);
627
628 let graphs_with_c = MultiGraphQueries::find_graphs_with_node(&graphs, "C");
630 assert_eq!(graphs_with_c.len(), 2); let all_nodes = MultiGraphQueries::get_all_unique_nodes(&graphs);
634 assert!(all_nodes.len() >= 5); 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 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}