1use crate::algebra::{Aggregate, Algebra, Expression, Term, Variable};
11use crate::query_analysis::{ValidationError, ValidationErrorType};
12use anyhow::Result;
13use std::collections::HashSet;
14
15pub struct QueryValidator {
17 config: ValidationConfig,
19 stats: ValidationStatistics,
21}
22
23#[derive(Debug, Clone)]
25pub struct ValidationConfig {
26 pub strict_compliance: bool,
28 pub performance_warnings: bool,
30 pub security_checks: bool,
32 pub max_complexity: usize,
34 pub max_triple_patterns: usize,
36 pub max_path_depth: usize,
38 pub warn_cartesian_products: bool,
40 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#[derive(Debug, Clone)]
61pub struct ValidationResult {
62 pub errors: Vec<ValidationError>,
64 pub warnings: Vec<ValidationWarning>,
66 pub complexity_score: usize,
68 pub is_valid: bool,
70}
71
72#[derive(Debug, Clone)]
74pub struct ValidationWarning {
75 pub warning_type: ValidationWarningType,
77 pub message: String,
79 pub location: String,
81 pub suggestion: Option<String>,
83 pub severity: u8,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum ValidationWarningType {
90 Performance,
92 BestPractice,
94 CartesianProduct,
96 Deprecated,
98 TypeInconsistency,
100 UnboundedQuery,
102 ComplexFilter,
104 MissingIndexHint,
106}
107
108#[derive(Debug, Clone, Default)]
110pub struct ValidationStatistics {
111 pub total_validated: usize,
113 pub total_errors: usize,
115 pub total_warnings: usize,
117}
118
119impl QueryValidator {
120 pub fn new() -> Self {
122 Self::with_config(ValidationConfig::default())
123 }
124
125 pub fn with_config(config: ValidationConfig) -> Self {
127 Self {
128 config,
129 stats: ValidationStatistics::default(),
130 }
131 }
132
133 pub fn validate(&mut self, algebra: &Algebra) -> Result<ValidationResult> {
135 let mut errors = Vec::new();
136 let mut warnings = Vec::new();
137
138 self.validate_variable_bindings(algebra, &mut errors)?;
140
141 self.validate_aggregates(algebra, &mut errors)?;
143
144 if self.config.warn_cartesian_products {
146 Self::check_cartesian_products(algebra, &mut warnings)?;
147 }
148
149 Self::validate_filters(algebra, &mut errors, &mut warnings)?;
151
152 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 if self.config.performance_warnings {
168 self.check_performance_issues(algebra, &mut warnings)?;
169 }
170
171 if self.config.security_checks {
173 self.check_security_issues(algebra, &mut warnings)?;
174 }
175
176 if self.config.check_type_consistency {
178 self.check_type_consistency(algebra, &mut warnings)?;
179 }
180
181 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 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 for group_cond in variables {
232 Self::validate_expression_variables(&group_cond.expr, &bound_vars, errors)?;
234 }
235
236 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 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 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 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 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 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 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 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 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 Self::check_cartesian_products(left, warnings)?;
452 Self::check_cartesian_products(right, warnings)?;
453 }
454
455 Ok(())
456 }
457
458 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 Self::validate_expression_variables(condition, &bound_vars, errors)?;
469
470 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 Self::validate_filters(pattern, errors, warnings)?;
487 }
488
489 Ok(())
490 }
491
492 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; complexity += Self::calculate_complexity(left)?;
503 complexity += Self::calculate_complexity(right)?;
504 }
505 Algebra::LeftJoin { left, right, .. } => {
506 complexity += 3; 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; complexity += by.len();
530 complexity += aggregates.len() * 2;
531 complexity += Self::calculate_complexity(pattern)?;
532 }
533 Algebra::OrderBy { pattern, .. } => {
534 complexity += 3; complexity += Self::calculate_complexity(pattern)?;
536 }
537 _ => {
538 complexity += 1;
539 }
540 }
541
542 Ok(complexity)
543 }
544
545 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 fn check_performance_issues(
565 &self,
566 algebra: &Algebra,
567 warnings: &mut Vec<ValidationWarning>,
568 ) -> Result<()> {
569 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 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 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 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 fn check_security_issues(
630 &self,
631 _algebra: &Algebra,
632 _warnings: &mut Vec<ValidationWarning>,
633 ) -> Result<()> {
634 Ok(())
640 }
641
642 fn check_type_consistency(
644 &self,
645 _algebra: &Algebra,
646 _warnings: &mut Vec<ValidationWarning>,
647 ) -> Result<()> {
648 Ok(())
654 }
655
656 pub fn statistics(&self) -> &ValidationStatistics {
658 &self.stats
659 }
660
661 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 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 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); 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 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 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}