Skip to main content

aurora_db/parser/
validator.rs

1//! AQL Validator - Validates parsed AQL documents before execution
2//!
3//! Performs:
4//! - Type checking against schema
5//! - Variable resolution
6//! - Filter operator validation
7//! - Collection and field existence checks
8
9use super::ast::{
10    self, Document, Field, Filter, FragmentDef, Mutation, Query, Selection, Subscription, Value,
11};
12use crate::types::{Collection, FieldType, ScalarType};
13use std::collections::{HashMap, HashSet};
14use std::fmt;
15
16/// Error codes for validation failures
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ErrorCode {
19    /// Referenced collection does not exist
20    UnknownCollection,
21    /// Referenced field does not exist in collection
22    UnknownField,
23    /// Explicitly invalid input (e.g. null where not allowed)
24    InvalidInput,
25    /// Value type doesn't match field type
26    TypeMismatch,
27    /// Required variable not provided
28    MissingRequiredVariable,
29    /// Optional variable used without default value
30    MissingOptionalVariable,
31    /// Filter operator not valid for field type
32    InvalidFilterOperator,
33    /// Duplicate alias in selection set
34    DuplicateAlias,
35    /// Invalid argument provided
36    InvalidArgument,
37    /// Unknown directive
38    UnknownDirective,
39    /// Unknown fragment
40    UnknownFragment,
41}
42
43impl fmt::Display for ErrorCode {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            ErrorCode::UnknownCollection => write!(f, "UnknownCollection"),
47            ErrorCode::UnknownField => write!(f, "UnknownField"),
48            ErrorCode::InvalidInput => write!(f, "InvalidInput"),
49            ErrorCode::TypeMismatch => write!(f, "TypeMismatch"),
50            ErrorCode::MissingRequiredVariable => write!(f, "MissingRequiredVariable"),
51            ErrorCode::MissingOptionalVariable => write!(f, "MissingOptionalVariable"),
52            ErrorCode::InvalidFilterOperator => write!(f, "InvalidFilterOperator"),
53            ErrorCode::DuplicateAlias => write!(f, "DuplicateAlias"),
54            ErrorCode::InvalidArgument => write!(f, "InvalidArgument"),
55            ErrorCode::UnknownDirective => write!(f, "UnknownDirective"),
56            ErrorCode::UnknownFragment => write!(f, "UnknownFragment"),
57        }
58    }
59}
60
61/// Validation error with context
62#[derive(Debug, Clone)]
63pub struct ValidationError {
64    pub code: ErrorCode,
65    pub message: String,
66    pub path: Option<String>,
67}
68
69impl fmt::Display for ValidationError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match &self.path {
72            Some(path) => write!(f, "[{}] {}", path, self.message),
73            None => write!(f, "{}", self.message),
74        }
75    }
76}
77
78impl std::error::Error for ValidationError {}
79
80impl ValidationError {
81    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
82        Self {
83            code,
84            message: message.into(),
85            path: None,
86        }
87    }
88
89    pub fn with_path(mut self, path: impl Into<String>) -> Self {
90        self.path = Some(path.into());
91        self
92    }
93}
94
95/// Validation result type
96pub type ValidationResult = Result<(), Vec<ValidationError>>;
97
98/// Schema provider trait for validation
99/// Allows validation without direct Aurora dependency
100pub trait SchemaProvider {
101    /// Get a collection definition by name
102    fn get_collection(&self, name: &str) -> Option<&Collection>;
103
104    /// Check if a collection exists
105    fn collection_exists(&self, name: &str) -> bool {
106        self.get_collection(name).is_some()
107    }
108}
109
110/// Simple in-memory schema for testing
111#[derive(Debug, Default)]
112pub struct InMemorySchema {
113    collections: HashMap<String, Collection>,
114}
115
116impl InMemorySchema {
117    pub fn new() -> Self {
118        Self::default()
119    }
120
121    pub fn add_collection(&mut self, collection: Collection) {
122        self.collections.insert(collection.name.clone(), collection);
123    }
124}
125
126impl SchemaProvider for InMemorySchema {
127    fn get_collection(&self, name: &str) -> Option<&Collection> {
128        self.collections.get(name)
129    }
130}
131
132/// Validation context holding schema and variable values
133pub struct ValidationContext<'a, S: SchemaProvider> {
134    pub schema: &'a S,
135    pub variables: HashMap<String, ast::Value>,
136    pub fragments: HashMap<String, &'a FragmentDef>,
137    pub errors: Vec<ValidationError>,
138    current_path: Vec<String>,
139    validating_fragments: HashSet<String>,
140}
141
142impl<'a, S: SchemaProvider> ValidationContext<'a, S> {
143    pub fn new(schema: &'a S) -> Self {
144        Self {
145            schema,
146            variables: HashMap::new(),
147            fragments: HashMap::new(),
148            errors: Vec::new(),
149            current_path: Vec::new(),
150            validating_fragments: HashSet::new(),
151        }
152    }
153
154    pub fn with_variables(mut self, variables: HashMap<String, ast::Value>) -> Self {
155        self.variables = variables;
156        self
157    }
158
159    fn push_path(&mut self, segment: &str) {
160        self.current_path.push(segment.to_string());
161    }
162
163    fn pop_path(&mut self) {
164        self.current_path.pop();
165    }
166
167    fn current_path_string(&self) -> Option<String> {
168        if self.current_path.is_empty() {
169            None
170        } else {
171            Some(self.current_path.join("."))
172        }
173    }
174
175    fn add_error(&mut self, code: ErrorCode, message: impl Into<String>) {
176        let mut error = ValidationError::new(code, message);
177        error.path = self.current_path_string();
178        self.errors.push(error);
179    }
180
181    pub fn has_errors(&self) -> bool {
182        !self.errors.is_empty()
183    }
184
185    pub fn into_result(self) -> ValidationResult {
186        if self.errors.is_empty() {
187            Ok(())
188        } else {
189            Err(self.errors)
190        }
191    }
192}
193
194/// Validate a complete AQL document
195pub fn validate_document<S: SchemaProvider>(
196    doc: &Document,
197    schema: &S,
198    variables: HashMap<String, ast::Value>,
199) -> ValidationResult {
200    let mut ctx = ValidationContext::new(schema).with_variables(variables);
201
202    // First pass: Collect fragment definitions
203    for op in &doc.operations {
204        if let ast::Operation::FragmentDefinition(frag) = op {
205            if ctx.fragments.insert(frag.name.clone(), frag).is_some() {
206                ctx.add_error(
207                    ErrorCode::InvalidInput,
208                    format!("Duplicate fragment definition '{}'", frag.name),
209                );
210            }
211        }
212    }
213
214    for (i, op) in doc.operations.iter().enumerate() {
215        ctx.push_path(&format!("operation[{}]", i));
216        match op {
217            ast::Operation::Query(query) => validate_query(query, &mut ctx),
218            ast::Operation::Mutation(mutation) => validate_mutation(mutation, &mut ctx),
219            ast::Operation::Subscription(sub) => validate_subscription(sub, &mut ctx),
220            ast::Operation::Schema(_) => {} // Schema definitions don't need validation
221            ast::Operation::Migration(_) => {}
222            ast::Operation::FragmentDefinition(_) => {} // Fragment definitions validated when used
223            ast::Operation::Introspection(_) => {}      // Introspection is always valid
224            ast::Operation::Handler(_) => {}            // Handler definitions validated separately
225        }
226        ctx.pop_path();
227    }
228
229    ctx.into_result()
230}
231
232/// Validate a query operation
233fn validate_query<S: SchemaProvider>(query: &Query, ctx: &mut ValidationContext<'_, S>) {
234    if let Some(name) = &query.name {
235        ctx.push_path(name);
236    }
237
238    // Validate variable definitions
239    validate_variable_definitions(&query.variable_definitions, ctx);
240
241    // Validate selection set
242    for selection in &query.selection_set {
243        match selection {
244            Selection::Field(field) => {
245                validate_field(field, None, ctx);
246            }
247            Selection::InlineFragment(inline) => {
248                validate_inline_fragment(inline, None, ctx);
249            }
250            Selection::FragmentSpread(fragment_name) => {
251                // Fragment spreads at query root are invalid — their fields would be
252                // interpreted as collection names by the planner/executor.
253                ctx.add_error(
254                    ErrorCode::InvalidArgument,
255                    format!(
256                        "Fragment spread '{}' is not allowed at query root; \
257                         use it inside a collection selection set instead",
258                        fragment_name
259                    ),
260                );
261            }
262            Selection::ComputedField(_) => {}
263        }
264    }
265
266    if query.name.is_some() {
267        ctx.pop_path();
268    }
269}
270
271/// Validate a mutation operation
272fn validate_mutation<S: SchemaProvider>(mutation: &Mutation, ctx: &mut ValidationContext<'_, S>) {
273    if let Some(name) = &mutation.name {
274        ctx.push_path(name);
275    }
276
277    // Validate variable definitions
278    validate_variable_definitions(&mutation.variable_definitions, ctx);
279
280    // Validate each mutation operation
281    for (i, op) in mutation.operations.iter().enumerate() {
282        ctx.push_path(&format!("mutation[{}]", i));
283        validate_mutation_operation(op, ctx);
284        ctx.pop_path();
285    }
286
287    if mutation.name.is_some() {
288        ctx.pop_path();
289    }
290}
291
292/// Validate an inline fragment
293///
294/// `expected_collection` is `Some(name)` when the fragment appears inside a
295/// collection selection set — the type condition must match the enclosing
296/// collection. At root level (query/subscription) it is `None`.
297fn validate_inline_fragment<S: SchemaProvider>(
298    inline: &ast::InlineFragment,
299    expected_collection: Option<&str>,
300    ctx: &mut ValidationContext<'_, S>,
301) {
302    // When nested inside a collection, the type condition must match it
303    if let Some(enc) = expected_collection {
304        if inline.type_condition != enc {
305            ctx.add_error(
306                ErrorCode::TypeMismatch,
307                format!(
308                    "Inline fragment on '{}' cannot appear inside '{}'",
309                    inline.type_condition, enc
310                ),
311            );
312            return;
313        }
314    }
315
316    // Check if type condition refers to a valid collection
317    if !ctx.schema.collection_exists(&inline.type_condition) {
318        ctx.add_error(
319            ErrorCode::UnknownCollection,
320            format!(
321                "Unknown collection '{}' in inline fragment",
322                inline.type_condition
323            ),
324        );
325        return;
326    }
327
328    // Validate inner selection set using the type condition as context
329    if let Some(collection) = ctx.schema.get_collection(&inline.type_condition) {
330        validate_selection_set(&inline.selection_set, collection, ctx);
331    }
332}
333
334/// Validate a subscription operation
335fn validate_subscription<S: SchemaProvider>(
336    sub: &Subscription,
337    ctx: &mut ValidationContext<'_, S>,
338) {
339    if let Some(name) = &sub.name {
340        ctx.push_path(name);
341    }
342
343    // Validate variable definitions
344    validate_variable_definitions(&sub.variable_definitions, ctx);
345
346    // Validate selection set
347    for selection in &sub.selection_set {
348        match selection {
349            Selection::Field(field) => {
350                validate_field(field, None, ctx);
351            }
352            Selection::InlineFragment(inline) => {
353                validate_inline_fragment(inline, None, ctx);
354            }
355            Selection::FragmentSpread(fragment_name) => {
356                if let Some(fragment) = ctx.fragments.get(fragment_name) {
357                    validate_fragment_spread(fragment_name, &fragment.type_condition, ctx);
358                } else {
359                    ctx.add_error(
360                        ErrorCode::UnknownFragment,
361                        format!("Fragment '{}' is not defined", fragment_name),
362                    );
363                }
364            }
365            Selection::ComputedField(_) => {}
366        }
367    }
368
369    if sub.name.is_some() {
370        ctx.pop_path();
371    }
372}
373
374/// Validate variable definitions
375fn validate_variable_definitions<S: SchemaProvider>(
376    definitions: &[ast::VariableDefinition],
377    ctx: &mut ValidationContext<'_, S>,
378) {
379    for def in definitions {
380        let var_name = &def.name;
381
382        // Check if required variable is provided
383        if def.var_type.is_required && def.default_value.is_none() {
384            if !ctx.variables.contains_key(var_name) {
385                ctx.add_error(
386                    ErrorCode::MissingRequiredVariable,
387                    format!("Required variable '{}' is not provided", var_name),
388                );
389            }
390        }
391    }
392}
393
394/// Validate a field selection
395///
396/// * `parent_collection` - If Some, this field is within a collection and we check field types.
397///                         If None, this is a top-level field that should be a collection reference.
398fn validate_field<S: SchemaProvider>(
399    field: &Field,
400    parent_collection: Option<&Collection>,
401    ctx: &mut ValidationContext<'_, S>,
402) {
403    let field_name = field.alias.as_ref().unwrap_or(&field.name);
404    ctx.push_path(field_name);
405
406    match parent_collection {
407        None => {
408            // Top-level field - always validate collection existence (even for leaf fields)
409            let collection_name = &field.name;
410            if !ctx.schema.collection_exists(collection_name) {
411                ctx.add_error(
412                    ErrorCode::UnknownCollection,
413                    format!("Collection '{}' does not exist", collection_name),
414                );
415            } else if field.selection_set.is_empty() {
416                ctx.add_error(
417                    ErrorCode::InvalidInput,
418                    format!("Collection '{}' requires a selection set", collection_name),
419                );
420            } else if let Some(collection) = ctx.schema.get_collection(collection_name) {
421                // Validate nested fields against collection schema
422                validate_selection_set(&field.selection_set, collection, ctx);
423
424                // Validate filter if present
425                for arg in &field.arguments {
426                    if arg.name == "where" || arg.name == "filter" {
427                        report_unknown_filter_ops(&arg.value, ctx);
428                        if let Some(filter) = extract_filter_from_value(&arg.value) {
429                            validate_filter(&filter, collection, ctx);
430                        }
431                    }
432                }
433            }
434        }
435        Some(collection) => {
436            // Nested field within a collection - only check selection set if non-empty
437            if !field.selection_set.is_empty() {
438                if let Some(field_def) = collection.fields.get(&field.name) {
439                    match &field_def.field_type {
440                        FieldType::Nested(nested_schema) => {
441                            // Validate selection set against nested schema
442                            validate_nested_selection_set(&field.selection_set, nested_schema, ctx);
443                        }
444                        FieldType::Object | FieldType::Any => {
445                            // Object/Any type - selection set is valid but we can't validate fields
446                            // (no schema to validate against - schemaless/dynamic data)
447                        }
448                        FieldType::Scalar(ScalarType::Object)
449                        | FieldType::Scalar(ScalarType::Any) => {
450                            // Scalar Object/Any - also allow selection sets without validation
451                        }
452                        _ => {
453                            // Non-object type with selection set - this is an error
454                            ctx.add_error(
455                                ErrorCode::TypeMismatch,
456                                format!(
457                                    "Field '{}' is not an object type but has a selection set",
458                                    field.name
459                                ),
460                            );
461                        }
462                    }
463                }
464                // If field doesn't exist, the error is already reported by validate_selection_set
465            }
466        }
467    }
468
469    ctx.pop_path();
470}
471
472/// Validate selection set against collection schema
473fn validate_selection_set<S: SchemaProvider>(
474    selections: &[Selection],
475    collection: &Collection,
476    ctx: &mut ValidationContext<'_, S>,
477) {
478    let mut aliases_seen: HashMap<String, bool> = HashMap::new();
479
480    for selection in selections {
481        // Validate based on selection type
482        match selection {
483            Selection::Field(field) => {
484                let display_name = field.alias.as_ref().unwrap_or(&field.name);
485
486                // Check for duplicate aliases
487                if aliases_seen.contains_key(display_name) {
488                    ctx.add_error(
489                        ErrorCode::DuplicateAlias,
490                        format!("Duplicate field/alias '{}' in selection", display_name),
491                    );
492                }
493                aliases_seen.insert(display_name.clone(), true);
494
495                // Check if field exists in collection schema
496                let field_name = &field.name;
497                if !field_name.starts_with("__") && field_name != "id" {
498                    if !collection.fields.contains_key(field_name) {
499                        ctx.add_error(
500                            ErrorCode::UnknownField,
501                            format!(
502                                "Field '{}' does not exist in collection '{}'",
503                                field_name, collection.name
504                            ),
505                        );
506                    }
507                }
508
509                // Recursively validate nested selection with parent context
510                validate_field(field, Some(collection), ctx);
511            }
512            Selection::InlineFragment(inline) => {
513                validate_inline_fragment(inline, Some(&collection.name), ctx);
514            }
515            Selection::FragmentSpread(fragment_name) => {
516                validate_fragment_spread(fragment_name, &collection.name, ctx);
517            }
518            Selection::ComputedField(cf) => {
519                let display_name = &cf.alias;
520                if aliases_seen.contains_key(display_name) {
521                    ctx.add_error(
522                        ErrorCode::DuplicateAlias,
523                        format!("Duplicate field/alias '{}' in selection", display_name),
524                    );
525                }
526                aliases_seen.insert(display_name.clone(), true);
527            }
528        }
529    }
530}
531
532/// Validate selection set against a nested object schema
533fn validate_nested_selection_set<S: SchemaProvider>(
534    selections: &[Selection],
535    nested_schema: &HashMap<String, crate::types::FieldDefinition>,
536    ctx: &mut ValidationContext<'_, S>,
537) {
538    let mut aliases_seen: HashMap<String, bool> = HashMap::new();
539
540    for selection in selections {
541        match selection {
542            Selection::Field(field) => {
543                let display_name = field.alias.as_ref().unwrap_or(&field.name);
544
545                // Check for duplicate aliases
546                if aliases_seen.contains_key(display_name) {
547                    ctx.add_error(
548                        ErrorCode::DuplicateAlias,
549                        format!("Duplicate field/alias '{}' in selection", display_name),
550                    );
551                }
552                aliases_seen.insert(display_name.clone(), true);
553
554                // Check if field exists in nested schema
555                let field_name = &field.name;
556                if !field_name.starts_with("__") {
557                    if !nested_schema.contains_key(field_name) {
558                        ctx.add_error(
559                            ErrorCode::UnknownField,
560                            format!("Field '{}' does not exist in nested object", field_name),
561                        );
562                    } else if !field.selection_set.is_empty() {
563                        // Validate further nesting
564                        if let Some(field_def) = nested_schema.get(field_name) {
565                            match &field_def.field_type {
566                                FieldType::Nested(deeper_schema) => {
567                                    ctx.push_path(field_name);
568                                    validate_nested_selection_set(
569                                        &field.selection_set,
570                                        deeper_schema,
571                                        ctx,
572                                    );
573                                    ctx.pop_path();
574                                }
575                                FieldType::Object | FieldType::Any => {
576                                    // Object/Any type - allow selection but can't validate
577                                }
578                                FieldType::Scalar(ScalarType::Object)
579                                | FieldType::Scalar(ScalarType::Any) => {
580                                    // Scalar Object/Any - allow selection but can't validate
581                                }
582                                _ => {
583                                    ctx.add_error(
584                                        ErrorCode::TypeMismatch,
585                                        format!(
586                                            "Field '{}' is not an object type but has a selection set",
587                                            field_name
588                                        ),
589                                    );
590                                }
591                            }
592                        }
593                    }
594                }
595            }
596            Selection::InlineFragment(_) | Selection::FragmentSpread(_) => {
597                // Fragments in nested objects not supported for now
598                ctx.add_error(
599                    ErrorCode::InvalidInput,
600                    "Fragments are not supported in nested object selections".to_string(),
601                );
602            }
603            Selection::ComputedField(cf) => {
604                let display_name = &cf.alias;
605                if aliases_seen.contains_key(display_name) {
606                    ctx.add_error(
607                        ErrorCode::DuplicateAlias,
608                        format!("Duplicate field/alias '{}' in selection", display_name),
609                    );
610                }
611                aliases_seen.insert(display_name.clone(), true);
612            }
613        }
614    }
615}
616
617/// Validate a fragment spread
618fn validate_fragment_spread<S: SchemaProvider>(
619    fragment_name: &str,
620    expected_collection: &str,
621    ctx: &mut ValidationContext<'_, S>,
622) {
623    // 0. Check for cycles
624    if ctx.validating_fragments.contains(fragment_name) {
625        ctx.add_error(
626            ErrorCode::InvalidInput,
627            format!("Fragment '{}' contains a cyclic reference", fragment_name),
628        );
629        return;
630    }
631
632    // 1. Check if fragment exists
633    let fragment_opt = ctx.fragments.get(fragment_name).cloned();
634
635    if let Some(fragment) = fragment_opt {
636        // 2. Check type condition matches
637        if fragment.type_condition != expected_collection {
638            ctx.add_error(
639                ErrorCode::TypeMismatch,
640                format!(
641                    "Fragment '{}' is defined on '{}' but used on '{}'",
642                    fragment_name, fragment.type_condition, expected_collection
643                ),
644            );
645            return;
646        }
647
648        // 3. Validate fragment's selection set against the collection
649        if let Some(collection) = ctx.schema.get_collection(expected_collection) {
650            ctx.push_path(&format!("...{}", fragment_name));
651            ctx.validating_fragments.insert(fragment_name.to_string());
652            validate_selection_set(&fragment.selection_set, collection, ctx);
653            ctx.validating_fragments.remove(fragment_name);
654            ctx.pop_path();
655        } else {
656            ctx.add_error(
657                ErrorCode::UnknownCollection,
658                format!(
659                    "Fragment '{}' references unknown collection '{}'",
660                    fragment_name, expected_collection
661                ),
662            );
663        }
664    } else {
665        ctx.add_error(
666            ErrorCode::UnknownFragment,
667            format!("Fragment '{}' is not defined", fragment_name),
668        );
669    }
670}
671
672/// Validate a mutation operation
673fn validate_mutation_operation<S: SchemaProvider>(
674    op: &ast::MutationOperation,
675    ctx: &mut ValidationContext<'_, S>,
676) {
677    match &op.operation {
678        ast::MutationOp::Insert { collection, data } => {
679            if let Some(col_def) = ctx.schema.get_collection(collection) {
680                validate_object_against_schema(data, col_def, ctx);
681            } else {
682                ctx.add_error(
683                    ErrorCode::UnknownCollection,
684                    format!("Collection '{}' does not exist", collection),
685                );
686            }
687        }
688        ast::MutationOp::InsertMany { collection, data } => {
689            if let Some(col_def) = ctx.schema.get_collection(collection) {
690                if let ast::Value::Array(items) = data {
691                    for item in items {
692                        validate_object_against_schema(item, col_def, ctx);
693                    }
694                }
695            } else {
696                ctx.add_error(
697                    ErrorCode::UnknownCollection,
698                    format!("Collection '{}' does not exist", collection),
699                );
700            }
701        }
702        ast::MutationOp::Update {
703            collection, data, ..
704        }
705        | ast::MutationOp::Upsert {
706            collection, data, ..
707        } => {
708            if let Some(col_def) = ctx.schema.get_collection(collection) {
709                // Validate partial object for updates (skipping required checks)
710                validate_partial_object(data, col_def, ctx);
711            } else {
712                ctx.add_error(
713                    ErrorCode::UnknownCollection,
714                    format!("Collection '{}' does not exist", collection),
715                );
716            }
717        }
718        ast::MutationOp::Delete { collection, .. } => {
719            if !ctx.schema.collection_exists(collection) {
720                ctx.add_error(
721                    ErrorCode::UnknownCollection,
722                    format!("Collection '{}' does not exist", collection),
723                );
724            }
725        }
726        ast::MutationOp::EnqueueJob { .. } => {}
727        ast::MutationOp::EnqueueJobs { .. } => {}
728        ast::MutationOp::Import { collection, data } => {
729            if let Some(col_def) = ctx.schema.get_collection(collection) {
730                for item in data {
731                    validate_object_against_schema(item, col_def, ctx);
732                }
733            }
734        }
735        ast::MutationOp::Export { .. } => {}
736        ast::MutationOp::Transaction { operations } => {
737            for (i, inner_op) in operations.iter().enumerate() {
738                ctx.push_path(&format!("tx[{}]", i));
739                validate_mutation_operation(inner_op, ctx);
740                ctx.pop_path();
741            }
742        }
743    }
744}
745
746/// Validate an object value against a collection schema
747fn validate_object_against_schema<S: SchemaProvider>(
748    value: &Value,
749    collection: &Collection,
750    ctx: &mut ValidationContext<'_, S>,
751) {
752    match value {
753        Value::Object(map) => {
754            // 1. Check provided fields validity
755            for (key, val) in map {
756                if let Some(field_def) = collection.fields.get(key) {
757                    // Check if null is allowed
758                    if matches!(val, Value::Null) && !field_def.nullable {
759                        ctx.add_error(
760                            ErrorCode::InvalidInput,
761                            format!("Field '{}' cannot be null", key),
762                        );
763                    } else if !matches!(val, Value::Null)
764                        && !validate_value_against_type(val, &field_def.field_type)
765                    {
766                        ctx.add_error(
767                            ErrorCode::TypeMismatch,
768                            format!(
769                                "Type mismatch for field '{}': expected {:?}, got {:?}",
770                                key,
771                                field_def.field_type,
772                                value_type_name(val)
773                            ),
774                        );
775                    }
776                } else if key != "id" {
777                    ctx.add_error(
778                        ErrorCode::UnknownField,
779                        format!(
780                            "Field '{}' not defined in collection '{}'",
781                            key, collection.name
782                        ),
783                    );
784                }
785            }
786
787            // 2. Check for missing required fields
788            for (name, def) in &collection.fields {
789                if !def.nullable && !map.contains_key(name) {
790                    ctx.add_error(
791                        ErrorCode::InvalidInput,
792                        format!("Missing required field '{}'", name),
793                    );
794                }
795            }
796        }
797        _ => {
798            ctx.add_error(
799                ErrorCode::TypeMismatch,
800                format!(
801                    "Expected object for collection '{}', got {:?}",
802                    collection.name,
803                    value_type_name(value)
804                ),
805            );
806        }
807    }
808}
809
810/// Validate a partial object (field subset) against a collection schema
811/// Used for Update operations where missing required fields are allowed
812fn validate_partial_object<S: SchemaProvider>(
813    value: &Value,
814    collection: &Collection,
815    ctx: &mut ValidationContext<'_, S>,
816) {
817    match value {
818        Value::Object(map) => {
819            // Check provided fields validity
820            for (key, val) in map {
821                if let Some(field_def) = collection.fields.get(key) {
822                    // Check if null is allowed
823                    if matches!(val, Value::Null) && !field_def.nullable {
824                        ctx.add_error(
825                            ErrorCode::InvalidInput,
826                            format!("Field '{}' cannot be null", key),
827                        );
828                    } else if !matches!(val, Value::Null)
829                        && !validate_value_against_type(val, &field_def.field_type)
830                    {
831                        ctx.add_error(
832                            ErrorCode::TypeMismatch,
833                            format!(
834                                "Type mismatch for field '{}': expected {:?}, got {:?}",
835                                key,
836                                field_def.field_type,
837                                value_type_name(val)
838                            ),
839                        );
840                    }
841                } else if key != "id" {
842                    ctx.add_error(
843                        ErrorCode::UnknownField,
844                        format!(
845                            "Field '{}' not defined in collection '{}'",
846                            key, collection.name
847                        ),
848                    );
849                }
850            }
851        }
852        _ => {
853            ctx.add_error(
854                ErrorCode::TypeMismatch,
855                format!(
856                    "Expected object for collection '{}', got {:?}",
857                    collection.name,
858                    value_type_name(value)
859                ),
860            );
861        }
862    }
863}
864
865/// Recursively validate value against type
866fn validate_value_against_type(value: &Value, expected: &FieldType) -> bool {
867    use crate::types::ScalarType;
868    match (expected, value) {
869        // Handle Scalar Types
870        (FieldType::Scalar(ScalarType::Any), _) => true,
871        (_, Value::Null) => true,
872        (_, Value::Variable(_)) => true,
873
874        (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
875        (FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
876        (FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
877        (FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true,
878        (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
879        (FieldType::Scalar(ScalarType::Uuid), Value::String(s)) => uuid::Uuid::parse_str(s).is_ok(),
880
881        // Handle Array Types
882        (FieldType::Array(scalar_inner), Value::Array(items)) => {
883            // Check each item matches the scalar inner type
884            let inner_field_type = FieldType::Scalar(scalar_inner.clone());
885            items
886                .iter()
887                .all(|item| validate_value_against_type(item, &inner_field_type))
888        }
889
890        // Handle Objects
891        (FieldType::Object, Value::Object(_)) => true,
892        // Handle Nested Objects (deep validation)
893        (FieldType::Nested(schema), Value::Object(map)) => {
894            // 1. Check all provided fields exist in schema and match type
895            for (key, val) in map {
896                if let Some(def) = schema.get(key) {
897                    if matches!(val, Value::Null) {
898                        if !def.nullable {
899                            return false; // Null not allowed
900                        }
901                    } else if !validate_value_against_type(val, &def.field_type) {
902                        return false; // Type mismatch in nested field
903                    }
904                } else {
905                    return false; // Unknown field in nested object
906                }
907            }
908            // 2. Check all required fields are provided
909            for (key, def) in schema.iter() {
910                if !def.nullable && !map.contains_key(key) {
911                    return false; // Missing required nested field
912                }
913            }
914            true
915        }
916        (FieldType::Nested(_), _) => false, // Not an object
917
918        // Handle ScalarType::Object (e.g. inside Array<Object>)
919        (FieldType::Scalar(ScalarType::Object), Value::Object(_)) => true,
920        // Handle ScalarType::Array (e.g. inside Array<Array>)
921        (FieldType::Scalar(ScalarType::Array), Value::Array(_)) => true,
922
923        _ => false,
924    }
925}
926
927/// Validate a filter expression
928fn validate_filter<S: SchemaProvider>(
929    filter: &Filter,
930    collection: &Collection,
931    ctx: &mut ValidationContext<'_, S>,
932) {
933    match filter {
934        Filter::Eq(field, value)
935        | Filter::Ne(field, value)
936        | Filter::Gt(field, value)
937        | Filter::Gte(field, value)
938        | Filter::Lt(field, value)
939        | Filter::Lte(field, value) => {
940            validate_filter_field(field, value, collection, ctx);
941        }
942        Filter::In(field, value) | Filter::NotIn(field, value) => {
943            // In/NotIn expects an array value
944            if !matches!(value, Value::Array(_)) {
945                ctx.add_error(
946                    ErrorCode::TypeMismatch,
947                    format!("Filter 'in'/'notIn' on '{}' requires an array value", field),
948                );
949            }
950            validate_filter_field_exists(field, collection, ctx);
951        }
952        Filter::ContainsAny(field, value) | Filter::ContainsAll(field, value) => {
953            if !matches!(value, Value::Array(_) | Value::Variable(_)) {
954                ctx.add_error(
955                    ErrorCode::TypeMismatch,
956                    format!(
957                        "containsAny/containsAll on field '{}' expects an array",
958                        field
959                    ),
960                );
961            }
962            validate_filter_field_exists(field, collection, ctx);
963        }
964        Filter::Contains(field, _)
965        | Filter::StartsWith(field, _)
966        | Filter::EndsWith(field, _)
967        | Filter::Matches(field, _) => {
968            // String operations - check field is a string type
969            if let Some(field_def) = collection.fields.get(field) {
970                if field_def.field_type != FieldType::SCALAR_STRING {
971                    ctx.add_error(
972                        ErrorCode::InvalidFilterOperator,
973                        format!("String operator on non-string field '{}'", field),
974                    );
975                }
976            } else {
977                validate_filter_field_exists(field, collection, ctx);
978            }
979        }
980        Filter::IsNull(field) | Filter::IsNotNull(field) => {
981            validate_filter_field_exists(field, collection, ctx);
982        }
983        Filter::And(filters) | Filter::Or(filters) => {
984            for f in filters {
985                validate_filter(f, collection, ctx);
986            }
987        }
988        Filter::Not(inner) => {
989            validate_filter(inner, collection, ctx);
990        }
991    }
992}
993
994/// Validate a filter field exists
995fn validate_filter_field_exists<S: SchemaProvider>(
996    field: &str,
997    collection: &Collection,
998    ctx: &mut ValidationContext<'_, S>,
999) {
1000    if field != "id" && !collection.fields.contains_key(field) {
1001        ctx.add_error(
1002            ErrorCode::UnknownField,
1003            format!(
1004                "Filter field '{}' does not exist in collection '{}'",
1005                field, collection.name
1006            ),
1007        );
1008    }
1009}
1010
1011/// Validate a filter field with value type checking
1012fn validate_filter_field<S: SchemaProvider>(
1013    field: &str,
1014    value: &Value,
1015    collection: &Collection,
1016    ctx: &mut ValidationContext<'_, S>,
1017) {
1018    if field == "id" {
1019        return; // id field always exists
1020    }
1021
1022    if let Some(field_def) = collection.fields.get(field) {
1023        // Check type compatibility
1024        if !is_type_compatible(&field_def.field_type, value) {
1025            ctx.add_error(
1026                ErrorCode::TypeMismatch,
1027                format!(
1028                    "Type mismatch: field '{}' expects {:?}, got {:?}",
1029                    field,
1030                    field_def.field_type,
1031                    value_type_name(value)
1032                ),
1033            );
1034        }
1035    } else {
1036        ctx.add_error(
1037            ErrorCode::UnknownField,
1038            format!(
1039                "Filter field '{}' does not exist in collection '{}'",
1040                field, collection.name
1041            ),
1042        );
1043    }
1044}
1045
1046/// Check if a value is compatible with a field type
1047fn is_type_compatible(field_type: &FieldType, value: &Value) -> bool {
1048    match (field_type, value) {
1049        (_, Value::Null) => true,        // Null is compatible with any type
1050        (_, Value::Variable(_)) => true, // Variables are resolved later
1051        (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
1052        (FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
1053        (FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
1054        (FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true, // Int can be used where Float expected
1055        (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
1056        (FieldType::Array(_), Value::Array(_)) => true,
1057        (FieldType::Object, Value::Object(_)) => true,
1058        (FieldType::Nested(_), Value::Object(_)) => true, // Structural compatibility for filters (deep check might be too expensive/complex here)
1059        (FieldType::Scalar(ScalarType::Object), Value::Object(_)) => true, // Support for ScalarType::Object
1060        (FieldType::Scalar(ScalarType::Array), Value::Array(_)) => true, // Support for ScalarType::Array
1061        (FieldType::Scalar(ScalarType::Any), _) => true, // Any type accepts all values
1062        (FieldType::Scalar(ScalarType::Uuid), Value::String(s)) => uuid::Uuid::parse_str(s).is_ok(), // Check string is valid UUID
1063        _ => false,
1064    }
1065}
1066
1067/// Get a human-readable type name for a value
1068fn value_type_name(value: &Value) -> &'static str {
1069    match value {
1070        Value::Null => "null",
1071        Value::Boolean(_) => "boolean",
1072        Value::Int(_) => "int",
1073        Value::Float(_) => "float",
1074        Value::String(_) => "string",
1075        Value::Array(_) => "array",
1076        Value::Object(_) => "object",
1077        Value::Variable(_) => "variable",
1078        Value::Enum(_) => "enum",
1079    }
1080}
1081
1082/// Report validation errors for unrecognised filter operators so users get a
1083/// clear message for typos instead of silently-dropped conditions.
1084fn report_unknown_filter_ops<S: SchemaProvider>(value: &Value, ctx: &mut ValidationContext<'_, S>) {
1085    const KNOWN_OPS: &[&str] = &[
1086        "eq",
1087        "ne",
1088        "gt",
1089        "gte",
1090        "lt",
1091        "lte",
1092        "in",
1093        "nin",
1094        "contains",
1095        "startsWith",
1096        "endsWith",
1097        "matches",
1098        "isNull",
1099        "isNotNull",
1100    ];
1101    if let Value::Object(map) = value {
1102        for (key, val) in map {
1103            match key.as_str() {
1104                "and" | "or" => {
1105                    if let Value::Array(arr) = val {
1106                        arr.iter().for_each(|v| report_unknown_filter_ops(v, ctx));
1107                    }
1108                }
1109                "not" => report_unknown_filter_ops(val, ctx),
1110                field => {
1111                    if let Value::Object(ops) = val {
1112                        // Only flag unknown keys when at least one recognized operator
1113                        // is present — otherwise this is a nested-field reference and
1114                        // we should recurse rather than flag every key as an unknown op.
1115                        let has_known_op = ops.keys().any(|k| KNOWN_OPS.contains(&k.as_str()));
1116                        if has_known_op {
1117                            for op in ops.keys() {
1118                                if !KNOWN_OPS.contains(&op.as_str()) {
1119                                    ctx.add_error(
1120                                        ErrorCode::InvalidArgument,
1121                                        format!(
1122                                            "Unknown filter operator '{}' on field '{}'",
1123                                            op, field
1124                                        ),
1125                                    );
1126                                }
1127                            }
1128                        } else {
1129                            // Nested field reference — recurse into it
1130                            report_unknown_filter_ops(val, ctx);
1131                        }
1132                    }
1133                }
1134            }
1135        }
1136    }
1137}
1138
1139/// Extract a Filter from an AST Value (for parsing where arguments)
1140fn extract_filter_from_value(value: &Value) -> Option<Filter> {
1141    match value {
1142        Value::Object(map) => {
1143            let mut filters = Vec::new();
1144
1145            for (key, val) in map {
1146                match key.as_str() {
1147                    "and" => {
1148                        if let Value::Array(arr) = val {
1149                            let sub_filters: Vec<Filter> =
1150                                arr.iter().filter_map(extract_filter_from_value).collect();
1151                            if !sub_filters.is_empty() {
1152                                filters.push(Filter::And(sub_filters));
1153                            }
1154                        }
1155                    }
1156                    "or" => {
1157                        if let Value::Array(arr) = val {
1158                            let sub_filters: Vec<Filter> =
1159                                arr.iter().filter_map(extract_filter_from_value).collect();
1160                            if !sub_filters.is_empty() {
1161                                filters.push(Filter::Or(sub_filters));
1162                            }
1163                        }
1164                    }
1165                    "not" => {
1166                        if let Some(inner) = extract_filter_from_value(val) {
1167                            filters.push(Filter::Not(Box::new(inner)));
1168                        }
1169                    }
1170                    field => {
1171                        // Field-level filter: { field: { eq: value } }
1172                        if let Value::Object(ops) = val {
1173                            for (op, op_val) in ops {
1174                                let filter = match op.as_str() {
1175                                    "eq" => Some(Filter::Eq(field.to_string(), op_val.clone())),
1176                                    "ne" => Some(Filter::Ne(field.to_string(), op_val.clone())),
1177                                    "gt" => Some(Filter::Gt(field.to_string(), op_val.clone())),
1178                                    "gte" => Some(Filter::Gte(field.to_string(), op_val.clone())),
1179                                    "lt" => Some(Filter::Lt(field.to_string(), op_val.clone())),
1180                                    "lte" => Some(Filter::Lte(field.to_string(), op_val.clone())),
1181                                    "in" => Some(Filter::In(field.to_string(), op_val.clone())),
1182                                    "nin" => Some(Filter::NotIn(field.to_string(), op_val.clone())),
1183                                    "contains" => {
1184                                        Some(Filter::Contains(field.to_string(), op_val.clone()))
1185                                    }
1186                                    "startsWith" => {
1187                                        Some(Filter::StartsWith(field.to_string(), op_val.clone()))
1188                                    }
1189                                    "endsWith" => {
1190                                        Some(Filter::EndsWith(field.to_string(), op_val.clone()))
1191                                    }
1192                                    "matches" => {
1193                                        Some(Filter::Matches(field.to_string(), op_val.clone()))
1194                                    }
1195                                    "isNull" => Some(Filter::IsNull(field.to_string())),
1196                                    "isNotNull" => Some(Filter::IsNotNull(field.to_string())),
1197                                    _ => None,
1198                                };
1199                                if let Some(f) = filter {
1200                                    filters.push(f);
1201                                }
1202                            }
1203                        }
1204                    }
1205                }
1206            }
1207
1208            match filters.len() {
1209                0 => None,
1210                1 => Some(filters.remove(0)),
1211                _ => Some(Filter::And(filters)),
1212            }
1213        }
1214        _ => None,
1215    }
1216}
1217
1218/// Resolve variables in a document, replacing Value::Variable with actual values
1219pub fn resolve_variables(
1220    doc: &mut Document,
1221    variables: &HashMap<String, ast::Value>,
1222) -> Result<(), ValidationError> {
1223    for op in &mut doc.operations {
1224        match op {
1225            ast::Operation::Query(query) => {
1226                resolve_in_fields(&mut query.selection_set, variables)?;
1227            }
1228            ast::Operation::Mutation(mutation) => {
1229                for mut_op in &mut mutation.operations {
1230                    resolve_in_mutation_op(mut_op, variables)?;
1231                }
1232            }
1233            ast::Operation::Subscription(sub) => {
1234                resolve_in_fields(&mut sub.selection_set, variables)?;
1235            }
1236            ast::Operation::Schema(_) => {}
1237            ast::Operation::Migration(_) => {}
1238            ast::Operation::FragmentDefinition(fragment) => {
1239                // Resolve variables inside fragment field arguments so they are
1240                // substituted before the fragment is expanded at execution time.
1241                resolve_in_fields(&mut fragment.selection_set, variables)?;
1242            }
1243            ast::Operation::Introspection(_) => {} // No variables in introspection
1244            ast::Operation::Handler(_) => {}       // Handlers don't have variable resolution
1245        }
1246    }
1247    Ok(())
1248}
1249
1250fn resolve_in_fields(
1251    fields: &mut [Selection],
1252    variables: &HashMap<String, ast::Value>,
1253) -> Result<(), ValidationError> {
1254    for selection in fields {
1255        match selection {
1256            Selection::Field(field) => {
1257                // Resolve variables in arguments
1258                for arg in &mut field.arguments {
1259                    resolve_in_value(&mut arg.value, variables)?;
1260                }
1261                // Recursively resolve in nested selection
1262                resolve_in_fields(&mut field.selection_set, variables)?;
1263            }
1264            Selection::InlineFragment(inline) => {
1265                resolve_in_fields(&mut inline.selection_set, variables)?;
1266            }
1267            Selection::FragmentSpread(_) => {
1268                // Nothing to resolve in fragment spread itself (name)
1269            }
1270            Selection::ComputedField(cf) => {
1271                resolve_in_computed_expression(&mut cf.expression, variables)?;
1272            }
1273        }
1274    }
1275    Ok(())
1276}
1277
1278fn resolve_in_computed_expression(
1279    expr: &mut ast::ComputedExpression,
1280    variables: &HashMap<String, ast::Value>,
1281) -> Result<(), ValidationError> {
1282    match expr {
1283        ast::ComputedExpression::FunctionCall { args, .. } => {
1284            for arg in args {
1285                resolve_in_expression(arg, variables)?;
1286            }
1287        }
1288        ast::ComputedExpression::PipeExpression { base, operations } => {
1289            resolve_in_expression(base, variables)?;
1290            for op in operations {
1291                for arg in &mut op.args {
1292                    resolve_in_expression(arg, variables)?;
1293                }
1294            }
1295        }
1296        _ => {}
1297    }
1298    Ok(())
1299}
1300
1301fn resolve_in_expression(
1302    expr: &mut ast::Expression,
1303    variables: &HashMap<String, ast::Value>,
1304) -> Result<(), ValidationError> {
1305    match expr {
1306        ast::Expression::Literal(v) => resolve_in_value(v, variables)?,
1307        ast::Expression::Variable(name) => {
1308            if let Some(val) = variables.get(name) {
1309                *expr = ast::Expression::Literal(val.clone());
1310            }
1311        }
1312        ast::Expression::FunctionCall { args, .. } => {
1313            for arg in args {
1314                resolve_in_expression(arg, variables)?;
1315            }
1316        }
1317        ast::Expression::Binary { left, right, .. } => {
1318            resolve_in_expression(left, variables)?;
1319            resolve_in_expression(right, variables)?;
1320        }
1321        ast::Expression::Unary { expr, .. } => {
1322            resolve_in_expression(expr, variables)?;
1323        }
1324        ast::Expression::Ternary {
1325            condition,
1326            then_expr,
1327            else_expr,
1328        } => {
1329            resolve_in_expression(condition, variables)?;
1330            resolve_in_expression(then_expr, variables)?;
1331            resolve_in_expression(else_expr, variables)?;
1332        }
1333        _ => {}
1334    }
1335    Ok(())
1336}
1337
1338fn resolve_in_mutation_op(
1339    op: &mut ast::MutationOperation,
1340    variables: &HashMap<String, ast::Value>,
1341) -> Result<(), ValidationError> {
1342    match &mut op.operation {
1343        ast::MutationOp::Insert { data, .. }
1344        | ast::MutationOp::Update { data, .. }
1345        | ast::MutationOp::Upsert { data, .. } => {
1346            resolve_in_value(data, variables)?;
1347        }
1348        ast::MutationOp::InsertMany { data, .. } => {
1349            resolve_in_value(data, variables)?;
1350        }
1351        ast::MutationOp::Delete { .. } => {}
1352        ast::MutationOp::EnqueueJob { payload, .. } => {
1353            resolve_in_value(payload, variables)?;
1354        }
1355        ast::MutationOp::EnqueueJobs { payloads, .. } => {
1356            for p in payloads {
1357                resolve_in_value(p, variables)?;
1358            }
1359        }
1360        ast::MutationOp::Import { data, .. } => {
1361            for item in data {
1362                resolve_in_value(item, variables)?;
1363            }
1364        }
1365        ast::MutationOp::Export { .. } => {}
1366        ast::MutationOp::Transaction { operations } => {
1367            for inner in operations {
1368                resolve_in_mutation_op(inner, variables)?;
1369            }
1370        }
1371    }
1372    resolve_in_fields(&mut op.selection_set, variables)
1373}
1374
1375fn resolve_in_value(
1376    value: &mut Value,
1377    variables: &HashMap<String, ast::Value>,
1378) -> Result<(), ValidationError> {
1379    match value {
1380        Value::Variable(name) => {
1381            if let Some(resolved) = variables.get(name) {
1382                *value = resolved.clone();
1383            } else {
1384                return Err(ValidationError::new(
1385                    ErrorCode::MissingRequiredVariable,
1386                    format!("Variable '{}' is not provided", name),
1387                ));
1388            }
1389        }
1390        Value::Array(items) => {
1391            for item in items {
1392                resolve_in_value(item, variables)?;
1393            }
1394        }
1395        Value::Object(map) => {
1396            for v in map.values_mut() {
1397                resolve_in_value(v, variables)?;
1398            }
1399        }
1400        _ => {}
1401    }
1402    Ok(())
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407    use super::*;
1408    use crate::types::FieldDefinition;
1409
1410    fn create_test_schema() -> InMemorySchema {
1411        let mut schema = InMemorySchema::new();
1412
1413        let mut users_fields = HashMap::new();
1414        users_fields.insert(
1415            "name".to_string(),
1416            FieldDefinition {
1417                field_type: FieldType::SCALAR_STRING,
1418                unique: false,
1419                indexed: false,
1420                nullable: false,
1421                ..Default::default()
1422            },
1423        );
1424        users_fields.insert(
1425            "email".to_string(),
1426            FieldDefinition {
1427                field_type: FieldType::SCALAR_STRING,
1428                unique: true,
1429                indexed: true,
1430                nullable: false,
1431                ..Default::default()
1432            },
1433        );
1434        users_fields.insert(
1435            "age".to_string(),
1436            FieldDefinition {
1437                field_type: FieldType::SCALAR_INT,
1438                unique: false,
1439                indexed: false,
1440                nullable: false,
1441                ..Default::default()
1442            },
1443        );
1444        users_fields.insert(
1445            "active".to_string(),
1446            FieldDefinition {
1447                field_type: FieldType::SCALAR_BOOL,
1448                unique: false,
1449                indexed: false,
1450                nullable: false,
1451                ..Default::default()
1452            },
1453        );
1454
1455        schema.add_collection(Collection {
1456            name: "users".to_string(),
1457            fields: users_fields,
1458        });
1459
1460        schema
1461    }
1462
1463    #[test]
1464    fn test_validate_unknown_collection() {
1465        let schema = create_test_schema();
1466        let doc = Document {
1467            operations: vec![ast::Operation::Query(Query {
1468                name: None,
1469                variable_definitions: vec![],
1470                directives: vec![],
1471                selection_set: vec![Selection::Field(Field {
1472                    alias: None,
1473                    name: "nonexistent".to_string(),
1474                    arguments: vec![],
1475                    directives: vec![],
1476                    computed_expression: None,
1477                    selection_set: vec![Selection::Field(Field {
1478                        alias: None,
1479                        name: "id".to_string(),
1480                        arguments: vec![],
1481                        directives: vec![],
1482                        selection_set: vec![],
1483                        computed_expression: None,
1484                    })],
1485                })],
1486                variables_values: HashMap::new(),
1487            })],
1488        };
1489
1490        let result = validate_document(&doc, &schema, HashMap::new());
1491        assert!(result.is_err());
1492        let errors = result.unwrap_err();
1493        assert!(
1494            errors
1495                .iter()
1496                .any(|e| e.code == ErrorCode::UnknownCollection)
1497        );
1498    }
1499
1500    #[test]
1501    fn test_validate_unknown_field() {
1502        let schema = create_test_schema();
1503        let doc = Document {
1504            operations: vec![ast::Operation::Query(Query {
1505                name: None,
1506                variable_definitions: vec![],
1507                directives: vec![],
1508                selection_set: vec![Selection::Field(Field {
1509                    alias: None,
1510                    name: "users".to_string(),
1511                    arguments: vec![],
1512                    directives: vec![],
1513                    computed_expression: None,
1514                    selection_set: vec![Selection::Field(Field {
1515                        alias: None,
1516                        name: "nonexistent_field".to_string(),
1517                        arguments: vec![],
1518                        directives: vec![],
1519                        selection_set: vec![],
1520
1521                        computed_expression: None,
1522                    })],
1523                })],
1524                variables_values: HashMap::new(),
1525            })],
1526        };
1527
1528        let result = validate_document(&doc, &schema, HashMap::new());
1529        assert!(result.is_err());
1530        let errors = result.unwrap_err();
1531        assert!(errors.iter().any(|e| e.code == ErrorCode::UnknownField));
1532    }
1533
1534    #[test]
1535    fn test_validate_missing_required_variable() {
1536        let schema = create_test_schema();
1537        let doc = Document {
1538            operations: vec![ast::Operation::Query(Query {
1539                name: Some("GetUsers".to_string()),
1540                variable_definitions: vec![ast::VariableDefinition {
1541                    name: "minAge".to_string(),
1542                    var_type: ast::TypeAnnotation {
1543                        name: "Int".to_string(),
1544                        is_array: false,
1545                        is_required: true,
1546                    },
1547                    default_value: None,
1548                }],
1549                directives: vec![],
1550                selection_set: vec![],
1551                variables_values: HashMap::new(),
1552            })],
1553        };
1554
1555        // No variables provided
1556        let result = validate_document(&doc, &schema, HashMap::new());
1557        assert!(result.is_err());
1558        let errors = result.unwrap_err();
1559        assert!(
1560            errors
1561                .iter()
1562                .any(|e| e.code == ErrorCode::MissingRequiredVariable)
1563        );
1564    }
1565
1566    #[test]
1567    fn test_validate_valid_query() {
1568        let schema = create_test_schema();
1569        let doc = Document {
1570            operations: vec![ast::Operation::Query(Query {
1571                name: Some("GetUsers".to_string()),
1572                variable_definitions: vec![],
1573                directives: vec![],
1574                selection_set: vec![Selection::Field(Field {
1575                    alias: None,
1576                    name: "users".to_string(),
1577                    arguments: vec![],
1578                    directives: vec![],
1579                    computed_expression: None,
1580                    selection_set: vec![
1581                        Selection::Field(Field {
1582                            alias: None,
1583                            name: "id".to_string(),
1584                            arguments: vec![],
1585                            directives: vec![],
1586                            selection_set: vec![],
1587
1588                            computed_expression: None,
1589                        }),
1590                        Selection::Field(Field {
1591                            alias: None,
1592                            name: "name".to_string(),
1593                            arguments: vec![],
1594                            directives: vec![],
1595                            selection_set: vec![],
1596                            computed_expression: None,
1597                        }),
1598                        Selection::Field(Field {
1599                            alias: None,
1600                            name: "email".to_string(),
1601                            arguments: vec![],
1602                            directives: vec![],
1603                            selection_set: vec![],
1604                            computed_expression: None,
1605                        }),
1606                    ],
1607                })],
1608                variables_values: HashMap::new(),
1609            })],
1610        };
1611
1612        let result = validate_document(&doc, &schema, HashMap::new());
1613        assert!(result.is_ok());
1614    }
1615
1616    #[test]
1617    fn test_validate_filter_type_mismatch() {
1618        let schema = create_test_schema();
1619        let collection = schema.get_collection("users").unwrap();
1620        let mut ctx = ValidationContext::new(&schema);
1621
1622        // age is Int, but we're filtering with a string
1623        let filter = Filter::Eq("age".to_string(), Value::String("not a number".to_string()));
1624        validate_filter(&filter, collection, &mut ctx);
1625
1626        assert!(ctx.has_errors());
1627        assert!(ctx.errors.iter().any(|e| e.code == ErrorCode::TypeMismatch));
1628    }
1629
1630    #[test]
1631    fn test_validate_filter_string_operator_on_int() {
1632        let schema = create_test_schema();
1633        let collection = schema.get_collection("users").unwrap();
1634        let mut ctx = ValidationContext::new(&schema);
1635
1636        // contains on int field should fail
1637        let filter = Filter::Contains("age".to_string(), Value::String("10".to_string()));
1638        validate_filter(&filter, collection, &mut ctx);
1639
1640        assert!(ctx.has_errors());
1641        assert!(
1642            ctx.errors
1643                .iter()
1644                .any(|e| e.code == ErrorCode::InvalidFilterOperator)
1645        );
1646    }
1647
1648    #[test]
1649    fn test_resolve_variables() {
1650        let mut doc = Document {
1651            operations: vec![ast::Operation::Query(Query {
1652                name: None,
1653                variable_definitions: vec![],
1654                directives: vec![],
1655                selection_set: vec![Selection::Field(Field {
1656                    alias: None,
1657                    name: "users".to_string(),
1658                    arguments: vec![ast::Argument {
1659                        name: "limit".to_string(),
1660                        value: Value::Variable("pageSize".to_string()),
1661                    }],
1662                    directives: vec![],
1663                    selection_set: vec![],
1664                    computed_expression: None,
1665                })],
1666                variables_values: HashMap::new(),
1667            })],
1668        };
1669
1670        let mut vars = HashMap::new();
1671        vars.insert("pageSize".to_string(), Value::Int(10));
1672
1673        let result = resolve_variables(&mut doc, &vars);
1674        assert!(result.is_ok());
1675
1676        // Check that variable was resolved
1677        if let ast::Operation::Query(query) = &doc.operations[0] {
1678            if let Selection::Field(user_field) = &query.selection_set[0] {
1679                let arg = &user_field.arguments[0];
1680                assert!(matches!(arg.value, Value::Int(10)));
1681            } else {
1682                panic!("Expected Selection::Field");
1683            }
1684        } else {
1685            panic!("Expected Query operation");
1686        }
1687    }
1688}