Skip to main content

oxirs_arq/
query_validator.rs

1//! Comprehensive SPARQL Query Validation
2//!
3//! This module provides extensive validation for SPARQL queries, including:
4//! - Syntax validation
5//! - Semantic validation (variable bindings, type consistency)
6//! - Best practices and performance warnings
7//! - Security validation (injection risks, resource limits)
8//! - SPARQL 1.1/1.2 compliance checking
9
10use crate::algebra::{Aggregate, Algebra, Expression, Term, Variable};
11use crate::query_analysis::{ValidationError, ValidationErrorType};
12use anyhow::Result;
13use std::collections::HashSet;
14
15/// Comprehensive query validator
16pub struct QueryValidator {
17    /// Configuration
18    config: ValidationConfig,
19    /// Statistics for validation
20    stats: ValidationStatistics,
21}
22
23/// Validation configuration
24#[derive(Debug, Clone)]
25pub struct ValidationConfig {
26    /// Enable strict SPARQL compliance checking
27    pub strict_compliance: bool,
28    /// Enable performance warnings
29    pub performance_warnings: bool,
30    /// Enable security checks
31    pub security_checks: bool,
32    /// Maximum query complexity allowed
33    pub max_complexity: usize,
34    /// Maximum number of triple patterns
35    pub max_triple_patterns: usize,
36    /// Maximum recursion depth for property paths
37    pub max_path_depth: usize,
38    /// Warn about cartesian products
39    pub warn_cartesian_products: bool,
40    /// Check for type consistency
41    pub check_type_consistency: bool,
42}
43
44impl Default for ValidationConfig {
45    fn default() -> Self {
46        Self {
47            strict_compliance: true,
48            performance_warnings: true,
49            security_checks: true,
50            max_complexity: 1000,
51            max_triple_patterns: 100,
52            max_path_depth: 10,
53            warn_cartesian_products: true,
54            check_type_consistency: true,
55        }
56    }
57}
58
59/// Validation result
60#[derive(Debug, Clone)]
61pub struct ValidationResult {
62    /// List of validation errors (must be fixed)
63    pub errors: Vec<ValidationError>,
64    /// List of warnings (should be reviewed)
65    pub warnings: Vec<ValidationWarning>,
66    /// Query complexity score
67    pub complexity_score: usize,
68    /// Validation passed
69    pub is_valid: bool,
70}
71
72/// Validation warning
73#[derive(Debug, Clone)]
74pub struct ValidationWarning {
75    /// Warning type
76    pub warning_type: ValidationWarningType,
77    /// Warning message
78    pub message: String,
79    /// Location where warning occurred
80    pub location: String,
81    /// Suggestion for improvement
82    pub suggestion: Option<String>,
83    /// Severity level (1-10)
84    pub severity: u8,
85}
86
87/// Validation warning types
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum ValidationWarningType {
90    /// Performance concern
91    Performance,
92    /// Best practices violation
93    BestPractice,
94    /// Potential cartesian product
95    CartesianProduct,
96    /// Deprecated feature usage
97    Deprecated,
98    /// Type inconsistency (non-fatal)
99    TypeInconsistency,
100    /// Unbounded query
101    UnboundedQuery,
102    /// Complex filter expression
103    ComplexFilter,
104    /// Missing index hint
105    MissingIndexHint,
106}
107
108/// Validation statistics
109#[derive(Debug, Clone, Default)]
110pub struct ValidationStatistics {
111    /// Total queries validated
112    pub total_validated: usize,
113    /// Total errors found
114    pub total_errors: usize,
115    /// Total warnings found
116    pub total_warnings: usize,
117}
118
119impl QueryValidator {
120    /// Create a new query validator
121    pub fn new() -> Self {
122        Self::with_config(ValidationConfig::default())
123    }
124
125    /// Create with custom configuration
126    pub fn with_config(config: ValidationConfig) -> Self {
127        Self {
128            config,
129            stats: ValidationStatistics::default(),
130        }
131    }
132
133    /// Validate a SPARQL query algebra
134    pub fn validate(&mut self, algebra: &Algebra) -> Result<ValidationResult> {
135        let mut errors = Vec::new();
136        let mut warnings = Vec::new();
137
138        // 1. Validate variable bindings
139        self.validate_variable_bindings(algebra, &mut errors)?;
140
141        // 2. Validate aggregates
142        self.validate_aggregates(algebra, &mut errors)?;
143
144        // 3. Check for cartesian products
145        if self.config.warn_cartesian_products {
146            Self::check_cartesian_products(algebra, &mut warnings)?;
147        }
148
149        // 4. Validate filters
150        Self::validate_filters(algebra, &mut errors, &mut warnings)?;
151
152        // 5. Check query complexity
153        let complexity_score = Self::calculate_complexity(algebra)?;
154        if complexity_score > self.config.max_complexity {
155            errors.push(ValidationError {
156                error_type: ValidationErrorType::SemanticInconsistency,
157                message: format!(
158                    "Query complexity ({}) exceeds maximum allowed ({})",
159                    complexity_score, self.config.max_complexity
160                ),
161                location: "Overall query".to_string(),
162                suggestion: Some("Consider breaking the query into smaller parts".to_string()),
163            });
164        }
165
166        // 6. Performance warnings
167        if self.config.performance_warnings {
168            self.check_performance_issues(algebra, &mut warnings)?;
169        }
170
171        // 7. Security checks
172        if self.config.security_checks {
173            self.check_security_issues(algebra, &mut warnings)?;
174        }
175
176        // 8. Type consistency checks
177        if self.config.check_type_consistency {
178            self.check_type_consistency(algebra, &mut warnings)?;
179        }
180
181        // Update statistics
182        self.stats.total_validated += 1;
183        self.stats.total_errors += errors.len();
184        self.stats.total_warnings += warnings.len();
185
186        let is_valid = errors.is_empty();
187
188        Ok(ValidationResult {
189            errors,
190            warnings,
191            complexity_score,
192            is_valid,
193        })
194    }
195
196    /// Validate variable bindings
197    fn validate_variable_bindings(
198        &self,
199        algebra: &Algebra,
200        errors: &mut Vec<ValidationError>,
201    ) -> Result<()> {
202        match algebra {
203            Algebra::Project { variables, pattern } => {
204                let bound_vars = Self::collect_bound_variables(pattern)?;
205
206                for var in variables {
207                    if !bound_vars.contains(var) {
208                        errors.push(ValidationError {
209                            error_type: ValidationErrorType::UnboundVariable,
210                            message: format!(
211                                "Variable ?{} in SELECT clause is not bound in the query pattern",
212                                var.as_str()
213                            ),
214                            location: "SELECT clause".to_string(),
215                            suggestion: Some(format!(
216                                "Add a triple pattern that binds ?{}",
217                                var.as_str()
218                            )),
219                        });
220                    }
221                }
222            }
223            Algebra::Group {
224                pattern,
225                variables,
226                aggregates,
227            } => {
228                let bound_vars = Self::collect_bound_variables(pattern)?;
229
230                // Validate GROUP BY variables
231                for group_cond in variables {
232                    // Validate the grouping expression
233                    Self::validate_expression_variables(&group_cond.expr, &bound_vars, errors)?;
234                }
235
236                // Validate aggregate expressions
237                for (_, agg) in aggregates {
238                    self.validate_aggregate_expression(agg, &bound_vars, errors)?;
239                }
240            }
241            Algebra::OrderBy { pattern, .. } => {
242                self.validate_variable_bindings(pattern, errors)?;
243            }
244            Algebra::Join { left, right } | Algebra::LeftJoin { left, right, .. } => {
245                self.validate_variable_bindings(left, errors)?;
246                self.validate_variable_bindings(right, errors)?;
247            }
248            Algebra::Union { left, right } => {
249                self.validate_variable_bindings(left, errors)?;
250                self.validate_variable_bindings(right, errors)?;
251            }
252            Algebra::Filter { pattern, .. } => {
253                self.validate_variable_bindings(pattern, errors)?;
254            }
255            _ => {}
256        }
257
258        Ok(())
259    }
260
261    /// Collect all bound variables in algebra
262    fn collect_bound_variables(algebra: &Algebra) -> Result<HashSet<Variable>> {
263        let mut vars = HashSet::new();
264
265        match algebra {
266            Algebra::Bgp(patterns) => {
267                for pattern in patterns {
268                    if let Term::Variable(v) = &pattern.subject {
269                        vars.insert(v.clone());
270                    }
271                    if let Term::Variable(v) = &pattern.predicate {
272                        vars.insert(v.clone());
273                    }
274                    if let Term::Variable(v) = &pattern.object {
275                        vars.insert(v.clone());
276                    }
277                }
278            }
279            Algebra::Join { left, right } => {
280                vars.extend(Self::collect_bound_variables(left)?);
281                vars.extend(Self::collect_bound_variables(right)?);
282            }
283            Algebra::LeftJoin { left, right, .. } => {
284                vars.extend(Self::collect_bound_variables(left)?);
285                vars.extend(Self::collect_bound_variables(right)?);
286            }
287            Algebra::Union { left, right } => {
288                vars.extend(Self::collect_bound_variables(left)?);
289                vars.extend(Self::collect_bound_variables(right)?);
290            }
291            Algebra::Filter { pattern, .. } => {
292                vars.extend(Self::collect_bound_variables(pattern)?);
293            }
294            Algebra::Project { pattern, .. } => {
295                vars.extend(Self::collect_bound_variables(pattern)?);
296            }
297            Algebra::Group { pattern, .. } => {
298                vars.extend(Self::collect_bound_variables(pattern)?);
299            }
300            _ => {}
301        }
302
303        Ok(vars)
304    }
305
306    /// Validate aggregates
307    fn validate_aggregates(
308        &self,
309        algebra: &Algebra,
310        errors: &mut Vec<ValidationError>,
311    ) -> Result<()> {
312        if let Algebra::Group {
313            variables: by,
314            aggregates,
315            pattern,
316        } = algebra
317        {
318            let bound_vars = Self::collect_bound_variables(pattern)?;
319
320            for (result_var, agg) in aggregates {
321                // Check if aggregate uses appropriate variables
322                match agg {
323                    Aggregate::Count { expr, .. } => {
324                        if let Some(expr) = expr {
325                            Self::validate_expression_variables(expr, &bound_vars, errors)?;
326                        }
327                    }
328                    Aggregate::Sum { expr, .. }
329                    | Aggregate::Avg { expr, .. }
330                    | Aggregate::Min { expr, .. }
331                    | Aggregate::Max { expr, .. } => {
332                        Self::validate_expression_variables(expr, &bound_vars, errors)?;
333                    }
334                    Aggregate::GroupConcat { expr, .. } => {
335                        Self::validate_expression_variables(expr, &bound_vars, errors)?;
336                    }
337                    Aggregate::Sample { expr, .. } => {
338                        Self::validate_expression_variables(expr, &bound_vars, errors)?;
339                    }
340                }
341
342                // Check if result variable conflicts with group by expression aliases
343                for group_cond in by {
344                    if let Some(alias) = &group_cond.alias {
345                        if alias == result_var {
346                            errors.push(ValidationError {
347                                error_type: ValidationErrorType::InvalidAggregate,
348                                message: format!(
349                                    "Aggregate result variable ?{} conflicts with GROUP BY alias",
350                                    result_var.as_str()
351                                ),
352                                location: "GROUP BY clause".to_string(),
353                                suggestion: Some(
354                                    "Use a different variable name for the aggregate result"
355                                        .to_string(),
356                                ),
357                            });
358                        }
359                    }
360                }
361            }
362        }
363
364        Ok(())
365    }
366
367    /// Validate aggregate expression
368    fn validate_aggregate_expression(
369        &self,
370        agg: &Aggregate,
371        bound_vars: &HashSet<Variable>,
372        errors: &mut Vec<ValidationError>,
373    ) -> Result<()> {
374        match agg {
375            Aggregate::Count { expr, .. } => {
376                if let Some(expr) = expr {
377                    Self::validate_expression_variables(expr, bound_vars, errors)?;
378                }
379            }
380            Aggregate::Sum { expr, .. }
381            | Aggregate::Avg { expr, .. }
382            | Aggregate::Min { expr, .. }
383            | Aggregate::Max { expr, .. } => {
384                Self::validate_expression_variables(expr, bound_vars, errors)?;
385            }
386            Aggregate::GroupConcat { expr, .. } => {
387                Self::validate_expression_variables(expr, bound_vars, errors)?;
388            }
389            Aggregate::Sample { expr, .. } => {
390                Self::validate_expression_variables(expr, bound_vars, errors)?;
391            }
392        }
393        Ok(())
394    }
395
396    /// Validate expression variables
397    fn validate_expression_variables(
398        expr: &Expression,
399        bound_vars: &HashSet<Variable>,
400        errors: &mut Vec<ValidationError>,
401    ) -> Result<()> {
402        match expr {
403            Expression::Variable(v) if !bound_vars.contains(v) => {
404                errors.push(ValidationError {
405                    error_type: ValidationErrorType::UnboundVariable,
406                    message: format!("Variable ?{} used in expression is not bound", v.as_str()),
407                    location: "Expression".to_string(),
408                    suggestion: Some(format!("Bind ?{} in a triple pattern", v.as_str())),
409                });
410            }
411            Expression::Binary { left, right, op: _ } => {
412                Self::validate_expression_variables(left, bound_vars, errors)?;
413                Self::validate_expression_variables(right, bound_vars, errors)?;
414            }
415            Expression::Unary { operand, op: _ } => {
416                Self::validate_expression_variables(operand, bound_vars, errors)?;
417            }
418            Expression::Function { args, .. } => {
419                for arg in args {
420                    Self::validate_expression_variables(arg, bound_vars, errors)?;
421                }
422            }
423            _ => {}
424        }
425        Ok(())
426    }
427
428    /// Check for cartesian products
429    fn check_cartesian_products(
430        algebra: &Algebra,
431        warnings: &mut Vec<ValidationWarning>,
432    ) -> Result<()> {
433        if let Algebra::Join { left, right } = algebra {
434            let left_vars = Self::collect_bound_variables(left)?;
435            let right_vars = Self::collect_bound_variables(right)?;
436
437            // Check if joins share any variables
438            let shared_vars: HashSet<_> = left_vars.intersection(&right_vars).collect();
439
440            if shared_vars.is_empty() {
441                warnings.push(ValidationWarning {
442                    warning_type: ValidationWarningType::CartesianProduct,
443                    message: "Detected potential cartesian product: join without shared variables".to_string(),
444                    location: "JOIN".to_string(),
445                    suggestion: Some("Ensure the join patterns share at least one variable to avoid expensive cartesian products".to_string()),
446                    severity: 8,
447                });
448            }
449
450            // Recursively check nested joins
451            Self::check_cartesian_products(left, warnings)?;
452            Self::check_cartesian_products(right, warnings)?;
453        }
454
455        Ok(())
456    }
457
458    /// Validate filters
459    fn validate_filters(
460        algebra: &Algebra,
461        errors: &mut Vec<ValidationError>,
462        warnings: &mut Vec<ValidationWarning>,
463    ) -> Result<()> {
464        if let Algebra::Filter { pattern, condition } = algebra {
465            let bound_vars = Self::collect_bound_variables(pattern)?;
466
467            // Validate filter expression uses bound variables
468            Self::validate_expression_variables(condition, &bound_vars, errors)?;
469
470            // Check filter complexity
471            let complexity = Self::calculate_expression_complexity(condition);
472            if complexity > 10 {
473                warnings.push(ValidationWarning {
474                    warning_type: ValidationWarningType::ComplexFilter,
475                    message: format!("Filter expression has high complexity ({})", complexity),
476                    location: "FILTER clause".to_string(),
477                    suggestion: Some(
478                        "Consider simplifying the filter or breaking it into multiple filters"
479                            .to_string(),
480                    ),
481                    severity: 5,
482                });
483            }
484
485            // Recursively validate nested patterns
486            Self::validate_filters(pattern, errors, warnings)?;
487        }
488
489        Ok(())
490    }
491
492    /// Calculate query complexity
493    pub fn calculate_complexity(algebra: &Algebra) -> Result<usize> {
494        let mut complexity = 0;
495
496        match algebra {
497            Algebra::Bgp(patterns) => {
498                complexity += patterns.len();
499            }
500            Algebra::Join { left, right } => {
501                complexity += 2; // Join operation cost
502                complexity += Self::calculate_complexity(left)?;
503                complexity += Self::calculate_complexity(right)?;
504            }
505            Algebra::LeftJoin { left, right, .. } => {
506                complexity += 3; // Optional join is more expensive
507                complexity += Self::calculate_complexity(left)?;
508                complexity += Self::calculate_complexity(right)?;
509            }
510            Algebra::Union { left, right } => {
511                complexity += 2;
512                complexity += Self::calculate_complexity(left)?;
513                complexity += Self::calculate_complexity(right)?;
514            }
515            Algebra::Filter {
516                pattern,
517                condition: expr,
518            } => {
519                complexity += 1;
520                complexity += Self::calculate_expression_complexity(expr);
521                complexity += Self::calculate_complexity(pattern)?;
522            }
523            Algebra::Group {
524                pattern,
525                variables: by,
526                aggregates,
527            } => {
528                complexity += 5; // Grouping is expensive
529                complexity += by.len();
530                complexity += aggregates.len() * 2;
531                complexity += Self::calculate_complexity(pattern)?;
532            }
533            Algebra::OrderBy { pattern, .. } => {
534                complexity += 3; // Sorting is expensive
535                complexity += Self::calculate_complexity(pattern)?;
536            }
537            _ => {
538                complexity += 1;
539            }
540        }
541
542        Ok(complexity)
543    }
544
545    /// Calculate expression complexity
546    fn calculate_expression_complexity(expr: &Expression) -> usize {
547        match expr {
548            Expression::Binary { left, right, .. } => {
549                1 + Self::calculate_expression_complexity(left)
550                    + Self::calculate_expression_complexity(right)
551            }
552            Expression::Unary { operand, .. } => 1 + Self::calculate_expression_complexity(operand),
553            Expression::Function { args, .. } => {
554                2 + args
555                    .iter()
556                    .map(Self::calculate_expression_complexity)
557                    .sum::<usize>()
558            }
559            _ => 1,
560        }
561    }
562
563    /// Check performance issues
564    fn check_performance_issues(
565        &self,
566        algebra: &Algebra,
567        warnings: &mut Vec<ValidationWarning>,
568    ) -> Result<()> {
569        // Check for unbounded queries
570        if Self::is_unbounded(algebra)? {
571            warnings.push(ValidationWarning {
572                warning_type: ValidationWarningType::UnboundedQuery,
573                message: "Query has no LIMIT clause and may return very large result sets"
574                    .to_string(),
575                location: "Overall query".to_string(),
576                suggestion: Some(
577                    "Add a LIMIT clause to prevent excessive memory usage".to_string(),
578                ),
579                severity: 6,
580            });
581        }
582
583        // Check triple pattern count
584        let pattern_count = Self::count_triple_patterns(algebra)?;
585        if pattern_count > self.config.max_triple_patterns {
586            warnings.push(ValidationWarning {
587                warning_type: ValidationWarningType::Performance,
588                message: format!(
589                    "Query has {} triple patterns (max recommended: {})",
590                    pattern_count, self.config.max_triple_patterns
591                ),
592                location: "WHERE clause".to_string(),
593                suggestion: Some("Consider breaking the query into smaller subqueries".to_string()),
594                severity: 7,
595            });
596        }
597
598        Ok(())
599    }
600
601    /// Check if query is unbounded
602    fn is_unbounded(algebra: &Algebra) -> Result<bool> {
603        match algebra {
604            Algebra::Slice { .. } => Ok(false),
605            Algebra::Project { pattern, .. } => Self::is_unbounded(pattern),
606            Algebra::OrderBy { pattern, .. } => Self::is_unbounded(pattern),
607            Algebra::Group { pattern, .. } => Self::is_unbounded(pattern),
608            Algebra::Filter { pattern, .. } => Self::is_unbounded(pattern),
609            _ => Ok(true),
610        }
611    }
612
613    /// Count triple patterns
614    fn count_triple_patterns(algebra: &Algebra) -> Result<usize> {
615        match algebra {
616            Algebra::Bgp(patterns) => Ok(patterns.len()),
617            Algebra::Join { left, right }
618            | Algebra::LeftJoin { left, right, .. }
619            | Algebra::Union { left, right } => {
620                Ok(Self::count_triple_patterns(left)? + Self::count_triple_patterns(right)?)
621            }
622            Algebra::Filter { pattern, .. } => Self::count_triple_patterns(pattern),
623            Algebra::Project { pattern, .. } => Self::count_triple_patterns(pattern),
624            _ => Ok(0),
625        }
626    }
627
628    /// Check security issues
629    fn check_security_issues(
630        &self,
631        _algebra: &Algebra,
632        _warnings: &mut Vec<ValidationWarning>,
633    ) -> Result<()> {
634        // Placeholder for security checks
635        // Could include checks for:
636        // - Suspicious patterns that might indicate injection
637        // - Queries that access sensitive predicates
638        // - Potential DoS patterns
639        Ok(())
640    }
641
642    /// Check type consistency
643    fn check_type_consistency(
644        &self,
645        _algebra: &Algebra,
646        _warnings: &mut Vec<ValidationWarning>,
647    ) -> Result<()> {
648        // Placeholder for type consistency checks
649        // Could include checks for:
650        // - Numeric operations on non-numeric variables
651        // - String operations on non-string variables
652        // - Comparison of incompatible types
653        Ok(())
654    }
655
656    /// Get validation statistics
657    pub fn statistics(&self) -> &ValidationStatistics {
658        &self.stats
659    }
660
661    /// Reset statistics
662    pub fn reset_statistics(&mut self) {
663        self.stats = ValidationStatistics::default();
664    }
665}
666
667impl Default for QueryValidator {
668    fn default() -> Self {
669        Self::new()
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use crate::algebra::{Literal, Term, TriplePattern};
677    use oxirs_core::model::NamedNode;
678
679    fn create_term(s: &str) -> Term {
680        Term::Iri(NamedNode::new(s).unwrap())
681    }
682
683    fn create_var(name: &str) -> Variable {
684        Variable::new(name).unwrap()
685    }
686
687    #[test]
688    fn test_validator_creation() {
689        let validator = QueryValidator::new();
690        assert!(validator.config.strict_compliance);
691    }
692
693    #[test]
694    fn test_unbound_variable_detection() {
695        let mut validator = QueryValidator::new();
696
697        // Create a query with unbound variable in projection
698        let pattern = Algebra::Bgp(vec![TriplePattern {
699            subject: Term::Variable(create_var("s")),
700            predicate: create_term("http://example.org/p"),
701            object: Term::Variable(create_var("o")),
702        }]);
703
704        let query = Algebra::Project {
705            variables: vec![create_var("s"), create_var("unbound")],
706            pattern: Box::new(pattern),
707        };
708
709        let result = validator.validate(&query).unwrap();
710        assert!(!result.is_valid);
711        assert_eq!(result.errors.len(), 1);
712        assert_eq!(
713            result.errors[0].error_type,
714            ValidationErrorType::UnboundVariable
715        );
716    }
717
718    #[test]
719    fn test_valid_query() {
720        let mut validator = QueryValidator::new();
721
722        let pattern = Algebra::Bgp(vec![TriplePattern {
723            subject: Term::Variable(create_var("s")),
724            predicate: create_term("http://example.org/p"),
725            object: Term::Variable(create_var("o")),
726        }]);
727
728        let query = Algebra::Project {
729            variables: vec![create_var("s"), create_var("o")],
730            pattern: Box::new(pattern),
731        };
732
733        let result = validator.validate(&query).unwrap();
734        assert!(result.is_valid);
735        assert!(result.errors.is_empty());
736    }
737
738    #[test]
739    fn test_cartesian_product_warning() {
740        let mut validator = QueryValidator::new();
741
742        // Create two BGPs with no shared variables (cartesian product)
743        let left = Algebra::Bgp(vec![TriplePattern {
744            subject: Term::Variable(create_var("s1")),
745            predicate: create_term("http://example.org/p1"),
746            object: Term::Variable(create_var("o1")),
747        }]);
748
749        let right = Algebra::Bgp(vec![TriplePattern {
750            subject: Term::Variable(create_var("s2")),
751            predicate: create_term("http://example.org/p2"),
752            object: Term::Variable(create_var("o2")),
753        }]);
754
755        let query = Algebra::Join {
756            left: Box::new(left),
757            right: Box::new(right),
758        };
759
760        let result = validator.validate(&query).unwrap();
761        assert!(result.is_valid); // No errors, just warnings
762        assert!(!result.warnings.is_empty());
763        assert_eq!(
764            result.warnings[0].warning_type,
765            ValidationWarningType::CartesianProduct
766        );
767    }
768
769    #[test]
770    fn test_complexity_calculation() {
771        let _validator = QueryValidator::new();
772
773        // Simple BGP
774        let simple = Algebra::Bgp(vec![
775            TriplePattern {
776                subject: Term::Variable(create_var("s")),
777                predicate: create_term("http://example.org/p"),
778                object: Term::Variable(create_var("o")),
779            },
780            TriplePattern {
781                subject: Term::Variable(create_var("o")),
782                predicate: create_term("http://example.org/p2"),
783                object: Term::Literal(Literal::string("test")),
784            },
785        ]);
786
787        let complexity = QueryValidator::calculate_complexity(&simple).unwrap();
788        assert_eq!(complexity, 2);
789    }
790
791    #[test]
792    fn test_validation_statistics() {
793        let mut validator = QueryValidator::new();
794
795        let pattern = Algebra::Bgp(vec![TriplePattern {
796            subject: Term::Variable(create_var("s")),
797            predicate: create_term("http://example.org/p"),
798            object: Term::Variable(create_var("o")),
799        }]);
800
801        let query = Algebra::Project {
802            variables: vec![create_var("s")],
803            pattern: Box::new(pattern),
804        };
805
806        validator.validate(&query).unwrap();
807
808        let stats = validator.statistics();
809        assert_eq!(stats.total_validated, 1);
810    }
811
812    #[test]
813    fn test_custom_config() {
814        let config = ValidationConfig {
815            max_complexity: 50,
816            warn_cartesian_products: false,
817            ..Default::default()
818        };
819
820        let validator = QueryValidator::with_config(config);
821        assert_eq!(validator.config.max_complexity, 50);
822        assert!(!validator.config.warn_cartesian_products);
823    }
824
825    #[test]
826    fn test_aggregate_validation() {
827        let mut validator = QueryValidator::new();
828
829        let pattern = Algebra::Bgp(vec![TriplePattern {
830            subject: Term::Variable(create_var("s")),
831            predicate: create_term("http://example.org/p"),
832            object: Term::Variable(create_var("o")),
833        }]);
834
835        let query = Algebra::Group {
836            pattern: Box::new(pattern),
837            variables: vec![],
838            aggregates: vec![(
839                create_var("count"),
840                Aggregate::Count {
841                    distinct: false,
842                    expr: None,
843                },
844            )],
845        };
846
847        let result = validator.validate(&query).unwrap();
848        assert!(result.is_valid);
849    }
850
851    #[test]
852    fn test_performance_warnings() {
853        let mut validator = QueryValidator::with_config(ValidationConfig {
854            max_triple_patterns: 2,
855            ..Default::default()
856        });
857
858        // Create a query with 3 triple patterns
859        let pattern = Algebra::Bgp(vec![
860            TriplePattern {
861                subject: Term::Variable(create_var("s")),
862                predicate: create_term("http://example.org/p1"),
863                object: Term::Variable(create_var("o")),
864            },
865            TriplePattern {
866                subject: Term::Variable(create_var("o")),
867                predicate: create_term("http://example.org/p2"),
868                object: Term::Variable(create_var("o2")),
869            },
870            TriplePattern {
871                subject: Term::Variable(create_var("o2")),
872                predicate: create_term("http://example.org/p3"),
873                object: Term::Literal(Literal::string("test")),
874            },
875        ]);
876
877        let result = validator.validate(&pattern).unwrap();
878        assert!(result.is_valid);
879        assert!(!result.warnings.is_empty());
880    }
881}