Skip to main content

oxirs_shacl/validation/
multi_graph.rs

1//! Multi-graph SHACL validation for federated and distributed RDF datasets
2//!
3//! This module provides validation capabilities across multiple named graphs,
4//! supporting federated queries, graph-scoped constraints, and distributed validation.
5
6#[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/// Configuration for multi-graph validation
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct MultiGraphValidationConfig {
39    /// Base validation configuration
40    pub base_config: ValidationConfig,
41
42    /// Graph selection strategy
43    pub graph_strategy: GraphSelectionStrategy,
44
45    /// Maximum number of graphs to validate concurrently
46    pub max_concurrent_graphs: usize,
47
48    /// Timeout for individual graph validation
49    pub graph_timeout: Duration,
50
51    /// Whether to enable cross-graph constraint validation
52    pub enable_cross_graph_constraints: bool,
53
54    /// Whether to aggregate results across graphs
55    pub aggregate_results: bool,
56
57    /// Graph priority weights for resource allocation
58    pub graph_priorities: HashMap<GraphName, f64>,
59
60    /// Remote endpoint configurations for federated validation
61    pub remote_endpoints: HashMap<GraphName, RemoteEndpointConfig>,
62
63    /// Cache configuration for cross-graph queries
64    pub cross_graph_cache_size: usize,
65
66    /// Whether to enable partial validation on errors
67    pub enable_partial_validation: bool,
68}
69
70/// Strategy for selecting graphs to validate
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub enum GraphSelectionStrategy {
73    /// Validate all available graphs
74    All,
75    /// Validate only specified graphs
76    Explicit(HashSet<GraphName>),
77    /// Validate graphs matching a pattern
78    Pattern(String),
79    /// Validate based on shape targets
80    TargetBased,
81    /// Custom selection function
82    Custom,
83}
84
85/// Configuration for remote SPARQL endpoints
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct RemoteEndpointConfig {
88    /// Endpoint URL
89    pub url: String,
90
91    /// Authentication credentials
92    pub auth: Option<EndpointAuth>,
93
94    /// Request timeout
95    pub timeout: Duration,
96
97    /// Maximum concurrent requests
98    pub max_concurrent_requests: usize,
99
100    /// Retry configuration
101    pub retry_config: RetryConfig,
102
103    /// Whether the endpoint supports SHACL-SPARQL
104    pub supports_shacl_sparql: bool,
105}
106
107/// Authentication configuration for remote endpoints
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct EndpointAuth {
110    pub username: String,
111    pub password: String,
112    pub auth_type: AuthType,
113}
114
115/// Authentication types
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub enum AuthType {
118    Basic,
119    Bearer,
120    ApiKey,
121}
122
123/// Retry configuration for endpoint requests
124#[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/// Result of multi-graph validation
133#[derive(Debug, Clone)]
134pub struct MultiGraphValidationResult {
135    /// Individual graph validation reports
136    pub graph_reports: HashMap<GraphName, ValidationReport>,
137
138    /// Aggregated validation report
139    pub aggregated_report: Option<ValidationReport>,
140
141    /// Cross-graph constraint violations
142    pub cross_graph_violations: Vec<CrossGraphViolation>,
143
144    /// Validation statistics per graph
145    pub graph_stats: HashMap<GraphName, GraphValidationStats>,
146
147    /// Overall validation statistics
148    pub overall_stats: MultiGraphStats,
149
150    /// Validation errors per graph
151    pub graph_errors: HashMap<GraphName, Vec<String>>,
152
153    /// Federated query statistics
154    pub federation_stats: Option<FederationStats>,
155}
156
157/// Violation that spans multiple graphs
158#[derive(Debug, Clone)]
159pub struct CrossGraphViolation {
160    /// Primary focus node
161    pub focus_node: Term,
162
163    /// Constraint that was violated
164    pub constraint_component_id: crate::ConstraintComponentId,
165
166    /// Graphs involved in the violation
167    pub involved_graphs: HashSet<GraphName>,
168
169    /// Cross-graph evidence
170    pub evidence: CrossGraphEvidence,
171
172    /// Violation message
173    pub message: String,
174
175    /// Severity level
176    pub severity: crate::Severity,
177}
178
179/// Evidence for cross-graph violations
180#[derive(Debug, Clone)]
181pub struct CrossGraphEvidence {
182    /// Triples from different graphs that contribute to the violation
183    pub contributing_triples: HashMap<GraphName, Vec<Triple>>,
184
185    /// Query results from federated queries
186    pub federated_results: Vec<QueryResult>,
187}
188
189/// Query result for federated queries
190#[derive(Debug, Clone)]
191pub struct QueryResult {
192    /// Source graph or endpoint
193    pub source: GraphName,
194
195    /// SPARQL query that was executed
196    pub query: String,
197
198    /// Query results
199    pub bindings: Vec<HashMap<String, Term>>,
200
201    /// Query execution time
202    pub execution_time: Duration,
203}
204
205/// Validation statistics for individual graphs
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct GraphValidationStats {
208    /// Graph identifier
209    pub graph_name: GraphName,
210
211    /// Number of triples validated
212    pub triples_validated: usize,
213
214    /// Number of shapes applied
215    pub shapes_applied: usize,
216
217    /// Number of constraints evaluated
218    pub constraints_evaluated: usize,
219
220    /// Validation duration
221    pub validation_duration: Duration,
222
223    /// Memory used for validation
224    pub memory_used_bytes: usize,
225
226    /// Number of violations found
227    pub violations_found: usize,
228
229    /// Whether validation completed successfully
230    pub validation_successful: bool,
231
232    /// Remote endpoint statistics (if applicable)
233    pub remote_stats: Option<RemoteGraphStats>,
234}
235
236/// Statistics for remote graph validation
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct RemoteGraphStats {
239    /// Endpoint URL
240    pub endpoint_url: String,
241
242    /// Number of SPARQL queries executed
243    pub queries_executed: usize,
244
245    /// Total network time
246    pub network_time: Duration,
247
248    /// Number of retry attempts
249    pub retry_attempts: usize,
250
251    /// Data transfer statistics
252    pub bytes_transferred: usize,
253}
254
255/// Overall multi-graph validation statistics
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct MultiGraphStats {
258    /// Total number of graphs validated
259    pub total_graphs: usize,
260
261    /// Number of successfully validated graphs
262    pub successful_graphs: usize,
263
264    /// Number of failed graph validations
265    pub failed_graphs: usize,
266
267    /// Total validation time
268    pub total_validation_time: Duration,
269
270    /// Maximum concurrent graphs validated
271    pub max_concurrent_graphs: usize,
272
273    /// Cross-graph constraint evaluations
274    pub cross_graph_evaluations: usize,
275
276    /// Total violations across all graphs
277    pub total_violations: usize,
278
279    /// Average validation time per graph
280    pub average_validation_time: Duration,
281
282    /// Resource utilization metrics
283    pub resource_utilization: ResourceUtilization,
284}
285
286/// Resource utilization metrics
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct ResourceUtilization {
289    /// Peak memory usage
290    pub peak_memory_bytes: usize,
291
292    /// Average CPU utilization
293    pub average_cpu_percent: f64,
294
295    /// Network bandwidth used
296    pub network_bandwidth_mbps: f64,
297
298    /// Disk I/O operations
299    pub disk_io_operations: usize,
300}
301
302/// Statistics for federated query execution
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct FederationStats {
305    /// Number of federated queries executed
306    pub federated_queries: usize,
307
308    /// Total federation overhead
309    pub federation_overhead: Duration,
310
311    /// Network latency statistics
312    pub network_latency: NetworkLatencyStats,
313
314    /// Data locality statistics
315    pub data_locality: DataLocalityStats,
316}
317
318/// Network latency statistics
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct NetworkLatencyStats {
321    /// Minimum latency observed
322    pub min_latency: Duration,
323
324    /// Maximum latency observed
325    pub max_latency: Duration,
326
327    /// Average latency
328    pub average_latency: Duration,
329
330    /// 95th percentile latency
331    pub p95_latency: Duration,
332}
333
334/// Data locality statistics
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct DataLocalityStats {
337    /// Percentage of data accessed locally
338    pub local_data_percent: f64,
339
340    /// Percentage of data accessed remotely
341    pub remote_data_percent: f64,
342
343    /// Cache hit ratio for remote data
344    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
375/// Multi-graph SHACL validation engine
376pub struct MultiGraphValidationEngine {
377    /// Validation configuration
378    config: MultiGraphValidationConfig,
379
380    /// SHACL shapes for validation
381    shapes: Arc<RwLock<IndexMap<ShapeId, Shape>>>,
382
383    /// Graph-specific shape mappings
384    #[allow(dead_code)]
385    graph_shape_mappings: HashMap<GraphName, HashSet<ShapeId>>,
386
387    /// Cross-graph constraint cache
388    #[allow(dead_code)]
389    cross_graph_cache: Arc<RwLock<HashMap<String, CrossGraphCacheEntry>>>,
390
391    /// Remote endpoint clients
392    #[allow(dead_code)]
393    remote_clients: HashMap<GraphName, RemoteGraphClient>,
394
395    /// Validation statistics
396    #[allow(dead_code)]
397    stats: Arc<RwLock<MultiGraphStats>>,
398}
399
400/// Cache entry for cross-graph queries
401#[derive(Debug, Clone)]
402#[allow(dead_code)]
403struct CrossGraphCacheEntry {
404    /// Cached query results
405    results: Vec<QueryResult>,
406
407    /// Cache timestamp
408    timestamp: Instant,
409
410    /// Cache expiry time
411    expiry: Duration,
412}
413
414/// Client for remote graph validation
415#[allow(dead_code)]
416struct RemoteGraphClient {
417    /// Endpoint configuration
418    config: RemoteEndpointConfig,
419
420    /// HTTP client
421    client: reqwest::Client,
422
423    /// Semaphore for concurrent request limiting
424    semaphore: Arc<Semaphore>,
425}
426
427impl MultiGraphValidationEngine {
428    /// Create a new multi-graph validation engine
429    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        // Initialize remote clients
438        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    /// Validate multiple graphs with the configured strategy
455    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        // Select graphs to validate based on strategy
462        let selected_graphs = self.select_graphs_for_validation(stores).await?;
463
464        // Validate graphs concurrently
465        let graph_results = self
466            .validate_graphs_concurrent(&selected_graphs, stores)
467            .await?;
468
469        // Evaluate cross-graph constraints if enabled
470        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        // Aggregate results if requested
478        let aggregated_report = if self.config.aggregate_results {
479            Some(self.aggregate_validation_reports(&graph_results)?)
480        } else {
481            None
482        };
483
484        // Calculate statistics
485        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    /// Select graphs for validation based on the configured strategy
505    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                // Simple pattern matching on graph names
517                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                // Select graphs based on shape targets
532                self.select_target_based_graphs(stores).await
533            }
534            GraphSelectionStrategy::Custom => {
535                // For now, select all graphs (custom logic would be pluggable)
536                Ok(stores.keys().cloned().collect())
537            }
538        }
539    }
540
541    /// Select graphs based on shape targets
542    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                // Check if any targets in this shape have focus nodes in this graph
555                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    /// Validate multiple graphs concurrently
581    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        // Collect results
629        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                    // Continue with partial results if enabled
638                    if !self.config.enable_partial_validation {
639                        return Err(e);
640                    }
641                }
642            }
643        }
644
645        Ok(results)
646    }
647
648    /// Validate a single graph
649    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    /// Evaluate cross-graph constraints
683    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        // Clone the shapes data we need before any await calls
691        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            // Identify shapes with cross-graph constraints and clone them
703            shapes
704                .values()
705                .filter(|shape| self.has_cross_graph_constraints(shape))
706                .cloned()
707                .collect()
708        }; // shapes guard is dropped here
709
710        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        // Evaluate federated equality constraints
723        let equality_violations = self.evaluate_federated_equality_constraints(stores).await?;
724        violations.extend(equality_violations);
725
726        // Evaluate cross-graph disjointness constraints
727        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    /// Aggregate validation reports from multiple graphs
735    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    /// Calculate overall statistics
759    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    /// Check if a shape has cross-graph constraints
809    fn has_cross_graph_constraints(&self, shape: &Shape) -> bool {
810        // Check for SPARQL constraints that might reference other graphs
811        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    /// Evaluate cross-graph constraints for a specific shape
824    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        // Find all focus nodes for this shape across all graphs
833        let mut target_selector = TargetSelector::new();
834
835        for (graph_name, store) in stores {
836            let mut all_focus_nodes = Vec::new();
837
838            // Iterate over each target and collect focus nodes
839            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                // Evaluate cross-graph SPARQL constraints
852                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    /// Evaluate a cross-graph SPARQL constraint
880    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        // Construct federated SPARQL query
889        let query = self.build_federated_sparql_query(
890            &sparql_constraint.query,
891            focus_node,
892            stores.keys().collect(),
893        )?;
894
895        // Execute federated query
896        let query_results = self.execute_federated_query(&query, stores).await?;
897
898        // Check if constraint is violated
899        let is_violated = query_results.is_empty(); // Assuming constraint expects results
900
901        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    /// Evaluate federated equality constraints
925    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        // Check for entities that should have equal properties across graphs
932        for (graph_name, store) in stores {
933            // Simple equality check - can be expanded based on specific requirements
934            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                // Check if the same subject-predicate exists in other graphs with different values
940                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                        // Check for conflicting values
950                        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    /// Evaluate cross-graph disjointness constraints
994    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        // Check for entities that should be disjoint across graphs
1001        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                // Check if the same subject exists in other graphs (violating disjointness)
1006                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    /// Build a federated SPARQL query
1048    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        // Add prefixes
1057        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        // Replace focus node placeholder
1062        let query_with_focus = base_query.replace("$this", &format!("{focus_node}"));
1063
1064        // Add GRAPH clauses for each graph
1065        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    /// Execute a federated SPARQL query
1080    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 now, simulate query execution
1088        // In a real implementation, this would use a SPARQL engine
1089        for graph_name in stores.keys() {
1090            let result = QueryResult {
1091                source: graph_name.clone(),
1092                query: query.to_string(),
1093                bindings: Vec::new(), // Would be populated by actual SPARQL execution
1094                execution_time: Duration::from_millis(10),
1095            };
1096            results.push(result);
1097        }
1098
1099        Ok(results)
1100    }
1101
1102    /// Find contributing triples for a focus node across graphs
1103    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            // Find all triples where the focus node is the subject
1114            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    /// Collect graph validation errors
1133    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    /// Calculate federation statistics
1160    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    /// Estimate memory usage for a store and report
1203    fn estimate_memory_usage(store: &dyn Store, report: &ValidationReport) -> usize {
1204        // Rough estimation
1205        let store_size = store.len().unwrap_or(0) * 100; // ~100 bytes per quad estimate
1206        let report_size = report.violations().len() * 200; // ~200 bytes per violation
1207        store_size + report_size
1208    }
1209
1210    /// Count cross-graph evaluations
1211    fn count_cross_graph_evaluations(
1212        &self,
1213        graph_results: &HashMap<GraphName, GraphValidationResult>,
1214    ) -> usize {
1215        if self.config.enable_cross_graph_constraints {
1216            // Rough estimate: number of graphs squared (each graph potentially interacts with others)
1217            graph_results.len() * graph_results.len()
1218        } else {
1219            0
1220        }
1221    }
1222
1223    /// Calculate peak memory usage across all graph validations
1224    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    /// Calculate average CPU usage during validation
1236    fn calculate_average_cpu_usage(
1237        &self,
1238        graph_results: &HashMap<GraphName, GraphValidationResult>,
1239    ) -> f64 {
1240        // Estimate CPU usage based on validation time and complexity
1241        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            // Rough CPU estimation: more constraints and longer time = higher CPU usage
1253            let cpu_factor =
1254                (total_constraints as f64) / (total_validation_time.as_millis() as f64);
1255            (cpu_factor * 100.0).min(100.0) // Cap at 100%
1256        } else {
1257            0.0
1258        }
1259    }
1260
1261    /// Calculate network bandwidth usage
1262    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        // Convert to Mbps
1281        } else {
1282            0.0
1283        }
1284    }
1285
1286    /// Calculate total disk I/O operations
1287    fn calculate_disk_io_operations(
1288        &self,
1289        graph_results: &HashMap<GraphName, GraphValidationResult>,
1290    ) -> usize {
1291        // Estimate disk I/O based on data access patterns
1292        graph_results
1293            .values()
1294            .map(|r| r.stats.triples_validated / 100) // Rough estimate: 1 I/O per 100 triples
1295            .sum()
1296    }
1297}
1298
1299/// Result of validating a single graph
1300#[derive(Debug, Clone)]
1301#[allow(dead_code)]
1302struct GraphValidationResult {
1303    /// Graph identifier
1304    graph_name: GraphName,
1305
1306    /// Validation report
1307    report: ValidationReport,
1308
1309    /// Validation statistics
1310    stats: GraphValidationStats,
1311}
1312
1313impl RemoteGraphClient {
1314    /// Create a new remote graph client
1315    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    /// Create new multi-graph statistics
1336    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        // Create test stores
1370        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}