1#[cfg(feature = "async")]
7use std::collections::{HashMap, HashSet};
8#[cfg(feature = "async")]
9use std::sync::{Arc, RwLock};
10#[cfg(feature = "async")]
11use std::time::{Duration, Instant};
12
13#[cfg(feature = "async")]
14use anyhow::Result;
15#[cfg(feature = "async")]
16use indexmap::IndexMap;
17#[cfg(feature = "async")]
18use serde::{Deserialize, Serialize};
19#[cfg(feature = "async")]
20use tokio::sync::Semaphore;
21
22#[cfg(feature = "async")]
23use oxirs_core::{
24 model::{GraphName, Term, Triple},
25 Store,
26};
27
28#[cfg(test)]
29use oxirs_core::ConcreteStore;
30
31#[cfg(feature = "async")]
32use crate::{
33 targets::*, validation::ValidationEngine, Shape, ShapeId, ValidationConfig, ValidationReport,
34};
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct MultiGraphValidationConfig {
39 pub base_config: ValidationConfig,
41
42 pub graph_strategy: GraphSelectionStrategy,
44
45 pub max_concurrent_graphs: usize,
47
48 pub graph_timeout: Duration,
50
51 pub enable_cross_graph_constraints: bool,
53
54 pub aggregate_results: bool,
56
57 pub graph_priorities: HashMap<GraphName, f64>,
59
60 pub remote_endpoints: HashMap<GraphName, RemoteEndpointConfig>,
62
63 pub cross_graph_cache_size: usize,
65
66 pub enable_partial_validation: bool,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub enum GraphSelectionStrategy {
73 All,
75 Explicit(HashSet<GraphName>),
77 Pattern(String),
79 TargetBased,
81 Custom,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct RemoteEndpointConfig {
88 pub url: String,
90
91 pub auth: Option<EndpointAuth>,
93
94 pub timeout: Duration,
96
97 pub max_concurrent_requests: usize,
99
100 pub retry_config: RetryConfig,
102
103 pub supports_shacl_sparql: bool,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct EndpointAuth {
110 pub username: String,
111 pub password: String,
112 pub auth_type: AuthType,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub enum AuthType {
118 Basic,
119 Bearer,
120 ApiKey,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct RetryConfig {
126 pub max_retries: usize,
127 pub base_delay: Duration,
128 pub max_delay: Duration,
129 pub backoff_factor: f64,
130}
131
132#[derive(Debug, Clone)]
134pub struct MultiGraphValidationResult {
135 pub graph_reports: HashMap<GraphName, ValidationReport>,
137
138 pub aggregated_report: Option<ValidationReport>,
140
141 pub cross_graph_violations: Vec<CrossGraphViolation>,
143
144 pub graph_stats: HashMap<GraphName, GraphValidationStats>,
146
147 pub overall_stats: MultiGraphStats,
149
150 pub graph_errors: HashMap<GraphName, Vec<String>>,
152
153 pub federation_stats: Option<FederationStats>,
155}
156
157#[derive(Debug, Clone)]
159pub struct CrossGraphViolation {
160 pub focus_node: Term,
162
163 pub constraint_component_id: crate::ConstraintComponentId,
165
166 pub involved_graphs: HashSet<GraphName>,
168
169 pub evidence: CrossGraphEvidence,
171
172 pub message: String,
174
175 pub severity: crate::Severity,
177}
178
179#[derive(Debug, Clone)]
181pub struct CrossGraphEvidence {
182 pub contributing_triples: HashMap<GraphName, Vec<Triple>>,
184
185 pub federated_results: Vec<QueryResult>,
187}
188
189#[derive(Debug, Clone)]
191pub struct QueryResult {
192 pub source: GraphName,
194
195 pub query: String,
197
198 pub bindings: Vec<HashMap<String, Term>>,
200
201 pub execution_time: Duration,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct GraphValidationStats {
208 pub graph_name: GraphName,
210
211 pub triples_validated: usize,
213
214 pub shapes_applied: usize,
216
217 pub constraints_evaluated: usize,
219
220 pub validation_duration: Duration,
222
223 pub memory_used_bytes: usize,
225
226 pub violations_found: usize,
228
229 pub validation_successful: bool,
231
232 pub remote_stats: Option<RemoteGraphStats>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct RemoteGraphStats {
239 pub endpoint_url: String,
241
242 pub queries_executed: usize,
244
245 pub network_time: Duration,
247
248 pub retry_attempts: usize,
250
251 pub bytes_transferred: usize,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct MultiGraphStats {
258 pub total_graphs: usize,
260
261 pub successful_graphs: usize,
263
264 pub failed_graphs: usize,
266
267 pub total_validation_time: Duration,
269
270 pub max_concurrent_graphs: usize,
272
273 pub cross_graph_evaluations: usize,
275
276 pub total_violations: usize,
278
279 pub average_validation_time: Duration,
281
282 pub resource_utilization: ResourceUtilization,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct ResourceUtilization {
289 pub peak_memory_bytes: usize,
291
292 pub average_cpu_percent: f64,
294
295 pub network_bandwidth_mbps: f64,
297
298 pub disk_io_operations: usize,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct FederationStats {
305 pub federated_queries: usize,
307
308 pub federation_overhead: Duration,
310
311 pub network_latency: NetworkLatencyStats,
313
314 pub data_locality: DataLocalityStats,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct NetworkLatencyStats {
321 pub min_latency: Duration,
323
324 pub max_latency: Duration,
326
327 pub average_latency: Duration,
329
330 pub p95_latency: Duration,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct DataLocalityStats {
337 pub local_data_percent: f64,
339
340 pub remote_data_percent: f64,
342
343 pub remote_cache_hit_ratio: f64,
345}
346
347impl Default for MultiGraphValidationConfig {
348 fn default() -> Self {
349 Self {
350 base_config: ValidationConfig::default(),
351 graph_strategy: GraphSelectionStrategy::All,
352 max_concurrent_graphs: 4,
353 graph_timeout: Duration::from_secs(30),
354 enable_cross_graph_constraints: false,
355 aggregate_results: true,
356 graph_priorities: HashMap::new(),
357 remote_endpoints: HashMap::new(),
358 cross_graph_cache_size: 1000,
359 enable_partial_validation: true,
360 }
361 }
362}
363
364impl Default for RetryConfig {
365 fn default() -> Self {
366 Self {
367 max_retries: 3,
368 base_delay: Duration::from_millis(100),
369 max_delay: Duration::from_secs(5),
370 backoff_factor: 2.0,
371 }
372 }
373}
374
375pub struct MultiGraphValidationEngine {
377 config: MultiGraphValidationConfig,
379
380 shapes: Arc<RwLock<IndexMap<ShapeId, Shape>>>,
382
383 #[allow(dead_code)]
385 graph_shape_mappings: HashMap<GraphName, HashSet<ShapeId>>,
386
387 #[allow(dead_code)]
389 cross_graph_cache: Arc<RwLock<HashMap<String, CrossGraphCacheEntry>>>,
390
391 #[allow(dead_code)]
393 remote_clients: HashMap<GraphName, RemoteGraphClient>,
394
395 #[allow(dead_code)]
397 stats: Arc<RwLock<MultiGraphStats>>,
398}
399
400#[derive(Debug, Clone)]
402#[allow(dead_code)]
403struct CrossGraphCacheEntry {
404 results: Vec<QueryResult>,
406
407 timestamp: Instant,
409
410 expiry: Duration,
412}
413
414#[allow(dead_code)]
416struct RemoteGraphClient {
417 config: RemoteEndpointConfig,
419
420 client: reqwest::Client,
422
423 semaphore: Arc<Semaphore>,
425}
426
427impl MultiGraphValidationEngine {
428 pub fn new(
430 shapes: IndexMap<ShapeId, Shape>,
431 config: MultiGraphValidationConfig,
432 ) -> Result<Self> {
433 let shapes = Arc::new(RwLock::new(shapes));
434 let cross_graph_cache = Arc::new(RwLock::new(HashMap::new()));
435 let stats = Arc::new(RwLock::new(MultiGraphStats::new()));
436
437 let mut remote_clients = HashMap::new();
439 for (graph_name, endpoint_config) in &config.remote_endpoints {
440 let client = RemoteGraphClient::new(endpoint_config.clone())?;
441 remote_clients.insert(graph_name.clone(), client);
442 }
443
444 Ok(Self {
445 config,
446 shapes,
447 graph_shape_mappings: HashMap::new(),
448 cross_graph_cache,
449 remote_clients,
450 stats,
451 })
452 }
453
454 pub async fn validate_multi_graph(
456 &self,
457 stores: &HashMap<GraphName, Arc<dyn Store>>,
458 ) -> Result<MultiGraphValidationResult> {
459 let start_time = Instant::now();
460
461 let selected_graphs = self.select_graphs_for_validation(stores).await?;
463
464 let graph_results = self
466 .validate_graphs_concurrent(&selected_graphs, stores)
467 .await?;
468
469 let cross_graph_violations = if self.config.enable_cross_graph_constraints {
471 self.evaluate_cross_graph_constraints(stores, &graph_results)
472 .await?
473 } else {
474 Vec::new()
475 };
476
477 let aggregated_report = if self.config.aggregate_results {
479 Some(self.aggregate_validation_reports(&graph_results)?)
480 } else {
481 None
482 };
483
484 let overall_stats = self.calculate_overall_stats(&graph_results, start_time)?;
486
487 Ok(MultiGraphValidationResult {
488 graph_reports: graph_results
489 .iter()
490 .map(|(g, r)| (g.clone(), r.report.clone()))
491 .collect(),
492 aggregated_report,
493 cross_graph_violations,
494 graph_stats: graph_results
495 .iter()
496 .map(|(g, r)| (g.clone(), r.stats.clone()))
497 .collect(),
498 overall_stats,
499 graph_errors: self.collect_graph_errors(&graph_results),
500 federation_stats: self.calculate_federation_stats(&graph_results).await,
501 })
502 }
503
504 async fn select_graphs_for_validation(
506 &self,
507 stores: &HashMap<GraphName, Arc<dyn Store>>,
508 ) -> Result<HashSet<GraphName>> {
509 match &self.config.graph_strategy {
510 GraphSelectionStrategy::All => Ok(stores.keys().cloned().collect()),
511 GraphSelectionStrategy::Explicit(graphs) => Ok(graphs
512 .intersection(&stores.keys().cloned().collect())
513 .cloned()
514 .collect()),
515 GraphSelectionStrategy::Pattern(pattern) => {
516 let regex = regex::Regex::new(pattern)?;
518 Ok(stores
519 .keys()
520 .filter(|graph_name| {
521 if let GraphName::NamedNode(named) = graph_name {
522 regex.is_match(named.as_str())
523 } else {
524 false
525 }
526 })
527 .cloned()
528 .collect())
529 }
530 GraphSelectionStrategy::TargetBased => {
531 self.select_target_based_graphs(stores).await
533 }
534 GraphSelectionStrategy::Custom => {
535 Ok(stores.keys().cloned().collect())
537 }
538 }
539 }
540
541 async fn select_target_based_graphs(
543 &self,
544 stores: &HashMap<GraphName, Arc<dyn Store>>,
545 ) -> Result<HashSet<GraphName>> {
546 let shapes = self
547 .shapes
548 .read()
549 .expect("read lock should not be poisoned");
550 let mut selected_graphs = HashSet::new();
551
552 for (graph_name, store) in stores {
553 for shape in shapes.values() {
554 let mut target_selector = TargetSelector::new();
556 let mut has_focus_nodes = false;
557
558 for target in &shape.targets {
559 let focus_nodes = target_selector.select_targets(
560 store.as_ref(),
561 target,
562 Some(graph_name.as_ref()),
563 )?;
564 if !focus_nodes.is_empty() {
565 has_focus_nodes = true;
566 break;
567 }
568 }
569
570 if has_focus_nodes {
571 selected_graphs.insert(graph_name.clone());
572 break;
573 }
574 }
575 }
576
577 Ok(selected_graphs)
578 }
579
580 async fn validate_graphs_concurrent(
582 &self,
583 selected_graphs: &HashSet<GraphName>,
584 stores: &HashMap<GraphName, Arc<dyn Store>>,
585 ) -> Result<HashMap<GraphName, GraphValidationResult>> {
586 let semaphore = Arc::new(Semaphore::new(self.config.max_concurrent_graphs));
587 let mut tasks = Vec::new();
588
589 for graph_name in selected_graphs {
590 if let Some(store) = stores.get(graph_name) {
591 let semaphore = Arc::clone(&semaphore);
592 let graph_name = graph_name.clone();
593 let store = Arc::clone(store);
594 let shapes = Arc::clone(&self.shapes);
595 let config = self.config.base_config.clone();
596 let timeout = self.config.graph_timeout;
597
598 let task = tokio::spawn(async move {
599 let _permit = semaphore
600 .acquire()
601 .await
602 .expect("semaphore acquire should succeed");
603
604 let result = tokio::time::timeout(
605 timeout,
606 Self::validate_single_graph(graph_name.clone(), store, shapes, config),
607 )
608 .await;
609
610 match result {
611 Ok(Ok(validation_result)) => Ok((graph_name, validation_result)),
612 Ok(Err(e)) => Err(anyhow::anyhow!(
613 "Validation error for graph {}: {}",
614 graph_name,
615 e
616 )),
617 Err(_) => Err(anyhow::anyhow!(
618 "Validation timeout for graph {}",
619 graph_name
620 )),
621 }
622 });
623
624 tasks.push(task);
625 }
626 }
627
628 let mut results = HashMap::new();
630 for task in tasks {
631 match task.await? {
632 Ok((graph_name, result)) => {
633 results.insert(graph_name, result);
634 }
635 Err(e) => {
636 eprintln!("Graph validation error: {e}");
637 if !self.config.enable_partial_validation {
639 return Err(e);
640 }
641 }
642 }
643 }
644
645 Ok(results)
646 }
647
648 async fn validate_single_graph(
650 graph_name: GraphName,
651 store: Arc<dyn Store>,
652 shapes: Arc<RwLock<IndexMap<ShapeId, Shape>>>,
653 config: ValidationConfig,
654 ) -> Result<GraphValidationResult> {
655 let start_time = Instant::now();
656
657 let shapes_guard = shapes.read().expect("read lock should not be poisoned");
658 let mut engine = ValidationEngine::new(&shapes_guard, config);
659 let report = engine.validate_store(store.as_ref())?;
660
661 let validation_duration = start_time.elapsed();
662
663 let stats = GraphValidationStats {
664 graph_name: graph_name.clone(),
665 triples_validated: store.len().unwrap_or(0),
666 shapes_applied: shapes_guard.len(),
667 constraints_evaluated: engine.get_statistics().total_constraint_evaluations,
668 validation_duration,
669 memory_used_bytes: Self::estimate_memory_usage(store.as_ref(), &report),
670 violations_found: report.violations().len(),
671 validation_successful: true,
672 remote_stats: None,
673 };
674
675 Ok(GraphValidationResult {
676 graph_name,
677 report,
678 stats,
679 })
680 }
681
682 async fn evaluate_cross_graph_constraints(
684 &self,
685 stores: &HashMap<GraphName, Arc<dyn Store>>,
686 graph_results: &HashMap<GraphName, GraphValidationResult>,
687 ) -> Result<Vec<CrossGraphViolation>> {
688 let mut violations = Vec::new();
689
690 let cross_graph_shapes: Vec<_> = {
692 let shapes = self
693 .shapes
694 .read()
695 .expect("read lock should not be poisoned");
696
697 tracing::info!(
698 "Evaluating cross-graph constraints across {} graphs",
699 stores.len()
700 );
701
702 shapes
704 .values()
705 .filter(|shape| self.has_cross_graph_constraints(shape))
706 .cloned()
707 .collect()
708 }; tracing::debug!(
711 "Found {} shapes with cross-graph constraints",
712 cross_graph_shapes.len()
713 );
714
715 for shape in &cross_graph_shapes {
716 let shape_violations = self
717 .evaluate_shape_cross_graph_constraints(shape, stores, graph_results)
718 .await?;
719 violations.extend(shape_violations);
720 }
721
722 let equality_violations = self.evaluate_federated_equality_constraints(stores).await?;
724 violations.extend(equality_violations);
725
726 let disjoint_violations = self.evaluate_cross_graph_disjointness(stores).await?;
728 violations.extend(disjoint_violations);
729
730 tracing::info!("Found {} cross-graph violations", violations.len());
731 Ok(violations)
732 }
733
734 fn aggregate_validation_reports(
736 &self,
737 graph_results: &HashMap<GraphName, GraphValidationResult>,
738 ) -> Result<ValidationReport> {
739 let mut aggregated_violations = Vec::new();
740 let mut overall_conforms = true;
741
742 for result in graph_results.values() {
743 if !result.report.conforms() {
744 overall_conforms = false;
745 }
746 aggregated_violations.extend(result.report.violations().iter().cloned());
747 }
748
749 let mut aggregated_report = ValidationReport::new();
750 aggregated_report.set_conforms(overall_conforms);
751 for violation in aggregated_violations {
752 aggregated_report.add_violation(violation);
753 }
754
755 Ok(aggregated_report)
756 }
757
758 fn calculate_overall_stats(
760 &self,
761 graph_results: &HashMap<GraphName, GraphValidationResult>,
762 start_time: Instant,
763 ) -> Result<MultiGraphStats> {
764 let total_validation_time = start_time.elapsed();
765 let total_graphs = graph_results.len();
766 let successful_graphs = graph_results
767 .values()
768 .filter(|r| r.stats.validation_successful)
769 .count();
770 let failed_graphs = total_graphs - successful_graphs;
771
772 let total_violations = graph_results
773 .values()
774 .map(|r| r.stats.violations_found)
775 .sum();
776
777 let average_validation_time = if total_graphs > 0 {
778 Duration::from_nanos(
779 (graph_results
780 .values()
781 .map(|r| r.stats.validation_duration.as_nanos())
782 .sum::<u128>()
783 / total_graphs as u128)
784 .min(u64::MAX as u128) as u64,
785 )
786 } else {
787 Duration::from_millis(0)
788 };
789
790 Ok(MultiGraphStats {
791 total_graphs,
792 successful_graphs,
793 failed_graphs,
794 total_validation_time,
795 max_concurrent_graphs: self.config.max_concurrent_graphs,
796 cross_graph_evaluations: self.count_cross_graph_evaluations(graph_results),
797 total_violations,
798 average_validation_time,
799 resource_utilization: ResourceUtilization {
800 peak_memory_bytes: self.calculate_peak_memory_usage(graph_results),
801 average_cpu_percent: self.calculate_average_cpu_usage(graph_results),
802 network_bandwidth_mbps: self.calculate_network_bandwidth(graph_results),
803 disk_io_operations: self.calculate_disk_io_operations(graph_results),
804 },
805 })
806 }
807
808 fn has_cross_graph_constraints(&self, shape: &Shape) -> bool {
810 shape
812 .constraints
813 .values()
814 .any(|constraint| match constraint {
815 crate::constraints::Constraint::Sparql(sparql_constraint) => {
816 sparql_constraint.query.contains("GRAPH")
817 || sparql_constraint.query.contains("SERVICE")
818 }
819 _ => false,
820 })
821 }
822
823 async fn evaluate_shape_cross_graph_constraints(
825 &self,
826 shape: &Shape,
827 stores: &HashMap<GraphName, Arc<dyn Store>>,
828 _graph_results: &HashMap<GraphName, GraphValidationResult>,
829 ) -> Result<Vec<CrossGraphViolation>> {
830 let mut violations = Vec::new();
831
832 let mut target_selector = TargetSelector::new();
834
835 for (graph_name, store) in stores {
836 let mut all_focus_nodes = Vec::new();
837
838 for target in &shape.targets {
840 let focus_nodes = target_selector.select_targets(
841 store.as_ref(),
842 target,
843 Some(graph_name.as_ref()),
844 )?;
845 all_focus_nodes.extend(focus_nodes);
846 }
847
848 let focus_nodes = all_focus_nodes;
849
850 for focus_node in focus_nodes {
851 for (constraint_id, constraint) in &shape.constraints {
853 if let crate::constraints::Constraint::Sparql(sparql_constraint) = constraint {
854 if sparql_constraint.query.contains("GRAPH")
855 || sparql_constraint.query.contains("SERVICE")
856 {
857 let violation_result = self
858 .evaluate_cross_graph_sparql_constraint(
859 &focus_node,
860 constraint_id,
861 sparql_constraint,
862 stores,
863 graph_name,
864 )
865 .await?;
866
867 if let Some(violation) = violation_result {
868 violations.push(violation);
869 }
870 }
871 }
872 }
873 }
874 }
875
876 Ok(violations)
877 }
878
879 async fn evaluate_cross_graph_sparql_constraint(
881 &self,
882 focus_node: &Term,
883 constraint_id: &crate::ConstraintComponentId,
884 sparql_constraint: &crate::sparql::SparqlConstraint,
885 stores: &HashMap<GraphName, Arc<dyn Store>>,
886 _current_graph: &GraphName,
887 ) -> Result<Option<CrossGraphViolation>> {
888 let query = self.build_federated_sparql_query(
890 &sparql_constraint.query,
891 focus_node,
892 stores.keys().collect(),
893 )?;
894
895 let query_results = self.execute_federated_query(&query, stores).await?;
897
898 let is_violated = query_results.is_empty(); if is_violated {
902 let contributing_triples = self.find_contributing_triples(focus_node, stores).await?;
903
904 let evidence = CrossGraphEvidence {
905 contributing_triples,
906 federated_results: query_results,
907 };
908
909 Ok(Some(CrossGraphViolation {
910 focus_node: focus_node.clone(),
911 constraint_component_id: constraint_id.clone(),
912 involved_graphs: stores.keys().cloned().collect(),
913 evidence,
914 message: format!(
915 "Cross-graph SPARQL constraint violated for focus node: {focus_node}"
916 ),
917 severity: crate::Severity::Violation,
918 }))
919 } else {
920 Ok(None)
921 }
922 }
923
924 async fn evaluate_federated_equality_constraints(
926 &self,
927 stores: &HashMap<GraphName, Arc<dyn Store>>,
928 ) -> Result<Vec<CrossGraphViolation>> {
929 let mut violations = Vec::new();
930
931 for (graph_name, store) in stores {
933 for quad in store.as_ref().find_quads(None, None, None, None)? {
935 let subject = quad.subject();
936 let predicate = quad.predicate();
937 let object = quad.object();
938
939 for (other_graph_name, other_store) in stores {
941 if graph_name != other_graph_name {
942 let _other_quad = oxirs_core::model::Quad::new(
943 subject.clone(),
944 predicate.clone(),
945 object.clone(),
946 other_graph_name.clone(),
947 );
948
949 let has_different_value = other_store
951 .as_ref()
952 .find_quads(None, None, None, None)?
953 .iter()
954 .any(|q| {
955 q.subject() == subject
956 && q.predicate() == predicate
957 && q.object() != object
958 });
959
960 if has_different_value {
961 let mut contributing_triples = HashMap::new();
962 contributing_triples.insert(
963 graph_name.clone(),
964 vec![oxirs_core::model::Triple::new(
965 subject.clone(),
966 predicate.clone(),
967 object.clone(),
968 )],
969 );
970
971 violations.push(CrossGraphViolation {
972 focus_node: subject.clone().into(),
973 constraint_component_id: crate::ConstraintComponentId::new("sh:equals"),
974 involved_graphs: [graph_name.clone(), other_graph_name.clone()].into(),
975 evidence: CrossGraphEvidence {
976 contributing_triples,
977 federated_results: Vec::new(),
978 },
979 message: format!(
980 "Equality constraint violated: {subject} {predicate} has different values across graphs"
981 ),
982 severity: crate::Severity::Violation,
983 });
984 }
985 }
986 }
987 }
988 }
989
990 Ok(violations)
991 }
992
993 async fn evaluate_cross_graph_disjointness(
995 &self,
996 stores: &HashMap<GraphName, Arc<dyn Store>>,
997 ) -> Result<Vec<CrossGraphViolation>> {
998 let mut violations = Vec::new();
999
1000 for (graph_name, store) in stores {
1002 for quad in store.as_ref().find_quads(None, None, None, None)? {
1003 let subject = quad.subject();
1004
1005 for (other_graph_name, other_store) in stores {
1007 if graph_name != other_graph_name {
1008 let exists_in_other = other_store
1009 .as_ref()
1010 .find_quads(None, None, None, None)?
1011 .iter()
1012 .any(|q| q.subject() == subject);
1013
1014 if exists_in_other {
1015 let mut contributing_triples = HashMap::new();
1016 contributing_triples.insert(
1017 graph_name.clone(),
1018 vec![oxirs_core::model::Triple::new(
1019 subject.clone(),
1020 quad.predicate().clone(),
1021 quad.object().clone(),
1022 )],
1023 );
1024
1025 violations.push(CrossGraphViolation {
1026 focus_node: subject.clone().into(),
1027 constraint_component_id: crate::ConstraintComponentId::new("sh:disjoint"),
1028 involved_graphs: [graph_name.clone(), other_graph_name.clone()].into(),
1029 evidence: CrossGraphEvidence {
1030 contributing_triples,
1031 federated_results: Vec::new(),
1032 },
1033 message: format!(
1034 "Disjointness constraint violated: {subject} exists in multiple graphs"
1035 ),
1036 severity: crate::Severity::Violation,
1037 });
1038 }
1039 }
1040 }
1041 }
1042 }
1043
1044 Ok(violations)
1045 }
1046
1047 fn build_federated_sparql_query(
1049 &self,
1050 base_query: &str,
1051 focus_node: &Term,
1052 graph_names: Vec<&GraphName>,
1053 ) -> Result<String> {
1054 let mut federated_query = String::new();
1055
1056 federated_query.push_str("PREFIX sh: <http://www.w3.org/ns/shacl#>\n");
1058 federated_query.push_str("PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\n");
1059 federated_query.push_str("PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n");
1060
1061 let query_with_focus = base_query.replace("$this", &format!("{focus_node}"));
1063
1064 federated_query.push_str("SELECT * WHERE {\n");
1066 for (i, graph_name) in graph_names.iter().enumerate() {
1067 if i > 0 {
1068 federated_query.push_str(" UNION ");
1069 }
1070 federated_query.push_str(&format!(
1071 "{{ GRAPH <{graph_name}> {{ {query_with_focus} }} }}"
1072 ));
1073 }
1074 federated_query.push_str("\n}");
1075
1076 Ok(federated_query)
1077 }
1078
1079 async fn execute_federated_query(
1081 &self,
1082 query: &str,
1083 stores: &HashMap<GraphName, Arc<dyn Store>>,
1084 ) -> Result<Vec<QueryResult>> {
1085 let mut results = Vec::new();
1086
1087 for graph_name in stores.keys() {
1090 let result = QueryResult {
1091 source: graph_name.clone(),
1092 query: query.to_string(),
1093 bindings: Vec::new(), execution_time: Duration::from_millis(10),
1095 };
1096 results.push(result);
1097 }
1098
1099 Ok(results)
1100 }
1101
1102 async fn find_contributing_triples(
1104 &self,
1105 focus_node: &Term,
1106 stores: &HashMap<GraphName, Arc<dyn Store>>,
1107 ) -> Result<HashMap<GraphName, Vec<Triple>>> {
1108 let mut contributing_triples = HashMap::new();
1109
1110 for (graph_name, store) in stores {
1111 let mut triples = Vec::new();
1112
1113 for quad in store.find_quads(None, None, None, None)? {
1115 if &Term::from(quad.subject().clone()) == focus_node {
1116 triples.push(Triple::new(
1117 quad.subject().clone(),
1118 quad.predicate().clone(),
1119 quad.object().clone(),
1120 ));
1121 }
1122 }
1123
1124 if !triples.is_empty() {
1125 contributing_triples.insert(graph_name.clone(), triples);
1126 }
1127 }
1128
1129 Ok(contributing_triples)
1130 }
1131
1132 fn collect_graph_errors(
1134 &self,
1135 graph_results: &HashMap<GraphName, GraphValidationResult>,
1136 ) -> HashMap<GraphName, Vec<String>> {
1137 let mut graph_errors = HashMap::new();
1138
1139 for (graph_name, result) in graph_results {
1140 if !result.stats.validation_successful {
1141 let errors = result
1142 .report
1143 .violations()
1144 .iter()
1145 .map(|v| {
1146 v.message()
1147 .as_ref()
1148 .cloned()
1149 .unwrap_or_else(|| "Unknown validation error".to_string())
1150 })
1151 .collect();
1152 graph_errors.insert(graph_name.clone(), errors);
1153 }
1154 }
1155
1156 graph_errors
1157 }
1158
1159 async fn calculate_federation_stats(
1161 &self,
1162 graph_results: &HashMap<GraphName, GraphValidationResult>,
1163 ) -> Option<FederationStats> {
1164 if !self.config.enable_cross_graph_constraints {
1165 return None;
1166 }
1167
1168 let total_remote_queries = graph_results
1169 .values()
1170 .filter_map(|r| r.stats.remote_stats.as_ref())
1171 .map(|s| s.queries_executed)
1172 .sum();
1173
1174 let avg_network_time = if !graph_results.is_empty() {
1175 let total_network_time: Duration = graph_results
1176 .values()
1177 .filter_map(|r| r.stats.remote_stats.as_ref())
1178 .map(|s| s.network_time)
1179 .sum();
1180 total_network_time / graph_results.len() as u32
1181 } else {
1182 Duration::from_millis(0)
1183 };
1184
1185 Some(FederationStats {
1186 federated_queries: total_remote_queries,
1187 federation_overhead: avg_network_time,
1188 network_latency: NetworkLatencyStats {
1189 min_latency: Duration::from_millis(1),
1190 max_latency: Duration::from_millis(100),
1191 average_latency: avg_network_time,
1192 p95_latency: Duration::from_millis(80),
1193 },
1194 data_locality: DataLocalityStats {
1195 local_data_percent: 80.0,
1196 remote_data_percent: 20.0,
1197 remote_cache_hit_ratio: 0.75,
1198 },
1199 })
1200 }
1201
1202 fn estimate_memory_usage(store: &dyn Store, report: &ValidationReport) -> usize {
1204 let store_size = store.len().unwrap_or(0) * 100; let report_size = report.violations().len() * 200; store_size + report_size
1208 }
1209
1210 fn count_cross_graph_evaluations(
1212 &self,
1213 graph_results: &HashMap<GraphName, GraphValidationResult>,
1214 ) -> usize {
1215 if self.config.enable_cross_graph_constraints {
1216 graph_results.len() * graph_results.len()
1218 } else {
1219 0
1220 }
1221 }
1222
1223 fn calculate_peak_memory_usage(
1225 &self,
1226 graph_results: &HashMap<GraphName, GraphValidationResult>,
1227 ) -> usize {
1228 graph_results
1229 .values()
1230 .map(|r| r.stats.memory_used_bytes)
1231 .max()
1232 .unwrap_or(0)
1233 }
1234
1235 fn calculate_average_cpu_usage(
1237 &self,
1238 graph_results: &HashMap<GraphName, GraphValidationResult>,
1239 ) -> f64 {
1240 let total_validation_time: Duration = graph_results
1242 .values()
1243 .map(|r| r.stats.validation_duration)
1244 .sum();
1245
1246 let total_constraints: usize = graph_results
1247 .values()
1248 .map(|r| r.stats.constraints_evaluated)
1249 .sum();
1250
1251 if total_validation_time.as_millis() > 0 {
1252 let cpu_factor =
1254 (total_constraints as f64) / (total_validation_time.as_millis() as f64);
1255 (cpu_factor * 100.0).min(100.0) } else {
1257 0.0
1258 }
1259 }
1260
1261 fn calculate_network_bandwidth(
1263 &self,
1264 graph_results: &HashMap<GraphName, GraphValidationResult>,
1265 ) -> f64 {
1266 let total_bytes_transferred: usize = graph_results
1267 .values()
1268 .filter_map(|r| r.stats.remote_stats.as_ref())
1269 .map(|s| s.bytes_transferred)
1270 .sum();
1271
1272 let total_time: Duration = graph_results
1273 .values()
1274 .filter_map(|r| r.stats.remote_stats.as_ref())
1275 .map(|s| s.network_time)
1276 .sum();
1277
1278 if total_time.as_secs() > 0 {
1279 (total_bytes_transferred as f64) / (total_time.as_secs() as f64) / 1_000_000.0
1280 } else {
1282 0.0
1283 }
1284 }
1285
1286 fn calculate_disk_io_operations(
1288 &self,
1289 graph_results: &HashMap<GraphName, GraphValidationResult>,
1290 ) -> usize {
1291 graph_results
1293 .values()
1294 .map(|r| r.stats.triples_validated / 100) .sum()
1296 }
1297}
1298
1299#[derive(Debug, Clone)]
1301#[allow(dead_code)]
1302struct GraphValidationResult {
1303 graph_name: GraphName,
1305
1306 report: ValidationReport,
1308
1309 stats: GraphValidationStats,
1311}
1312
1313impl RemoteGraphClient {
1314 fn new(config: RemoteEndpointConfig) -> Result<Self> {
1316 let client = reqwest::Client::builder().timeout(config.timeout).build()?;
1317
1318 let semaphore = Arc::new(Semaphore::new(config.max_concurrent_requests));
1319
1320 Ok(Self {
1321 config,
1322 client,
1323 semaphore,
1324 })
1325 }
1326}
1327
1328impl Default for MultiGraphStats {
1329 fn default() -> Self {
1330 Self::new()
1331 }
1332}
1333
1334impl MultiGraphStats {
1335 pub fn new() -> Self {
1337 Self {
1338 total_graphs: 0,
1339 successful_graphs: 0,
1340 failed_graphs: 0,
1341 total_validation_time: Duration::from_millis(0),
1342 max_concurrent_graphs: 0,
1343 cross_graph_evaluations: 0,
1344 total_violations: 0,
1345 average_validation_time: Duration::from_millis(0),
1346 resource_utilization: ResourceUtilization {
1347 peak_memory_bytes: 0,
1348 average_cpu_percent: 0.0,
1349 network_bandwidth_mbps: 0.0,
1350 disk_io_operations: 0,
1351 },
1352 }
1353 }
1354}
1355
1356#[cfg(test)]
1357mod tests {
1358 use super::*;
1359 use oxirs_core::NamedNode;
1360
1361 #[tokio::test]
1362 async fn test_multi_graph_validation_basic() {
1363 let shapes = IndexMap::new();
1364 let config = MultiGraphValidationConfig::default();
1365
1366 let engine =
1367 MultiGraphValidationEngine::new(shapes, config).expect("construction should succeed");
1368
1369 let mut stores: HashMap<GraphName, Arc<dyn Store>> = HashMap::new();
1371 let store1 = Arc::new(ConcreteStore::new().expect("store creation should succeed"))
1372 as Arc<dyn Store>;
1373 let graph1 =
1374 GraphName::NamedNode(NamedNode::new("http://example.org/graph1").expect("valid IRI"));
1375 stores.insert(graph1, store1);
1376
1377 let result = engine
1378 .validate_multi_graph(&stores)
1379 .await
1380 .expect("async operation should succeed");
1381
1382 assert_eq!(result.overall_stats.total_graphs, 1);
1383 assert_eq!(result.overall_stats.successful_graphs, 1);
1384 }
1385}