Skip to main content

sqry_core/query/
validator.rs

1//! Semantic validation for query ASTs
2//!
3//! This module provides validation for parsed query ASTs, checking:
4//! - Field names against the field registry
5//! - Operator compatibility with field types
6//! - Value type matching
7//! - Regex pattern validity
8//! - Contradiction detection
9//!
10//! The validator provides helpful error messages with suggestions for typos
11//! and clear explanations of validation failures.
12//!
13//! # Regex Support (FT-C.1)
14//!
15//! The validator supports both standard regex patterns (via the `regex` crate)
16//! and advanced patterns with lookaround assertions (via the `fancy-regex` crate).
17//!
18//! Lookaround support includes:
19//! - Positive lookahead: `(?=...)`
20//! - Negative lookahead: `(?!...)`
21//! - Positive lookbehind: `(?<=...)`
22//! - Negative lookbehind: `(?<!...)`
23//!
24//! The validator automatically detects lookaround patterns and uses the
25//! appropriate regex engine.
26
27use regex::Regex;
28use std::collections::HashMap;
29
30use super::error::ValidationError;
31use super::registry::FieldRegistry;
32use super::types::{Condition, Expr, Field, FieldDescriptor, FieldType, Operator, Span, Value};
33
34const SAFE_FUZZY_FIELDS: &[&str] = &[
35    "kind",
36    "path",
37    "lang",
38    "repo",
39    "parent",
40    "scope.type",
41    "scope.name",
42    "scope.parent",
43    "scope.ancestor",
44    "callers",
45    "callees",
46    "imports",
47    "exports",
48    "returns",
49    "references",
50    // Phase A C indirect-call precision (U18.1) — fuzzy-correct common typos
51    // like `address_take:`, `resolve_via:`, `callsite_promiscous:` so users on
52    // the `mcp__sqry__semantic_search` surface get the same suggestion
53    // experience the planner-surface (`sqry_query`) already provides via U14.
54    "address_taken",
55    "resolved_via",
56    "callsite_promiscuous",
57];
58
59/// Semantic validator for query ASTs
60///
61/// Validates queries against a field registry to ensure:
62/// - All field names exist
63/// - Operators are compatible with field types
64/// - Values match expected types
65/// - Regex patterns are valid
66///
67/// # Example
68///
69/// ```
70/// use sqry_core::query::registry::FieldRegistry;
71/// use sqry_core::query::validator::Validator;
72/// use sqry_core::query::types::{Expr, Condition, Field, Operator, Value, Span};
73///
74/// let registry = FieldRegistry::with_core_fields();
75/// let validator = Validator::new(registry);
76///
77/// let condition = Expr::Condition(Condition {
78///     field: Field::new("kind"),
79///     operator: Operator::Equal,
80///     value: Value::String("function".to_string()),
81///     span: Span::default(),
82/// });
83///
84/// assert!(validator.validate(&condition).is_ok());
85/// ```
86/// Configuration for validation behaviour.
87#[derive(Clone, Copy, Debug)]
88pub struct ValidationOptions {
89    /// Enable fuzzy field correction (opt-in).
90    pub fuzzy_fields: bool,
91    /// Maximum edit distance allowed for fuzzy field correction.
92    pub fuzzy_field_distance: usize,
93}
94
95impl Default for ValidationOptions {
96    fn default() -> Self {
97        Self {
98            fuzzy_fields: false,
99            fuzzy_field_distance: 2,
100        }
101    }
102}
103
104/// Validator for query expressions and field/value semantics.
105pub struct Validator {
106    registry: FieldRegistry,
107    options: ValidationOptions,
108}
109
110impl Validator {
111    /// Create a new validator with the given field registry
112    #[must_use]
113    pub fn new(registry: FieldRegistry) -> Self {
114        Self {
115            registry,
116            options: ValidationOptions::default(),
117        }
118    }
119
120    /// Create a new validator with options
121    #[must_use]
122    pub fn with_options(registry: FieldRegistry, options: ValidationOptions) -> Self {
123        Self { registry, options }
124    }
125
126    /// Validate a query expression
127    ///
128    /// Returns `Ok(())` if the expression is valid, or a `ValidationError` if validation fails.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`ValidationError`] when the expression uses unknown fields, invalid operators, or mismatched value types.
133    pub fn validate(&self, expr: &Expr) -> Result<(), ValidationError> {
134        self.validate_node_with_depth(expr, 0)
135    }
136
137    /// Normalize field names using fuzzy options (when enabled) and return a new expression tree.
138    ///
139    /// # Errors
140    ///
141    /// Returns [`ValidationError`] when normalization fails or fuzzy correction is unsafe.
142    pub fn normalize_expr(&self, expr: &Expr) -> Result<Expr, ValidationError> {
143        match expr {
144            Expr::And(operands) => Ok(Expr::And(self.normalize_operands(operands)?)),
145            Expr::Or(operands) => Ok(Expr::Or(self.normalize_operands(operands)?)),
146            Expr::Not(op) => Ok(Expr::Not(Box::new(self.normalize_expr(op)?))),
147            Expr::Condition(cond) => Ok(Expr::Condition(self.normalize_condition(cond)?)),
148            Expr::Join(join) => Ok(Expr::Join(crate::query::types::JoinExpr {
149                left: Box::new(self.normalize_expr(&join.left)?),
150                edge: join.edge.clone(),
151                right: Box::new(self.normalize_expr(&join.right)?),
152                span: join.span.clone(),
153            })),
154        }
155    }
156
157    /// Validate a single AST node recursively, tracking subquery nesting depth.
158    fn validate_node_with_depth(
159        &self,
160        node: &Expr,
161        subquery_depth: usize,
162    ) -> Result<(), ValidationError> {
163        match node {
164            Expr::And(operands) | Expr::Or(operands) => {
165                for operand in operands {
166                    self.validate_node_with_depth(operand, subquery_depth)?;
167                }
168                Ok(())
169            }
170            Expr::Not(operand) => self.validate_node_with_depth(operand, subquery_depth),
171            Expr::Condition(condition) => {
172                self.validate_condition(condition)?;
173                // If the value is a subquery, validate its inner expression
174                // with incremented depth
175                if let Value::Subquery(inner) = &condition.value {
176                    let new_depth = subquery_depth + 1;
177                    if new_depth > crate::query::types::MAX_SUBQUERY_DEPTH {
178                        return Err(ValidationError::SubqueryDepthExceeded {
179                            depth: new_depth,
180                            max_depth: crate::query::types::MAX_SUBQUERY_DEPTH,
181                            span: condition.span.clone(),
182                        });
183                    }
184                    self.validate_node_with_depth(inner, new_depth)?;
185                }
186                Ok(())
187            }
188            Expr::Join(join) => {
189                self.validate_node_with_depth(&join.left, subquery_depth)?;
190                self.validate_node_with_depth(&join.right, subquery_depth)?;
191                Ok(())
192            }
193        }
194    }
195
196    /// Validate a condition
197    fn validate_condition(&self, condition: &Condition) -> Result<(), ValidationError> {
198        let field_name = condition.field.as_str();
199        let field_desc = self.resolve_field_descriptor(condition)?;
200
201        Self::validate_operator(field_name, field_desc, condition)?;
202        Self::validate_value_type(field_name, field_desc, condition)?;
203        Self::validate_enum_value(field_name, field_desc, condition)?;
204        Self::validate_regex_pattern(condition)?;
205
206        Ok(())
207    }
208
209    fn resolve_field_descriptor<'a>(
210        &'a self,
211        condition: &Condition,
212    ) -> Result<&'a FieldDescriptor, ValidationError> {
213        let field_name = condition.field.as_str();
214        self.registry.get(field_name).ok_or_else(|| {
215            let suggestion = self.suggest_field(field_name);
216            ValidationError::UnknownField {
217                field: field_name.to_string(),
218                suggestion,
219                span: condition.span.clone(),
220            }
221        })
222    }
223
224    fn validate_operator(
225        field_name: &str,
226        field_desc: &FieldDescriptor,
227        condition: &Condition,
228    ) -> Result<(), ValidationError> {
229        if field_desc.supports_operator(&condition.operator) {
230            return Ok(());
231        }
232
233        Err(ValidationError::InvalidOperator {
234            field: field_name.to_string(),
235            operator: condition.operator.clone(),
236            valid_operators: field_desc.operators.to_vec(),
237            span: condition.span.clone(),
238        })
239    }
240
241    fn validate_value_type(
242        field_name: &str,
243        field_desc: &FieldDescriptor,
244        condition: &Condition,
245    ) -> Result<(), ValidationError> {
246        let is_value_type_valid = match (&condition.operator, &condition.value) {
247            // Regex values are valid with ~= operator for String/Enum/Path fields
248            (Operator::Regex, Value::Regex(_)) => matches!(
249                field_desc.field_type,
250                FieldType::String | FieldType::Enum(_) | FieldType::Path
251            ),
252            // For all other cases, use standard type matching
253            _ => field_desc.matches_value_type(&condition.value),
254        };
255
256        if is_value_type_valid {
257            return Ok(());
258        }
259
260        Err(ValidationError::TypeMismatch {
261            field: field_name.to_string(),
262            expected: field_desc.field_type.clone(),
263            got: condition.value.clone(),
264            span: condition.span.clone(),
265        })
266    }
267
268    fn validate_enum_value(
269        field_name: &str,
270        field_desc: &FieldDescriptor,
271        condition: &Condition,
272    ) -> Result<(), ValidationError> {
273        if let FieldType::Enum(allowed_values) = &field_desc.field_type
274            && let Value::String(value) = &condition.value
275            && !allowed_values.contains(&value.as_str())
276        {
277            return Err(ValidationError::InvalidEnumValue {
278                field: field_name.to_string(),
279                value: value.clone(),
280                valid_values: allowed_values.clone(),
281                span: condition.span.clone(),
282            });
283        }
284
285        Ok(())
286    }
287
288    fn validate_regex_pattern(condition: &Condition) -> Result<(), ValidationError> {
289        let Value::Regex(regex_val) = &condition.value else {
290            return Ok(());
291        };
292
293        // Check if pattern contains lookaround assertions
294        let has_lookaround = regex_val.pattern.contains("(?=")
295            || regex_val.pattern.contains("(?!")
296            || regex_val.pattern.contains("(?<=")
297            || regex_val.pattern.contains("(?<!");
298
299        if has_lookaround {
300            // Use fancy-regex for lookaround support
301            if let Err(e) = fancy_regex::Regex::new(&regex_val.pattern) {
302                return Err(ValidationError::InvalidRegexPattern {
303                    pattern: regex_val.pattern.clone(),
304                    error: e.to_string(),
305                    span: condition.span.clone(),
306                });
307            }
308        } else {
309            // Use standard regex for performance
310            if let Err(e) = Regex::new(&regex_val.pattern) {
311                return Err(ValidationError::InvalidRegexPattern {
312                    pattern: regex_val.pattern.clone(),
313                    error: e.to_string(),
314                    span: condition.span.clone(),
315                });
316            }
317        }
318
319        Ok(())
320    }
321
322    fn normalize_operands(&self, operands: &[Expr]) -> Result<Vec<Expr>, ValidationError> {
323        let mut normalized = Vec::with_capacity(operands.len());
324        for operand in operands {
325            normalized.push(self.normalize_expr(operand)?);
326        }
327        Ok(normalized)
328    }
329
330    /// Detect contradictions in the query
331    ///
332    /// Returns warnings for impossible queries, such as:
333    /// - `kind:function AND kind:class` (same field with different values)
334    /// - `async:true AND async:false` (boolean contradiction)
335    #[allow(clippy::only_used_in_recursion)]
336    #[must_use]
337    pub fn detect_contradictions(&self, expr: &Expr) -> Vec<ContradictionWarning> {
338        let mut warnings = Vec::new();
339
340        if let Expr::And(operands) = expr {
341            warnings.extend(Self::detect_exact_match_contradictions(operands));
342        }
343
344        warnings.extend(self.detect_nested_contradictions(expr));
345
346        warnings
347    }
348
349    fn detect_exact_match_contradictions(operands: &[Expr]) -> Vec<ContradictionWarning> {
350        let constraints = Self::collect_exact_constraints(operands);
351        constraints
352            .into_iter()
353            .filter_map(|(field, values)| {
354                Self::contradiction_for_field(operands, field.as_str(), &values)
355            })
356            .collect()
357    }
358
359    fn detect_nested_contradictions(&self, expr: &Expr) -> Vec<ContradictionWarning> {
360        match expr {
361            Expr::And(operands) | Expr::Or(operands) => operands
362                .iter()
363                .flat_map(|operand| self.detect_contradictions(operand))
364                .collect(),
365            Expr::Not(operand) => self.detect_contradictions(operand),
366            Expr::Condition(_) => Vec::new(),
367            Expr::Join(join) => {
368                let mut warnings = self.detect_contradictions(&join.left);
369                warnings.extend(self.detect_contradictions(&join.right));
370                warnings
371            }
372        }
373    }
374
375    fn collect_exact_constraints(operands: &[Expr]) -> HashMap<String, Vec<(String, usize)>> {
376        let mut constraints: HashMap<String, Vec<(String, usize)>> = HashMap::new();
377
378        for (idx, operand) in operands.iter().enumerate() {
379            if let Expr::Condition(condition) = operand
380                && condition.operator == Operator::Equal
381            {
382                if let Some(value) = condition.value.as_string() {
383                    constraints
384                        .entry(condition.field.as_str().to_string())
385                        .or_default()
386                        .push((value.to_string(), idx));
387                } else if let Value::Boolean(value) = &condition.value {
388                    constraints
389                        .entry(condition.field.as_str().to_string())
390                        .or_default()
391                        .push((value.to_string(), idx));
392                }
393            }
394        }
395
396        constraints
397    }
398
399    fn contradiction_for_field(
400        operands: &[Expr],
401        field: &str,
402        values: &[(String, usize)],
403    ) -> Option<ContradictionWarning> {
404        if values.len() <= 1 {
405            return None;
406        }
407
408        let unique_values: Vec<_> = values
409            .iter()
410            .map(|(v, _)| v.as_str())
411            .collect::<std::collections::HashSet<_>>()
412            .into_iter()
413            .collect();
414
415        if unique_values.len() <= 1 {
416            return None;
417        }
418
419        let merged_span = Self::merge_operand_spans(operands, values);
420        let value_list = unique_values.join("' and '");
421        Some(ContradictionWarning {
422            message: format!("Query is impossible: field '{field}' cannot be both '{value_list}'"),
423            span: merged_span,
424        })
425    }
426
427    fn merge_operand_spans(operands: &[Expr], values: &[(String, usize)]) -> Span {
428        values
429            .iter()
430            .filter_map(|(_, idx)| match &operands[*idx] {
431                Expr::Condition(cond) => Some(cond.span.clone()),
432                _ => None,
433            })
434            .fold(None, |acc: Option<Span>, span| {
435                Some(acc.map_or(span.clone(), |s| s.merge(&span)))
436            })
437            .unwrap_or_default()
438    }
439
440    /// Suggest a field name for a typo using Levenshtein distance
441    ///
442    /// Returns the closest matching field name if the edit distance is ≤ 2.
443    /// Matching is case-insensitive to handle case typos like "KIND" → "kind".
444    fn suggest_field(&self, input: &str) -> Option<String> {
445        self.suggest_field_with_threshold(input, 2)
446            .into_iter()
447            .next()
448    }
449
450    fn suggest_field_with_threshold(&self, input: &str, max_distance: usize) -> Vec<String> {
451        let input_lower = input.to_lowercase();
452        let mut best_match: Option<usize> = None;
453        let mut candidates: Vec<String> = Vec::new();
454
455        for field_name in self.registry.field_names() {
456            // Check for exact case-insensitive match first
457            if field_name.to_lowercase() == input_lower {
458                return vec![field_name.to_string()];
459            }
460
461            // Otherwise use Levenshtein distance
462            let distance = levenshtein_distance(&input_lower, &field_name.to_lowercase());
463
464            // Only suggest if distance within threshold
465            if distance <= max_distance {
466                match best_match {
467                    Some(best_dist) if distance < best_dist => {
468                        best_match = Some(distance);
469                        candidates.clear();
470                        candidates.push(field_name.to_string());
471                    }
472                    Some(best_dist) if distance == best_dist => {
473                        candidates.push(field_name.to_string());
474                    }
475                    None => {
476                        best_match = Some(distance);
477                        candidates.push(field_name.to_string());
478                    }
479                    _ => {}
480                }
481            }
482        }
483
484        candidates
485    }
486
487    fn normalize_condition(&self, condition: &Condition) -> Result<Condition, ValidationError> {
488        // Normalize a nested subquery value first, recursively, so alias
489        // resolution reaches every field inside a relation subquery, e.g.
490        // `callers:(file:main.rs)`, and not just the outer condition.
491        // `validate_node_with_depth` and `resolve_variables` both descend into
492        // `Value::Subquery`, so normalization must too; otherwise a `file:`
493        // alias nested in a subquery survives to the executor unresolved,
494        // hits graph_eval's `_ => Ok(false)` (no `file` arm), and silently
495        // matches nothing. Recursing through `normalize_expr` covers boolean
496        // and negation wrappers inside the subquery as well (issue #513).
497        let normalized_value = match &condition.value {
498            Value::Subquery(inner) => Value::Subquery(Box::new(self.normalize_expr(inner)?)),
499            other => other.clone(),
500        };
501
502        // Fast path: known field, either a canonical name or a registered alias.
503        //
504        // Resolve any alias to its canonical field name here. Downstream
505        // evaluators match on the canonical field (e.g. `path`), so an alias
506        // like `file` (registered as `file` -> `path`) that survives to the
507        // executor has no match arm and silently returns zero results even
508        // though validation passed. Rewriting the field to its canonical form
509        // makes `file:` a true alias of `path:` end to end (issue #513).
510        let field_name = condition.field.as_str();
511        let canonical = self
512            .registry
513            .resolve_canonical(field_name)
514            .map(std::string::ToString::to_string);
515        if let Some(canonical) = canonical {
516            let mut resolved = condition.clone();
517            if canonical != field_name {
518                resolved.field = Field::new(canonical);
519            }
520            resolved.value = normalized_value;
521            return Ok(resolved);
522        }
523
524        // Fuzzy disabled: reject unknown fields outright.
525        if !self.options.fuzzy_fields {
526            return Err(ValidationError::UnknownField {
527                field: condition.field.as_str().to_string(),
528                suggestion: self.suggest_field(condition.field.as_str()),
529                span: condition.span.clone(),
530            });
531        }
532
533        // Try fuzzy suggestion within threshold
534        let suggestions = self.suggest_field_with_threshold(
535            condition.field.as_str(),
536            self.options.fuzzy_field_distance,
537        );
538        match suggestions.len() {
539            1 => {
540                let mut corrected = condition.clone();
541                let candidate = suggestions[0].clone();
542
543                // Do not auto-correct fields that are prone to ambiguity (e.g., "name").
544                // Users must spell these exactly or accept explicit errors.
545                if !SAFE_FUZZY_FIELDS.contains(&candidate.as_str()) {
546                    return Err(ValidationError::UnsafeFuzzyCorrection {
547                        input: condition.field.as_str().to_string(),
548                        suggestion: candidate,
549                        span: condition.span.clone(),
550                    });
551                }
552
553                corrected.field = Field::new(candidate);
554                corrected.value = normalized_value;
555                Ok(corrected)
556            }
557            n if n > 1 => Err(ValidationError::UnknownField {
558                field: condition.field.as_str().to_string(),
559                suggestion: Some(format!("ambiguous: {}", suggestions.join(", "))),
560                span: condition.span.clone(),
561            }),
562            _ => Err(ValidationError::UnknownField {
563                field: condition.field.as_str().to_string(),
564                suggestion: None,
565                span: condition.span.clone(),
566            }),
567        }
568    }
569}
570
571/// Warning about a potential contradiction in the query
572#[derive(Debug, Clone, PartialEq)]
573pub struct ContradictionWarning {
574    /// Warning message
575    pub message: String,
576    /// Location of the contradiction
577    pub span: Span,
578}
579
580/// Compute Levenshtein distance (edit distance) between two strings
581///
582/// Returns the minimum number of single-character edits (insertions, deletions, substitutions)
583/// required to change one string into the other.
584#[allow(clippy::needless_range_loop)]
585fn levenshtein_distance(s1: &str, s2: &str) -> usize {
586    let len1 = s1.chars().count();
587    let len2 = s2.chars().count();
588
589    // Create matrix
590    let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
591
592    // Initialize first row and column
593    for i in 0..=len1 {
594        matrix[i][0] = i;
595    }
596    for j in 0..=len2 {
597        matrix[0][j] = j;
598    }
599
600    // Fill matrix
601    let s1_chars: Vec<char> = s1.chars().collect();
602    let s2_chars: Vec<char> = s2.chars().collect();
603
604    for (i, c1) in s1_chars.iter().enumerate() {
605        for (j, c2) in s2_chars.iter().enumerate() {
606            let cost = usize::from(c1 != c2);
607
608            matrix[i + 1][j + 1] = std::cmp::min(
609                std::cmp::min(
610                    matrix[i][j + 1] + 1, // deletion
611                    matrix[i + 1][j] + 1, // insertion
612                ),
613                matrix[i][j] + cost, // substitution
614            );
615        }
616    }
617
618    matrix[len1][len2]
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624    use crate::query::types::{Field, Span};
625
626    #[test]
627    fn test_levenshtein_distance() {
628        assert_eq!(levenshtein_distance("", ""), 0);
629        assert_eq!(levenshtein_distance("hello", "hello"), 0);
630        assert_eq!(levenshtein_distance("hello", "hallo"), 1);
631        assert_eq!(levenshtein_distance("kind", "knd"), 1);
632        assert_eq!(levenshtein_distance("kind", "kond"), 1);
633        assert_eq!(levenshtein_distance("kind", "king"), 1);
634        assert_eq!(levenshtein_distance("kind", "xyz"), 4);
635    }
636
637    #[test]
638    fn test_validate_valid_condition() {
639        let registry = FieldRegistry::with_core_fields();
640        let validator = Validator::new(registry);
641
642        let condition = Expr::Condition(Condition {
643            field: Field::new("kind"),
644            operator: Operator::Equal,
645            value: Value::String("function".to_string()),
646            span: Span::default(),
647        });
648
649        assert!(validator.validate(&condition).is_ok());
650    }
651
652    #[test]
653    fn test_validate_unknown_field() {
654        let registry = FieldRegistry::with_core_fields();
655        let validator = Validator::new(registry);
656
657        let condition = Expr::Condition(Condition {
658            field: Field::new("unknown"),
659            operator: Operator::Equal,
660            value: Value::String("value".to_string()),
661            span: Span::default(),
662        });
663
664        let result = validator.validate(&condition);
665        assert!(result.is_err());
666        assert!(matches!(
667            result.unwrap_err(),
668            ValidationError::UnknownField { .. }
669        ));
670    }
671
672    /// Issue #513: `file:` is advertised as an alias of `path:` but the
673    /// executor only has a `path` match arm, so an unresolved `file` field
674    /// silently matched nothing. `normalize_expr` must rewrite the alias to
675    /// its canonical field name so both spell out to the same condition.
676    #[test]
677    fn test_normalize_resolves_file_alias_to_path() {
678        let registry = FieldRegistry::with_core_fields();
679        let validator = Validator::new(registry);
680
681        let file_expr = Expr::Condition(Condition {
682            field: Field::new("file"),
683            operator: Operator::Equal,
684            value: Value::String("crates/**".to_string()),
685            span: Span::default(),
686        });
687        let normalized = validator.normalize_expr(&file_expr).unwrap();
688        let Expr::Condition(cond) = normalized else {
689            panic!("expected condition");
690        };
691        assert_eq!(cond.field.as_str(), "path");
692        assert!(matches!(cond.value, Value::String(ref s) if s == "crates/**"));
693        assert_eq!(cond.operator, Operator::Equal);
694    }
695
696    /// `file:X` and `path:X` must normalize to identical conditions so the two
697    /// spellings return the same matches end to end (issue #513).
698    #[test]
699    fn test_normalize_file_and_path_are_equivalent() {
700        let registry = FieldRegistry::with_core_fields();
701        let validator = Validator::new(registry);
702
703        let make = |field: &str| {
704            Expr::Condition(Condition {
705                field: Field::new(field),
706                operator: Operator::Equal,
707                value: Value::String("src/**/*.rs".to_string()),
708                span: Span::default(),
709            })
710        };
711
712        let from_file = validator.normalize_expr(&make("file")).unwrap();
713        let from_path = validator.normalize_expr(&make("path")).unwrap();
714
715        let (Expr::Condition(file_cond), Expr::Condition(path_cond)) = (from_file, from_path)
716        else {
717            panic!("expected conditions");
718        };
719        assert_eq!(file_cond.field.as_str(), path_cond.field.as_str());
720        assert_eq!(file_cond.field.as_str(), "path");
721        assert_eq!(file_cond.operator, path_cond.operator);
722        assert!(matches!((&file_cond.value, &path_cond.value),
723                (Value::String(a), Value::String(b)) if a == b));
724
725        // Both must still pass validation after normalization.
726        assert!(validator.validate(&Expr::Condition(file_cond)).is_ok());
727        assert!(validator.validate(&Expr::Condition(path_cond)).is_ok());
728    }
729
730    /// The `language` -> `lang` alias must canonicalize the same way, and a
731    /// nested alias inside a boolean expression must be rewritten too.
732    #[test]
733    fn test_normalize_language_alias_and_nested() {
734        let registry = FieldRegistry::with_core_fields();
735        let validator = Validator::new(registry);
736
737        let expr = Expr::And(vec![
738            Expr::Condition(Condition {
739                field: Field::new("kind"),
740                operator: Operator::Equal,
741                value: Value::String("struct".to_string()),
742                span: Span::default(),
743            }),
744            Expr::Condition(Condition {
745                field: Field::new("file"),
746                operator: Operator::Equal,
747                value: Value::String("crates/**".to_string()),
748                span: Span::default(),
749            }),
750            Expr::Condition(Condition {
751                field: Field::new("language"),
752                operator: Operator::Equal,
753                value: Value::String("rust".to_string()),
754                span: Span::default(),
755            }),
756        ]);
757
758        let Expr::And(operands) = validator.normalize_expr(&expr).unwrap() else {
759            panic!("expected And");
760        };
761        let fields: Vec<&str> = operands
762            .iter()
763            .map(|op| match op {
764                Expr::Condition(c) => c.field.as_str(),
765                _ => panic!("expected condition"),
766            })
767            .collect();
768        assert_eq!(fields, vec!["kind", "path", "lang"]);
769    }
770
771    /// Issue #513 (subquery regression): a `file:` alias inside a relation
772    /// subquery, e.g. `callers:(file:main.rs)`, must be canonicalized to
773    /// `path` too. Normalization used to rewrite only the outer condition and
774    /// leave `Value::Subquery` fields untouched, so nested `file:` reached the
775    /// executor unresolved and matched nothing.
776    #[test]
777    fn test_normalize_resolves_file_alias_inside_subquery() {
778        let registry = FieldRegistry::with_core_fields();
779        let validator = Validator::new(registry);
780
781        let relation_with = |inner_field: &str| {
782            Expr::Condition(Condition {
783                field: Field::new("callers"),
784                operator: Operator::Equal,
785                value: Value::Subquery(Box::new(Expr::Condition(Condition {
786                    field: Field::new(inner_field),
787                    operator: Operator::Equal,
788                    value: Value::String("main.rs".to_string()),
789                    span: Span::default(),
790                }))),
791                span: Span::default(),
792            })
793        };
794
795        let from_file = validator.normalize_expr(&relation_with("file")).unwrap();
796        let from_path = validator.normalize_expr(&relation_with("path")).unwrap();
797
798        // The inner subquery field must be canonicalized to `path`, and the two
799        // spellings must produce an identical normalized AST.
800        assert_eq!(from_file, from_path);
801
802        let Expr::Condition(cond) = from_file else {
803            panic!("expected condition");
804        };
805        let Value::Subquery(inner) = cond.value else {
806            panic!("expected subquery value");
807        };
808        let Expr::Condition(inner_cond) = *inner else {
809            panic!("expected inner condition");
810        };
811        assert_eq!(inner_cond.field.as_str(), "path");
812    }
813
814    /// The recursion must also reach fields nested inside a boolean wrapper
815    /// within a subquery, e.g. `callers:(file:a.rs OR file:b.rs)`.
816    #[test]
817    fn test_normalize_resolves_file_alias_in_nested_boolean_subquery() {
818        let registry = FieldRegistry::with_core_fields();
819        let validator = Validator::new(registry);
820
821        let make_leaf = |value: &str| {
822            Expr::Condition(Condition {
823                field: Field::new("file"),
824                operator: Operator::Equal,
825                value: Value::String(value.to_string()),
826                span: Span::default(),
827            })
828        };
829        let expr = Expr::Condition(Condition {
830            field: Field::new("callers"),
831            operator: Operator::Equal,
832            value: Value::Subquery(Box::new(Expr::Or(vec![
833                make_leaf("a.rs"),
834                make_leaf("b.rs"),
835            ]))),
836            span: Span::default(),
837        });
838
839        let Expr::Condition(cond) = validator.normalize_expr(&expr).unwrap() else {
840            panic!("expected condition");
841        };
842        let Value::Subquery(inner) = cond.value else {
843            panic!("expected subquery value");
844        };
845        let Expr::Or(operands) = *inner else {
846            panic!("expected Or inside subquery");
847        };
848        for op in operands {
849            let Expr::Condition(leaf) = op else {
850                panic!("expected condition leaf");
851            };
852            assert_eq!(leaf.field.as_str(), "path");
853        }
854    }
855
856    #[test]
857    fn test_suggest_field_typo() {
858        let registry = FieldRegistry::with_core_fields();
859        let validator = Validator::new(registry);
860
861        let suggestion = validator.suggest_field("knd");
862        assert_eq!(suggestion, Some("kind".to_string()));
863
864        let suggestion = validator.suggest_field("kond");
865        assert_eq!(suggestion, Some("kind".to_string()));
866
867        let suggestion = validator.suggest_field("nme");
868        assert_eq!(suggestion, Some("name".to_string()));
869    }
870
871    #[test]
872    fn test_suggest_field_no_match() {
873        let registry = FieldRegistry::with_core_fields();
874        let validator = Validator::new(registry);
875
876        let suggestion = validator.suggest_field("xyz");
877        assert!(suggestion.is_none());
878
879        let suggestion = validator.suggest_field("foobar");
880        assert!(suggestion.is_none());
881    }
882
883    #[test]
884    fn test_fuzzy_field_correction_enabled() {
885        let registry = FieldRegistry::with_core_fields();
886        let options = ValidationOptions {
887            fuzzy_fields: true,
888            fuzzy_field_distance: 2,
889        };
890        let validator = Validator::with_options(registry, options);
891        let cond = Condition {
892            field: Field::new("knd"),
893            operator: Operator::Equal,
894            value: Value::String("function".to_string()),
895            span: Span::default(),
896        };
897        let normalized = validator
898            .normalize_condition(&cond)
899            .expect("should normalize");
900        assert_eq!(normalized.field.as_str(), "kind");
901    }
902
903    #[test]
904    fn test_fuzzy_field_ambiguous_rejected() {
905        let registry = FieldRegistry::with_core_fields();
906        let options = ValidationOptions {
907            fuzzy_fields: true,
908            fuzzy_field_distance: 2,
909        };
910        let validator = Validator::with_options(registry, options);
911        let cond = Condition {
912            field: Field::new("nam"),
913            operator: Operator::Equal,
914            value: Value::String("foo".to_string()),
915            span: Span::default(),
916        };
917        let result = validator.normalize_condition(&cond);
918        assert!(result.is_err(), "ambiguous correction must error");
919    }
920
921    #[test]
922    fn test_fuzzy_field_disabled_rejects() {
923        let registry = FieldRegistry::with_core_fields();
924        let validator = Validator::new(registry);
925        let cond = Condition {
926            field: Field::new("knd"),
927            operator: Operator::Equal,
928            value: Value::String("function".to_string()),
929            span: Span::default(),
930        };
931        let result = validator.normalize_condition(&cond);
932        assert!(result.is_err(), "disabled fuzzy should reject typos");
933    }
934
935    #[test]
936    fn test_fuzzy_field_non_whitelisted_returns_unsafe_error() {
937        // Add a custom field that is NOT in SAFE_FUZZY_FIELDS whitelist
938        let mut registry = FieldRegistry::with_core_fields();
939        registry.add_field(super::super::types::FieldDescriptor {
940            name: "custom",
941            field_type: FieldType::String,
942            operators: &[Operator::Equal],
943            indexed: false,
944            doc: "A custom field for testing",
945        });
946        let options = ValidationOptions {
947            fuzzy_fields: true,
948            fuzzy_field_distance: 2,
949        };
950        let validator = Validator::with_options(registry, options);
951        // "custm" is a typo for "custom" (distance 1)
952        let cond = Condition {
953            field: Field::new("custm"),
954            operator: Operator::Equal,
955            value: Value::String("test".to_string()),
956            span: Span::default(),
957        };
958        let result = validator.normalize_condition(&cond);
959        assert!(result.is_err(), "non-whitelisted field should error");
960        assert!(
961            matches!(
962                result.unwrap_err(),
963                ValidationError::UnsafeFuzzyCorrection { .. }
964            ),
965            "should return UnsafeFuzzyCorrection, not UnknownField"
966        );
967    }
968
969    #[test]
970    fn test_suggest_field_case_insensitive() {
971        let registry = FieldRegistry::with_core_fields();
972        let validator = Validator::new(registry);
973
974        // Exact case-insensitive match
975        let suggestion = validator.suggest_field("KIND");
976        assert_eq!(suggestion, Some("kind".to_string()));
977
978        let suggestion = validator.suggest_field("Name");
979        assert_eq!(suggestion, Some("name".to_string()));
980
981        // Case-insensitive with typo
982        let suggestion = validator.suggest_field("KND");
983        assert_eq!(suggestion, Some("kind".to_string()));
984    }
985
986    #[test]
987    fn test_validate_invalid_operator() {
988        let registry = FieldRegistry::with_core_fields();
989        let validator = Validator::new(registry);
990
991        let condition = Expr::Condition(Condition {
992            field: Field::new("kind"),
993            operator: Operator::Greater,
994            value: Value::String("function".to_string()),
995            span: Span::default(),
996        });
997
998        let result = validator.validate(&condition);
999        assert!(result.is_err());
1000        assert!(matches!(
1001            result.unwrap_err(),
1002            ValidationError::InvalidOperator { .. }
1003        ));
1004    }
1005
1006    #[test]
1007    fn test_validate_type_mismatch() {
1008        let registry = FieldRegistry::with_core_fields();
1009        let _validator = Validator::new(registry);
1010
1011        // Add an async field for testing
1012        let mut registry = FieldRegistry::with_core_fields();
1013        registry.add_field(super::super::types::FieldDescriptor {
1014            name: "async",
1015            field_type: FieldType::Bool,
1016            operators: &[Operator::Equal],
1017            indexed: false,
1018            doc: "Whether function is async",
1019        });
1020        let validator = Validator::new(registry);
1021
1022        let condition = Expr::Condition(Condition {
1023            field: Field::new("async"),
1024            operator: Operator::Equal,
1025            value: Value::Number(123),
1026            span: Span::default(),
1027        });
1028
1029        let result = validator.validate(&condition);
1030        assert!(result.is_err());
1031        assert!(matches!(
1032            result.unwrap_err(),
1033            ValidationError::TypeMismatch { .. }
1034        ));
1035    }
1036
1037    #[test]
1038    fn test_validate_invalid_enum_value() {
1039        let registry = FieldRegistry::with_core_fields();
1040        let validator = Validator::new(registry);
1041
1042        let condition = Expr::Condition(Condition {
1043            field: Field::new("kind"),
1044            operator: Operator::Equal,
1045            value: Value::String("invalid_kind".to_string()),
1046            span: Span::default(),
1047        });
1048
1049        let result = validator.validate(&condition);
1050        assert!(result.is_err());
1051        assert!(matches!(
1052            result.unwrap_err(),
1053            ValidationError::InvalidEnumValue { .. }
1054        ));
1055    }
1056
1057    #[test]
1058    fn test_validate_valid_enum_value() {
1059        let registry = FieldRegistry::with_core_fields();
1060        let validator = Validator::new(registry);
1061
1062        let valid_kinds = ["function", "method", "class", "struct", "trait"];
1063
1064        for kind in &valid_kinds {
1065            let condition = Expr::Condition(Condition {
1066                field: Field::new("kind"),
1067                operator: Operator::Equal,
1068                value: Value::String((*kind).to_string()),
1069                span: Span::default(),
1070            });
1071
1072            assert!(validator.validate(&condition).is_ok());
1073        }
1074    }
1075
1076    #[test]
1077    fn test_validate_invalid_regex() {
1078        let registry = FieldRegistry::with_core_fields();
1079        let validator = Validator::new(registry);
1080
1081        let condition = Expr::Condition(Condition {
1082            field: Field::new("name"),
1083            operator: Operator::Regex,
1084            value: Value::Regex(super::super::types::RegexValue {
1085                pattern: "[invalid".to_string(),
1086                flags: super::super::types::RegexFlags::default(),
1087            }),
1088            span: Span::default(),
1089        });
1090
1091        let result = validator.validate(&condition);
1092        assert!(result.is_err());
1093        assert!(matches!(
1094            result.unwrap_err(),
1095            ValidationError::InvalidRegexPattern { .. }
1096        ));
1097    }
1098
1099    #[test]
1100    fn test_validate_valid_regex() {
1101        let registry = FieldRegistry::with_core_fields();
1102        let validator = Validator::new(registry);
1103
1104        let condition = Expr::Condition(Condition {
1105            field: Field::new("name"),
1106            operator: Operator::Regex,
1107            value: Value::Regex(super::super::types::RegexValue {
1108                pattern: "^test_.*".to_string(),
1109                flags: super::super::types::RegexFlags::default(),
1110            }),
1111            span: Span::default(),
1112        });
1113
1114        assert!(validator.validate(&condition).is_ok());
1115    }
1116
1117    #[test]
1118    fn test_detect_contradiction_enum() {
1119        let registry = FieldRegistry::with_core_fields();
1120        let validator = Validator::new(registry);
1121
1122        let expr = Expr::And(vec![
1123            Expr::Condition(Condition {
1124                field: Field::new("kind"),
1125                operator: Operator::Equal,
1126                value: Value::String("function".to_string()),
1127                span: Span::default(),
1128            }),
1129            Expr::Condition(Condition {
1130                field: Field::new("kind"),
1131                operator: Operator::Equal,
1132                value: Value::String("class".to_string()),
1133                span: Span::default(),
1134            }),
1135        ]);
1136
1137        let warnings = validator.detect_contradictions(&expr);
1138        assert_eq!(warnings.len(), 1);
1139        assert!(warnings[0].message.contains("kind"));
1140        assert!(warnings[0].message.contains("function"));
1141        assert!(warnings[0].message.contains("class"));
1142    }
1143
1144    #[test]
1145    fn test_detect_contradiction_boolean() {
1146        let mut registry = FieldRegistry::with_core_fields();
1147        registry.add_field(super::super::types::FieldDescriptor {
1148            name: "async",
1149            field_type: FieldType::Bool,
1150            operators: &[Operator::Equal],
1151            indexed: false,
1152            doc: "Whether function is async",
1153        });
1154        let validator = Validator::new(registry);
1155
1156        let expr = Expr::And(vec![
1157            Expr::Condition(Condition {
1158                field: Field::new("async"),
1159                operator: Operator::Equal,
1160                value: Value::Boolean(true),
1161                span: Span::default(),
1162            }),
1163            Expr::Condition(Condition {
1164                field: Field::new("async"),
1165                operator: Operator::Equal,
1166                value: Value::Boolean(false),
1167                span: Span::default(),
1168            }),
1169        ]);
1170
1171        let warnings = validator.detect_contradictions(&expr);
1172        assert_eq!(warnings.len(), 1);
1173        assert!(warnings[0].message.contains("async"));
1174    }
1175
1176    #[test]
1177    fn test_no_contradiction_or() {
1178        let registry = FieldRegistry::with_core_fields();
1179        let validator = Validator::new(registry);
1180
1181        let expr = Expr::Or(vec![
1182            Expr::Condition(Condition {
1183                field: Field::new("kind"),
1184                operator: Operator::Equal,
1185                value: Value::String("function".to_string()),
1186                span: Span::default(),
1187            }),
1188            Expr::Condition(Condition {
1189                field: Field::new("kind"),
1190                operator: Operator::Equal,
1191                value: Value::String("class".to_string()),
1192                span: Span::default(),
1193            }),
1194        ]);
1195
1196        let warnings = validator.detect_contradictions(&expr);
1197        assert_eq!(warnings.len(), 0);
1198    }
1199
1200    #[test]
1201    fn test_no_contradiction_different_fields() {
1202        let mut registry = FieldRegistry::with_core_fields();
1203        registry.add_field(super::super::types::FieldDescriptor {
1204            name: "async",
1205            field_type: FieldType::Bool,
1206            operators: &[Operator::Equal],
1207            indexed: false,
1208            doc: "Whether function is async",
1209        });
1210        let validator = Validator::new(registry);
1211
1212        let expr = Expr::And(vec![
1213            Expr::Condition(Condition {
1214                field: Field::new("kind"),
1215                operator: Operator::Equal,
1216                value: Value::String("function".to_string()),
1217                span: Span::default(),
1218            }),
1219            Expr::Condition(Condition {
1220                field: Field::new("async"),
1221                operator: Operator::Equal,
1222                value: Value::Boolean(true),
1223                span: Span::default(),
1224            }),
1225        ]);
1226
1227        let warnings = validator.detect_contradictions(&expr);
1228        assert_eq!(warnings.len(), 0);
1229    }
1230
1231    #[test]
1232    fn test_validate_and_expression() {
1233        let mut registry = FieldRegistry::with_core_fields();
1234        registry.add_field(super::super::types::FieldDescriptor {
1235            name: "async",
1236            field_type: FieldType::Bool,
1237            operators: &[Operator::Equal],
1238            indexed: false,
1239            doc: "Whether function is async",
1240        });
1241        let validator = Validator::new(registry);
1242
1243        let expr = Expr::And(vec![
1244            Expr::Condition(Condition {
1245                field: Field::new("kind"),
1246                operator: Operator::Equal,
1247                value: Value::String("function".to_string()),
1248                span: Span::default(),
1249            }),
1250            Expr::Condition(Condition {
1251                field: Field::new("async"),
1252                operator: Operator::Equal,
1253                value: Value::Boolean(true),
1254                span: Span::default(),
1255            }),
1256        ]);
1257
1258        assert!(validator.validate(&expr).is_ok());
1259    }
1260
1261    #[test]
1262    fn test_validate_or_expression() {
1263        let registry = FieldRegistry::with_core_fields();
1264        let validator = Validator::new(registry);
1265
1266        let expr = Expr::Or(vec![
1267            Expr::Condition(Condition {
1268                field: Field::new("kind"),
1269                operator: Operator::Equal,
1270                value: Value::String("function".to_string()),
1271                span: Span::default(),
1272            }),
1273            Expr::Condition(Condition {
1274                field: Field::new("kind"),
1275                operator: Operator::Equal,
1276                value: Value::String("class".to_string()),
1277                span: Span::default(),
1278            }),
1279        ]);
1280
1281        assert!(validator.validate(&expr).is_ok());
1282    }
1283
1284    #[test]
1285    fn test_validate_not_expression() {
1286        let registry = FieldRegistry::with_core_fields();
1287        let validator = Validator::new(registry);
1288
1289        let expr = Expr::Not(Box::new(Expr::Condition(Condition {
1290            field: Field::new("kind"),
1291            operator: Operator::Equal,
1292            value: Value::String("function".to_string()),
1293            span: Span::default(),
1294        })));
1295
1296        assert!(validator.validate(&expr).is_ok());
1297    }
1298
1299    #[test]
1300    fn test_validate_nested_expression() {
1301        let mut registry = FieldRegistry::with_core_fields();
1302        registry.add_field(super::super::types::FieldDescriptor {
1303            name: "async",
1304            field_type: FieldType::Bool,
1305            operators: &[Operator::Equal],
1306            indexed: false,
1307            doc: "Whether function is async",
1308        });
1309        let validator = Validator::new(registry);
1310
1311        let expr = Expr::And(vec![
1312            Expr::Or(vec![
1313                Expr::Condition(Condition {
1314                    field: Field::new("kind"),
1315                    operator: Operator::Equal,
1316                    value: Value::String("function".to_string()),
1317                    span: Span::default(),
1318                }),
1319                Expr::Condition(Condition {
1320                    field: Field::new("kind"),
1321                    operator: Operator::Equal,
1322                    value: Value::String("method".to_string()),
1323                    span: Span::default(),
1324                }),
1325            ]),
1326            Expr::Condition(Condition {
1327                field: Field::new("async"),
1328                operator: Operator::Equal,
1329                value: Value::Boolean(true),
1330                span: Span::default(),
1331            }),
1332        ]);
1333
1334        assert!(validator.validate(&expr).is_ok());
1335    }
1336
1337    #[test]
1338    fn test_detect_nested_contradiction() {
1339        let registry = FieldRegistry::with_core_fields();
1340        let validator = Validator::new(registry);
1341
1342        // Nested: (kind:function AND kind:class) OR async:true
1343        // Should detect contradiction in left branch
1344        let expr = Expr::Or(vec![
1345            Expr::And(vec![
1346                Expr::Condition(Condition {
1347                    field: Field::new("kind"),
1348                    operator: Operator::Equal,
1349                    value: Value::String("function".to_string()),
1350                    span: Span::default(),
1351                }),
1352                Expr::Condition(Condition {
1353                    field: Field::new("kind"),
1354                    operator: Operator::Equal,
1355                    value: Value::String("class".to_string()),
1356                    span: Span::default(),
1357                }),
1358            ]),
1359            Expr::Condition(Condition {
1360                field: Field::new("name"),
1361                operator: Operator::Equal,
1362                value: Value::String("test".to_string()),
1363                span: Span::default(),
1364            }),
1365        ]);
1366
1367        let warnings = validator.detect_contradictions(&expr);
1368        assert_eq!(warnings.len(), 1);
1369        assert!(warnings[0].message.contains("kind"));
1370        assert!(warnings[0].message.contains("function"));
1371        assert!(warnings[0].message.contains("class"));
1372    }
1373
1374    #[test]
1375    fn test_contradiction_warning_has_span() {
1376        let registry = FieldRegistry::with_core_fields();
1377        let validator = Validator::new(registry);
1378
1379        let expr = Expr::And(vec![
1380            Expr::Condition(Condition {
1381                field: Field::new("kind"),
1382                operator: Operator::Equal,
1383                value: Value::String("function".to_string()),
1384                span: Span::with_position(0, 13, 1, 1),
1385            }),
1386            Expr::Condition(Condition {
1387                field: Field::new("kind"),
1388                operator: Operator::Equal,
1389                value: Value::String("class".to_string()),
1390                span: Span::with_position(18, 28, 1, 19),
1391            }),
1392        ]);
1393
1394        let warnings = validator.detect_contradictions(&expr);
1395        assert_eq!(warnings.len(), 1);
1396
1397        // Verify span is present and covers both conditions
1398        assert_eq!(warnings[0].span.start, 0);
1399        assert_eq!(warnings[0].span.end, 28);
1400    }
1401
1402    // ================================================================
1403    // Subquery depth validation tests
1404    // ================================================================
1405
1406    /// Build a subquery chain nested to the given depth.
1407    ///
1408    /// Depth 1: `callers:(kind:function)`
1409    /// Depth 2: `callers:(callers:(kind:function))`
1410    /// etc.
1411    fn build_nested_subquery(depth: usize) -> Expr {
1412        let mut expr = Expr::Condition(Condition {
1413            field: Field::new("kind"),
1414            operator: Operator::Equal,
1415            value: Value::String("function".to_string()),
1416            span: Span::default(),
1417        });
1418        for _ in 0..depth {
1419            expr = Expr::Condition(Condition {
1420                field: Field::new("callers"),
1421                operator: Operator::Equal,
1422                value: Value::Subquery(Box::new(expr)),
1423                span: Span::default(),
1424            });
1425        }
1426        expr
1427    }
1428
1429    #[test]
1430    fn test_subquery_depth_at_max_succeeds() {
1431        let registry = FieldRegistry::with_core_fields();
1432        let validator = Validator::new(registry);
1433
1434        // Build subquery nested exactly at MAX_SUBQUERY_DEPTH
1435        let expr = build_nested_subquery(crate::query::types::MAX_SUBQUERY_DEPTH);
1436        assert!(
1437            validator.validate(&expr).is_ok(),
1438            "subquery at exactly MAX_SUBQUERY_DEPTH should be valid"
1439        );
1440    }
1441
1442    #[test]
1443    fn test_subquery_depth_exceeds_max_fails() {
1444        let registry = FieldRegistry::with_core_fields();
1445        let validator = Validator::new(registry);
1446
1447        // Build subquery nested one beyond MAX_SUBQUERY_DEPTH
1448        let expr = build_nested_subquery(crate::query::types::MAX_SUBQUERY_DEPTH + 1);
1449        let result = validator.validate(&expr);
1450        assert!(
1451            result.is_err(),
1452            "subquery beyond MAX_SUBQUERY_DEPTH should fail"
1453        );
1454        assert!(
1455            matches!(
1456                result.unwrap_err(),
1457                ValidationError::SubqueryDepthExceeded { .. }
1458            ),
1459            "error should be SubqueryDepthExceeded"
1460        );
1461    }
1462}