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        }
263    }
264
265    if query.name.is_some() {
266        ctx.pop_path();
267    }
268}
269
270/// Validate a mutation operation
271fn validate_mutation<S: SchemaProvider>(mutation: &Mutation, ctx: &mut ValidationContext<'_, S>) {
272    if let Some(name) = &mutation.name {
273        ctx.push_path(name);
274    }
275
276    // Validate variable definitions
277    validate_variable_definitions(&mutation.variable_definitions, ctx);
278
279    // Validate each mutation operation
280    for (i, op) in mutation.operations.iter().enumerate() {
281        ctx.push_path(&format!("mutation[{}]", i));
282        validate_mutation_operation(op, ctx);
283        ctx.pop_path();
284    }
285
286    if mutation.name.is_some() {
287        ctx.pop_path();
288    }
289}
290
291/// Validate an inline fragment
292///
293/// `expected_collection` is `Some(name)` when the fragment appears inside a
294/// collection selection set — the type condition must match the enclosing
295/// collection. At root level (query/subscription) it is `None`.
296fn validate_inline_fragment<S: SchemaProvider>(
297    inline: &ast::InlineFragment,
298    expected_collection: Option<&str>,
299    ctx: &mut ValidationContext<'_, S>,
300) {
301    // When nested inside a collection, the type condition must match it
302    if let Some(enc) = expected_collection {
303        if inline.type_condition != enc {
304            ctx.add_error(
305                ErrorCode::TypeMismatch,
306                format!(
307                    "Inline fragment on '{}' cannot appear inside '{}'",
308                    inline.type_condition, enc
309                ),
310            );
311            return;
312        }
313    }
314
315    // Check if type condition refers to a valid collection
316    if !ctx.schema.collection_exists(&inline.type_condition) {
317        ctx.add_error(
318            ErrorCode::UnknownCollection,
319            format!(
320                "Unknown collection '{}' in inline fragment",
321                inline.type_condition
322            ),
323        );
324        return;
325    }
326
327    // Validate inner selection set using the type condition as context
328    if let Some(collection) = ctx.schema.get_collection(&inline.type_condition) {
329        validate_selection_set(&inline.selection_set, collection, ctx);
330    }
331}
332
333/// Validate a subscription operation
334fn validate_subscription<S: SchemaProvider>(
335    sub: &Subscription,
336    ctx: &mut ValidationContext<'_, S>,
337) {
338    if let Some(name) = &sub.name {
339        ctx.push_path(name);
340    }
341
342    // Validate variable definitions
343    validate_variable_definitions(&sub.variable_definitions, ctx);
344
345    // Validate selection set
346    for selection in &sub.selection_set {
347        match selection {
348            Selection::Field(field) => {
349                validate_field(field, None, ctx);
350            }
351            Selection::InlineFragment(inline) => {
352                validate_inline_fragment(inline, None, ctx);
353            }
354            Selection::FragmentSpread(fragment_name) => {
355                if let Some(fragment) = ctx.fragments.get(fragment_name) {
356                    validate_fragment_spread(fragment_name, &fragment.type_condition, ctx);
357                } else {
358                    ctx.add_error(
359                        ErrorCode::UnknownFragment,
360                        format!("Fragment '{}' is not defined", fragment_name),
361                    );
362                }
363            }
364        }
365    }
366
367    if sub.name.is_some() {
368        ctx.pop_path();
369    }
370}
371
372/// Validate variable definitions
373fn validate_variable_definitions<S: SchemaProvider>(
374    definitions: &[ast::VariableDefinition],
375    ctx: &mut ValidationContext<'_, S>,
376) {
377    for def in definitions {
378        let var_name = &def.name;
379
380        // Check if required variable is provided
381        if def.var_type.is_required && def.default_value.is_none() {
382            if !ctx.variables.contains_key(var_name) {
383                ctx.add_error(
384                    ErrorCode::MissingRequiredVariable,
385                    format!("Required variable '{}' is not provided", var_name),
386                );
387            }
388        }
389    }
390}
391
392/// Validate a field selection
393///
394/// * `parent_collection` - If Some, this field is within a collection and we check field types.
395///                         If None, this is a top-level field that should be a collection reference.
396fn validate_field<S: SchemaProvider>(
397    field: &Field,
398    parent_collection: Option<&Collection>,
399    ctx: &mut ValidationContext<'_, S>,
400) {
401    let field_name = field.alias.as_ref().unwrap_or(&field.name);
402    ctx.push_path(field_name);
403
404    match parent_collection {
405        None => {
406            // Top-level field - always validate collection existence (even for leaf fields)
407            let collection_name = &field.name;
408            if !ctx.schema.collection_exists(collection_name) {
409                ctx.add_error(
410                    ErrorCode::UnknownCollection,
411                    format!("Collection '{}' does not exist", collection_name),
412                );
413            } else if field.selection_set.is_empty() {
414                ctx.add_error(
415                    ErrorCode::InvalidInput,
416                    format!("Collection '{}' requires a selection set", collection_name),
417                );
418            } else if let Some(collection) = ctx.schema.get_collection(collection_name) {
419                // Validate nested fields against collection schema
420                validate_selection_set(&field.selection_set, collection, ctx);
421
422                // Validate filter if present
423                for arg in &field.arguments {
424                    if arg.name == "where" || arg.name == "filter" {
425                        report_unknown_filter_ops(&arg.value, ctx);
426                        if let Some(filter) = extract_filter_from_value(&arg.value) {
427                            validate_filter(&filter, collection, ctx);
428                        }
429                    }
430                }
431            }
432        }
433        Some(collection) => {
434            // Nested field within a collection - only check selection set if non-empty
435            if !field.selection_set.is_empty() {
436                if let Some(field_def) = collection.fields.get(&field.name) {
437                    match &field_def.field_type {
438                        FieldType::Nested(nested_schema) => {
439                            // Validate selection set against nested schema
440                            validate_nested_selection_set(&field.selection_set, nested_schema, ctx);
441                        }
442                        FieldType::Object | FieldType::Any => {
443                            // Object/Any type - selection set is valid but we can't validate fields
444                            // (no schema to validate against - schemaless/dynamic data)
445                        }
446                        FieldType::Scalar(ScalarType::Object)
447                        | FieldType::Scalar(ScalarType::Any) => {
448                            // Scalar Object/Any - also allow selection sets without validation
449                        }
450                        _ => {
451                            // Non-object type with selection set - this is an error
452                            ctx.add_error(
453                                ErrorCode::TypeMismatch,
454                                format!(
455                                    "Field '{}' is not an object type but has a selection set",
456                                    field.name
457                                ),
458                            );
459                        }
460                    }
461                }
462                // If field doesn't exist, the error is already reported by validate_selection_set
463            }
464        }
465    }
466
467    ctx.pop_path();
468}
469
470/// Validate selection set against collection schema
471fn validate_selection_set<S: SchemaProvider>(
472    selections: &[Selection],
473    collection: &Collection,
474    ctx: &mut ValidationContext<'_, S>,
475) {
476    let mut aliases_seen: HashMap<String, bool> = HashMap::new();
477
478    for selection in selections {
479        // Validate based on selection type
480        match selection {
481            Selection::Field(field) => {
482                let display_name = field.alias.as_ref().unwrap_or(&field.name);
483
484                // Check for duplicate aliases
485                if aliases_seen.contains_key(display_name) {
486                    ctx.add_error(
487                        ErrorCode::DuplicateAlias,
488                        format!("Duplicate field/alias '{}' in selection", display_name),
489                    );
490                }
491                aliases_seen.insert(display_name.clone(), true);
492
493                // Check if field exists in collection schema
494                let field_name = &field.name;
495                if !field_name.starts_with("__") && field_name != "id" {
496                    if !collection.fields.contains_key(field_name) {
497                        ctx.add_error(
498                            ErrorCode::UnknownField,
499                            format!(
500                                "Field '{}' does not exist in collection '{}'",
501                                field_name, collection.name
502                            ),
503                        );
504                    }
505                }
506
507                // Recursively validate nested selection with parent context
508                validate_field(field, Some(collection), ctx);
509            }
510            Selection::InlineFragment(inline) => {
511                validate_inline_fragment(inline, Some(&collection.name), ctx);
512            }
513            Selection::FragmentSpread(fragment_name) => {
514                validate_fragment_spread(fragment_name, &collection.name, ctx);
515            }
516        }
517    }
518}
519
520/// Validate selection set against a nested object schema
521fn validate_nested_selection_set<S: SchemaProvider>(
522    selections: &[Selection],
523    nested_schema: &HashMap<String, crate::types::FieldDefinition>,
524    ctx: &mut ValidationContext<'_, S>,
525) {
526    let mut aliases_seen: HashMap<String, bool> = HashMap::new();
527
528    for selection in selections {
529        match selection {
530            Selection::Field(field) => {
531                let display_name = field.alias.as_ref().unwrap_or(&field.name);
532
533                // Check for duplicate aliases
534                if aliases_seen.contains_key(display_name) {
535                    ctx.add_error(
536                        ErrorCode::DuplicateAlias,
537                        format!("Duplicate field/alias '{}' in selection", display_name),
538                    );
539                }
540                aliases_seen.insert(display_name.clone(), true);
541
542                // Check if field exists in nested schema
543                let field_name = &field.name;
544                if !field_name.starts_with("__") {
545                    if !nested_schema.contains_key(field_name) {
546                        ctx.add_error(
547                            ErrorCode::UnknownField,
548                            format!("Field '{}' does not exist in nested object", field_name),
549                        );
550                    } else if !field.selection_set.is_empty() {
551                        // Validate further nesting
552                        if let Some(field_def) = nested_schema.get(field_name) {
553                            match &field_def.field_type {
554                                FieldType::Nested(deeper_schema) => {
555                                    ctx.push_path(field_name);
556                                    validate_nested_selection_set(
557                                        &field.selection_set,
558                                        deeper_schema,
559                                        ctx,
560                                    );
561                                    ctx.pop_path();
562                                }
563                                FieldType::Object | FieldType::Any => {
564                                    // Object/Any type - allow selection but can't validate
565                                }
566                                FieldType::Scalar(ScalarType::Object)
567                                | FieldType::Scalar(ScalarType::Any) => {
568                                    // Scalar Object/Any - allow selection but can't validate
569                                }
570                                _ => {
571                                    ctx.add_error(
572                                        ErrorCode::TypeMismatch,
573                                        format!(
574                                            "Field '{}' is not an object type but has a selection set",
575                                            field_name
576                                        ),
577                                    );
578                                }
579                            }
580                        }
581                    }
582                }
583            }
584            Selection::InlineFragment(_) | Selection::FragmentSpread(_) => {
585                // Fragments in nested objects not supported for now
586                ctx.add_error(
587                    ErrorCode::InvalidInput,
588                    "Fragments are not supported in nested object selections".to_string(),
589                );
590            }
591        }
592    }
593}
594
595/// Validate a fragment spread
596fn validate_fragment_spread<S: SchemaProvider>(
597    fragment_name: &str,
598    expected_collection: &str,
599    ctx: &mut ValidationContext<'_, S>,
600) {
601    // 0. Check for cycles
602    if ctx.validating_fragments.contains(fragment_name) {
603        ctx.add_error(
604            ErrorCode::InvalidInput,
605            format!("Fragment '{}' contains a cyclic reference", fragment_name),
606        );
607        return;
608    }
609
610    // 1. Check if fragment exists
611    let fragment_opt = ctx.fragments.get(fragment_name).cloned();
612
613    if let Some(fragment) = fragment_opt {
614        // 2. Check type condition matches
615        if fragment.type_condition != expected_collection {
616            ctx.add_error(
617                ErrorCode::TypeMismatch,
618                format!(
619                    "Fragment '{}' is defined on '{}' but used on '{}'",
620                    fragment_name, fragment.type_condition, expected_collection
621                ),
622            );
623            return;
624        }
625
626        // 3. Validate fragment's selection set against the collection
627        if let Some(collection) = ctx.schema.get_collection(expected_collection) {
628            ctx.push_path(&format!("...{}", fragment_name));
629            ctx.validating_fragments.insert(fragment_name.to_string());
630            validate_selection_set(&fragment.selection_set, collection, ctx);
631            ctx.validating_fragments.remove(fragment_name);
632            ctx.pop_path();
633        } else {
634            ctx.add_error(
635                ErrorCode::UnknownCollection,
636                format!(
637                    "Fragment '{}' references unknown collection '{}'",
638                    fragment_name, expected_collection
639                ),
640            );
641        }
642    } else {
643        ctx.add_error(
644            ErrorCode::UnknownFragment,
645            format!("Fragment '{}' is not defined", fragment_name),
646        );
647    }
648}
649
650/// Validate a mutation operation
651fn validate_mutation_operation<S: SchemaProvider>(
652    op: &ast::MutationOperation,
653    ctx: &mut ValidationContext<'_, S>,
654) {
655    match &op.operation {
656        ast::MutationOp::Insert { collection, data } => {
657            if let Some(col_def) = ctx.schema.get_collection(collection) {
658                validate_object_against_schema(data, col_def, ctx);
659            } else {
660                ctx.add_error(
661                    ErrorCode::UnknownCollection,
662                    format!("Collection '{}' does not exist", collection),
663                );
664            }
665        }
666        ast::MutationOp::InsertMany { collection, data } => {
667            if let Some(col_def) = ctx.schema.get_collection(collection) {
668                for item in data {
669                    validate_object_against_schema(item, col_def, ctx);
670                }
671            } else {
672                ctx.add_error(
673                    ErrorCode::UnknownCollection,
674                    format!("Collection '{}' does not exist", collection),
675                );
676            }
677        }
678        ast::MutationOp::Update {
679            collection, data, ..
680        }
681        | ast::MutationOp::Upsert {
682            collection, data, ..
683        } => {
684            if let Some(col_def) = ctx.schema.get_collection(collection) {
685                // Validate partial object for updates (skipping required checks)
686                validate_partial_object(data, col_def, ctx);
687            } else {
688                ctx.add_error(
689                    ErrorCode::UnknownCollection,
690                    format!("Collection '{}' does not exist", collection),
691                );
692            }
693        }
694        ast::MutationOp::Delete { collection, .. } => {
695            if !ctx.schema.collection_exists(collection) {
696                ctx.add_error(
697                    ErrorCode::UnknownCollection,
698                    format!("Collection '{}' does not exist", collection),
699                );
700            }
701        }
702        ast::MutationOp::EnqueueJob { .. } => {}
703        ast::MutationOp::EnqueueJobs { .. } => {}
704        ast::MutationOp::Import { collection, data } => {
705            if let Some(col_def) = ctx.schema.get_collection(collection) {
706                for item in data {
707                    validate_object_against_schema(item, col_def, ctx);
708                }
709            }
710        }
711        ast::MutationOp::Export { .. } => {}
712        ast::MutationOp::Transaction { operations } => {
713            for (i, inner_op) in operations.iter().enumerate() {
714                ctx.push_path(&format!("tx[{}]", i));
715                validate_mutation_operation(inner_op, ctx);
716                ctx.pop_path();
717            }
718        }
719    }
720}
721
722/// Validate an object value against a collection schema
723fn validate_object_against_schema<S: SchemaProvider>(
724    value: &Value,
725    collection: &Collection,
726    ctx: &mut ValidationContext<'_, S>,
727) {
728    match value {
729        Value::Object(map) => {
730            // 1. Check provided fields validity
731            for (key, val) in map {
732                if let Some(field_def) = collection.fields.get(key) {
733                    // Check if null is allowed
734                    if matches!(val, Value::Null) && !field_def.nullable {
735                        ctx.add_error(
736                            ErrorCode::InvalidInput,
737                            format!("Field '{}' cannot be null", key),
738                        );
739                    } else if !matches!(val, Value::Null)
740                        && !validate_value_against_type(val, &field_def.field_type)
741                    {
742                        ctx.add_error(
743                            ErrorCode::TypeMismatch,
744                            format!(
745                                "Type mismatch for field '{}': expected {:?}, got {:?}",
746                                key,
747                                field_def.field_type,
748                                value_type_name(val)
749                            ),
750                        );
751                    }
752                } else if key != "id" {
753                    ctx.add_error(
754                        ErrorCode::UnknownField,
755                        format!(
756                            "Field '{}' not defined in collection '{}'",
757                            key, collection.name
758                        ),
759                    );
760                }
761            }
762
763            // 2. Check for missing required fields
764            for (name, def) in &collection.fields {
765                if !def.nullable && !map.contains_key(name) {
766                    ctx.add_error(
767                        ErrorCode::InvalidInput,
768                        format!("Missing required field '{}'", name),
769                    );
770                }
771            }
772        }
773        _ => {
774            ctx.add_error(
775                ErrorCode::TypeMismatch,
776                format!(
777                    "Expected object for collection '{}', got {:?}",
778                    collection.name,
779                    value_type_name(value)
780                ),
781            );
782        }
783    }
784}
785
786/// Validate a partial object (field subset) against a collection schema
787/// Used for Update operations where missing required fields are allowed
788fn validate_partial_object<S: SchemaProvider>(
789    value: &Value,
790    collection: &Collection,
791    ctx: &mut ValidationContext<'_, S>,
792) {
793    match value {
794        Value::Object(map) => {
795            // Check provided fields validity
796            for (key, val) in map {
797                if let Some(field_def) = collection.fields.get(key) {
798                    // Check if null is allowed
799                    if matches!(val, Value::Null) && !field_def.nullable {
800                        ctx.add_error(
801                            ErrorCode::InvalidInput,
802                            format!("Field '{}' cannot be null", key),
803                        );
804                    } else if !matches!(val, Value::Null)
805                        && !validate_value_against_type(val, &field_def.field_type)
806                    {
807                        ctx.add_error(
808                            ErrorCode::TypeMismatch,
809                            format!(
810                                "Type mismatch for field '{}': expected {:?}, got {:?}",
811                                key,
812                                field_def.field_type,
813                                value_type_name(val)
814                            ),
815                        );
816                    }
817                } else if key != "id" {
818                    ctx.add_error(
819                        ErrorCode::UnknownField,
820                        format!(
821                            "Field '{}' not defined in collection '{}'",
822                            key, collection.name
823                        ),
824                    );
825                }
826            }
827        }
828        _ => {
829            ctx.add_error(
830                ErrorCode::TypeMismatch,
831                format!(
832                    "Expected object for collection '{}', got {:?}",
833                    collection.name,
834                    value_type_name(value)
835                ),
836            );
837        }
838    }
839}
840
841/// Recursively validate value against type
842fn validate_value_against_type(value: &Value, expected: &FieldType) -> bool {
843    use crate::types::ScalarType;
844    match (expected, value) {
845        // Handle Scalar Types
846        (FieldType::Scalar(ScalarType::Any), _) => true,
847        (_, Value::Null) => true,
848        (_, Value::Variable(_)) => true,
849
850        (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
851        (FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
852        (FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
853        (FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true,
854        (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
855        (FieldType::Scalar(ScalarType::Uuid), Value::String(_)) => true,
856
857        // Handle Array Types
858        (FieldType::Array(scalar_inner), Value::Array(items)) => {
859            // Check each item matches the scalar inner type
860            let inner_field_type = FieldType::Scalar(scalar_inner.clone());
861            items
862                .iter()
863                .all(|item| validate_value_against_type(item, &inner_field_type))
864        }
865
866        // Handle Objects
867        (FieldType::Object, Value::Object(_)) => true,
868        // Handle Nested Objects (deep validation)
869        (FieldType::Nested(schema), Value::Object(map)) => {
870            // 1. Check all provided fields exist in schema and match type
871            for (key, val) in map {
872                if let Some(def) = schema.get(key) {
873                    if matches!(val, Value::Null) {
874                        if !def.nullable {
875                            return false; // Null not allowed
876                        }
877                    } else if !validate_value_against_type(val, &def.field_type) {
878                        return false; // Type mismatch in nested field
879                    }
880                } else {
881                    return false; // Unknown field in nested object
882                }
883            }
884            // 2. Check all required fields are provided
885            for (key, def) in schema.iter() {
886                if !def.nullable && !map.contains_key(key) {
887                    return false; // Missing required nested field
888                }
889            }
890            true
891        }
892        (FieldType::Nested(_), _) => false, // Not an object
893
894        // Handle ScalarType::Object (e.g. inside Array<Object>)
895        (FieldType::Scalar(ScalarType::Object), Value::Object(_)) => true,
896        // Handle ScalarType::Array (e.g. inside Array<Array>)
897        (FieldType::Scalar(ScalarType::Array), Value::Array(_)) => true,
898
899        _ => false,
900    }
901}
902
903/// Validate a filter expression
904fn validate_filter<S: SchemaProvider>(
905    filter: &Filter,
906    collection: &Collection,
907    ctx: &mut ValidationContext<'_, S>,
908) {
909    match filter {
910        Filter::Eq(field, value)
911        | Filter::Ne(field, value)
912        | Filter::Gt(field, value)
913        | Filter::Gte(field, value)
914        | Filter::Lt(field, value)
915        | Filter::Lte(field, value) => {
916            validate_filter_field(field, value, collection, ctx);
917        }
918        Filter::In(field, value) | Filter::NotIn(field, value) => {
919            // In/NotIn expects an array value
920            if !matches!(value, Value::Array(_)) {
921                ctx.add_error(
922                    ErrorCode::TypeMismatch,
923                    format!("Filter 'in'/'notIn' on '{}' requires an array value", field),
924                );
925            }
926            validate_filter_field_exists(field, collection, ctx);
927        }
928        Filter::ContainsAny(field, value) | Filter::ContainsAll(field, value) => {
929            if !matches!(value, Value::Array(_) | Value::Variable(_)) {
930                ctx.add_error(
931                    ErrorCode::TypeMismatch,
932                    format!("containsAny/containsAll on field '{}' expects an array", field),
933                );
934            }
935            validate_filter_field_exists(field, collection, ctx);
936        }
937        Filter::Contains(field, _)
938        | Filter::StartsWith(field, _)
939        | Filter::EndsWith(field, _)
940        | Filter::Matches(field, _) => {
941            // String operations - check field is a string type
942            if let Some(field_def) = collection.fields.get(field) {
943                if field_def.field_type != FieldType::SCALAR_STRING {
944                    ctx.add_error(
945                        ErrorCode::InvalidFilterOperator,
946                        format!("String operator on non-string field '{}'", field),
947                    );
948                }
949            } else {
950                validate_filter_field_exists(field, collection, ctx);
951            }
952        }
953        Filter::IsNull(field) | Filter::IsNotNull(field) => {
954            validate_filter_field_exists(field, collection, ctx);
955        }
956        Filter::And(filters) | Filter::Or(filters) => {
957            for f in filters {
958                validate_filter(f, collection, ctx);
959            }
960        }
961        Filter::Not(inner) => {
962            validate_filter(inner, collection, ctx);
963        }
964    }
965}
966
967/// Validate a filter field exists
968fn validate_filter_field_exists<S: SchemaProvider>(
969    field: &str,
970    collection: &Collection,
971    ctx: &mut ValidationContext<'_, S>,
972) {
973    if field != "id" && !collection.fields.contains_key(field) {
974        ctx.add_error(
975            ErrorCode::UnknownField,
976            format!(
977                "Filter field '{}' does not exist in collection '{}'",
978                field, collection.name
979            ),
980        );
981    }
982}
983
984/// Validate a filter field with value type checking
985fn validate_filter_field<S: SchemaProvider>(
986    field: &str,
987    value: &Value,
988    collection: &Collection,
989    ctx: &mut ValidationContext<'_, S>,
990) {
991    if field == "id" {
992        return; // id field always exists
993    }
994
995    if let Some(field_def) = collection.fields.get(field) {
996        // Check type compatibility
997        if !is_type_compatible(&field_def.field_type, value) {
998            ctx.add_error(
999                ErrorCode::TypeMismatch,
1000                format!(
1001                    "Type mismatch: field '{}' expects {:?}, got {:?}",
1002                    field,
1003                    field_def.field_type,
1004                    value_type_name(value)
1005                ),
1006            );
1007        }
1008    } else {
1009        ctx.add_error(
1010            ErrorCode::UnknownField,
1011            format!(
1012                "Filter field '{}' does not exist in collection '{}'",
1013                field, collection.name
1014            ),
1015        );
1016    }
1017}
1018
1019/// Check if a value is compatible with a field type
1020fn is_type_compatible(field_type: &FieldType, value: &Value) -> bool {
1021    match (field_type, value) {
1022        (_, Value::Null) => true,        // Null is compatible with any type
1023        (_, Value::Variable(_)) => true, // Variables are resolved later
1024        (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
1025        (FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
1026        (FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
1027        (FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true, // Int can be used where Float expected
1028        (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
1029        (FieldType::Array(_), Value::Array(_)) => true,
1030        (FieldType::Object, Value::Object(_)) => true,
1031        (FieldType::Nested(_), Value::Object(_)) => true, // Structural compatibility for filters (deep check might be too expensive/complex here)
1032        (FieldType::Scalar(ScalarType::Object), Value::Object(_)) => true, // Support for ScalarType::Object
1033        (FieldType::Scalar(ScalarType::Array), Value::Array(_)) => true, // Support for ScalarType::Array
1034        (FieldType::Scalar(ScalarType::Any), _) => true, // Any type accepts all values
1035        (FieldType::Scalar(ScalarType::Uuid), Value::String(_)) => true, // UUIDs are often passed as strings
1036        _ => false,
1037    }
1038}
1039
1040/// Get a human-readable type name for a value
1041fn value_type_name(value: &Value) -> &'static str {
1042    match value {
1043        Value::Null => "null",
1044        Value::Boolean(_) => "boolean",
1045        Value::Int(_) => "int",
1046        Value::Float(_) => "float",
1047        Value::String(_) => "string",
1048        Value::Array(_) => "array",
1049        Value::Object(_) => "object",
1050        Value::Variable(_) => "variable",
1051        Value::Enum(_) => "enum",
1052    }
1053}
1054
1055/// Report validation errors for unrecognised filter operators so users get a
1056/// clear message for typos instead of silently-dropped conditions.
1057fn report_unknown_filter_ops<S: SchemaProvider>(
1058    value: &Value,
1059    ctx: &mut ValidationContext<'_, S>,
1060) {
1061    const KNOWN_OPS: &[&str] = &[
1062        "eq", "ne", "gt", "gte", "lt", "lte",
1063        "in", "nin", "contains", "startsWith", "endsWith",
1064        "matches", "isNull", "isNotNull",
1065    ];
1066    if let Value::Object(map) = value {
1067        for (key, val) in map {
1068            match key.as_str() {
1069                "and" | "or" => {
1070                    if let Value::Array(arr) = val {
1071                        arr.iter().for_each(|v| report_unknown_filter_ops(v, ctx));
1072                    }
1073                }
1074                "not" => report_unknown_filter_ops(val, ctx),
1075                field => {
1076                    if let Value::Object(ops) = val {
1077                        // Only flag unknown keys when at least one recognized operator
1078                        // is present — otherwise this is a nested-field reference and
1079                        // we should recurse rather than flag every key as an unknown op.
1080                        let has_known_op = ops.keys().any(|k| KNOWN_OPS.contains(&k.as_str()));
1081                        if has_known_op {
1082                            for op in ops.keys() {
1083                                if !KNOWN_OPS.contains(&op.as_str()) {
1084                                    ctx.add_error(
1085                                        ErrorCode::InvalidArgument,
1086                                        format!(
1087                                            "Unknown filter operator '{}' on field '{}'",
1088                                            op, field
1089                                        ),
1090                                    );
1091                                }
1092                            }
1093                        } else {
1094                            // Nested field reference — recurse into it
1095                            report_unknown_filter_ops(val, ctx);
1096                        }
1097                    }
1098                }
1099            }
1100        }
1101    }
1102}
1103
1104/// Extract a Filter from an AST Value (for parsing where arguments)
1105fn extract_filter_from_value(value: &Value) -> Option<Filter> {
1106    match value {
1107        Value::Object(map) => {
1108            let mut filters = Vec::new();
1109
1110            for (key, val) in map {
1111                match key.as_str() {
1112                    "and" => {
1113                        if let Value::Array(arr) = val {
1114                            let sub_filters: Vec<Filter> =
1115                                arr.iter().filter_map(extract_filter_from_value).collect();
1116                            if !sub_filters.is_empty() {
1117                                filters.push(Filter::And(sub_filters));
1118                            }
1119                        }
1120                    }
1121                    "or" => {
1122                        if let Value::Array(arr) = val {
1123                            let sub_filters: Vec<Filter> =
1124                                arr.iter().filter_map(extract_filter_from_value).collect();
1125                            if !sub_filters.is_empty() {
1126                                filters.push(Filter::Or(sub_filters));
1127                            }
1128                        }
1129                    }
1130                    "not" => {
1131                        if let Some(inner) = extract_filter_from_value(val) {
1132                            filters.push(Filter::Not(Box::new(inner)));
1133                        }
1134                    }
1135                    field => {
1136                        // Field-level filter: { field: { eq: value } }
1137                        if let Value::Object(ops) = val {
1138                            for (op, op_val) in ops {
1139                                let filter = match op.as_str() {
1140                                    "eq" => Some(Filter::Eq(field.to_string(), op_val.clone())),
1141                                    "ne" => Some(Filter::Ne(field.to_string(), op_val.clone())),
1142                                    "gt" => Some(Filter::Gt(field.to_string(), op_val.clone())),
1143                                    "gte" => Some(Filter::Gte(field.to_string(), op_val.clone())),
1144                                    "lt" => Some(Filter::Lt(field.to_string(), op_val.clone())),
1145                                    "lte" => Some(Filter::Lte(field.to_string(), op_val.clone())),
1146                                    "in" => Some(Filter::In(field.to_string(), op_val.clone())),
1147                                    "nin" => Some(Filter::NotIn(field.to_string(), op_val.clone())),
1148                                    "contains" => {
1149                                        Some(Filter::Contains(field.to_string(), op_val.clone()))
1150                                    }
1151                                    "startsWith" => {
1152                                        Some(Filter::StartsWith(field.to_string(), op_val.clone()))
1153                                    }
1154                                    "endsWith" => {
1155                                        Some(Filter::EndsWith(field.to_string(), op_val.clone()))
1156                                    }
1157                                    "matches" => {
1158                                        Some(Filter::Matches(field.to_string(), op_val.clone()))
1159                                    }
1160                                    "isNull" => Some(Filter::IsNull(field.to_string())),
1161                                    "isNotNull" => Some(Filter::IsNotNull(field.to_string())),
1162                                    _ => None,
1163                                };
1164                                if let Some(f) = filter {
1165                                    filters.push(f);
1166                                }
1167                            }
1168                        }
1169                    }
1170                }
1171            }
1172
1173            match filters.len() {
1174                0 => None,
1175                1 => Some(filters.remove(0)),
1176                _ => Some(Filter::And(filters)),
1177            }
1178        }
1179        _ => None,
1180    }
1181}
1182
1183/// Resolve variables in a document, replacing Value::Variable with actual values
1184pub fn resolve_variables(
1185    doc: &mut Document,
1186    variables: &HashMap<String, ast::Value>,
1187) -> Result<(), ValidationError> {
1188    for op in &mut doc.operations {
1189        match op {
1190            ast::Operation::Query(query) => {
1191                resolve_in_fields(&mut query.selection_set, variables)?;
1192            }
1193            ast::Operation::Mutation(mutation) => {
1194                for mut_op in &mut mutation.operations {
1195                    resolve_in_mutation_op(mut_op, variables)?;
1196                }
1197            }
1198            ast::Operation::Subscription(sub) => {
1199                resolve_in_fields(&mut sub.selection_set, variables)?;
1200            }
1201            ast::Operation::Schema(_) => {}
1202            ast::Operation::Migration(_) => {}
1203            ast::Operation::FragmentDefinition(fragment) => {
1204                // Resolve variables inside fragment field arguments so they are
1205                // substituted before the fragment is expanded at execution time.
1206                resolve_in_fields(&mut fragment.selection_set, variables)?;
1207            }
1208            ast::Operation::Introspection(_) => {}      // No variables in introspection
1209            ast::Operation::Handler(_) => {}            // Handlers don't have variable resolution
1210        }
1211    }
1212    Ok(())
1213}
1214
1215fn resolve_in_fields(
1216    fields: &mut [Selection],
1217    variables: &HashMap<String, ast::Value>,
1218) -> Result<(), ValidationError> {
1219    for selection in fields {
1220        match selection {
1221            Selection::Field(field) => {
1222                // Resolve variables in arguments
1223                for arg in &mut field.arguments {
1224                    resolve_in_value(&mut arg.value, variables)?;
1225                }
1226                // Recursively resolve in nested selection
1227                resolve_in_fields(&mut field.selection_set, variables)?;
1228            }
1229            Selection::InlineFragment(inline) => {
1230                resolve_in_fields(&mut inline.selection_set, variables)?;
1231            }
1232            Selection::FragmentSpread(_) => {
1233                // Nothing to resolve in fragment spread itself (name)
1234            }
1235        }
1236    }
1237    Ok(())
1238}
1239
1240fn resolve_in_mutation_op(
1241    op: &mut ast::MutationOperation,
1242    variables: &HashMap<String, ast::Value>,
1243) -> Result<(), ValidationError> {
1244    match &mut op.operation {
1245        ast::MutationOp::Insert { data, .. }
1246        | ast::MutationOp::Update { data, .. }
1247        | ast::MutationOp::Upsert { data, .. } => {
1248            resolve_in_value(data, variables)?;
1249        }
1250        ast::MutationOp::InsertMany { data, .. } => {
1251            for item in data {
1252                resolve_in_value(item, variables)?;
1253            }
1254        }
1255        ast::MutationOp::Delete { .. } => {}
1256        ast::MutationOp::EnqueueJob { payload, .. } => {
1257            resolve_in_value(payload, variables)?;
1258        }
1259        ast::MutationOp::EnqueueJobs { payloads, .. } => {
1260            for p in payloads {
1261                resolve_in_value(p, variables)?;
1262            }
1263        }
1264        ast::MutationOp::Import { data, .. } => {
1265            for item in data {
1266                resolve_in_value(item, variables)?;
1267            }
1268        }
1269        ast::MutationOp::Export { .. } => {}
1270        ast::MutationOp::Transaction { operations } => {
1271            for inner in operations {
1272                resolve_in_mutation_op(inner, variables)?;
1273            }
1274        }
1275    }
1276    resolve_in_fields(&mut op.selection_set, variables)
1277}
1278
1279fn resolve_in_value(
1280    value: &mut Value,
1281    variables: &HashMap<String, ast::Value>,
1282) -> Result<(), ValidationError> {
1283    match value {
1284        Value::Variable(name) => {
1285            if let Some(resolved) = variables.get(name) {
1286                *value = resolved.clone();
1287            } else {
1288                return Err(ValidationError::new(
1289                    ErrorCode::MissingRequiredVariable,
1290                    format!("Variable '{}' is not provided", name),
1291                ));
1292            }
1293        }
1294        Value::Array(items) => {
1295            for item in items {
1296                resolve_in_value(item, variables)?;
1297            }
1298        }
1299        Value::Object(map) => {
1300            for v in map.values_mut() {
1301                resolve_in_value(v, variables)?;
1302            }
1303        }
1304        _ => {}
1305    }
1306    Ok(())
1307}
1308
1309#[cfg(test)]
1310mod tests {
1311    use super::*;
1312    use crate::types::FieldDefinition;
1313
1314    fn create_test_schema() -> InMemorySchema {
1315        let mut schema = InMemorySchema::new();
1316
1317        let mut users_fields = HashMap::new();
1318        users_fields.insert(
1319            "name".to_string(),
1320            FieldDefinition {
1321                field_type: FieldType::SCALAR_STRING,
1322                unique: false,
1323                indexed: false,
1324                nullable: false,
1325                ..Default::default()
1326            },
1327        );
1328        users_fields.insert(
1329            "email".to_string(),
1330            FieldDefinition {
1331                field_type: FieldType::SCALAR_STRING,
1332                unique: true,
1333                indexed: true,
1334                nullable: false,
1335                ..Default::default()
1336            },
1337        );
1338        users_fields.insert(
1339            "age".to_string(),
1340            FieldDefinition {
1341                field_type: FieldType::SCALAR_INT,
1342                unique: false,
1343                indexed: false,
1344                nullable: false,
1345                ..Default::default()
1346            },
1347        );
1348        users_fields.insert(
1349            "active".to_string(),
1350            FieldDefinition {
1351                field_type: FieldType::SCALAR_BOOL,
1352                unique: false,
1353                indexed: false,
1354                nullable: false,
1355                ..Default::default()
1356            },
1357        );
1358
1359        schema.add_collection(Collection {
1360            name: "users".to_string(),
1361            fields: users_fields,
1362        });
1363
1364        schema
1365    }
1366
1367    #[test]
1368    fn test_validate_unknown_collection() {
1369        let schema = create_test_schema();
1370        let doc = Document {
1371            operations: vec![ast::Operation::Query(Query {
1372                name: None,
1373                variable_definitions: vec![],
1374                directives: vec![],
1375                selection_set: vec![Selection::Field(Field {
1376                    alias: None,
1377                    name: "nonexistent".to_string(),
1378                    arguments: vec![],
1379                    directives: vec![],
1380                    selection_set: vec![Selection::Field(Field {
1381                        alias: None,
1382                        name: "id".to_string(),
1383                        arguments: vec![],
1384                        directives: vec![],
1385                        selection_set: vec![],
1386                    })],
1387                })],
1388                variables_values: HashMap::new(),
1389            })],
1390        };
1391
1392        let result = validate_document(&doc, &schema, HashMap::new());
1393        assert!(result.is_err());
1394        let errors = result.unwrap_err();
1395        assert!(
1396            errors
1397                .iter()
1398                .any(|e| e.code == ErrorCode::UnknownCollection)
1399        );
1400    }
1401
1402    #[test]
1403    fn test_validate_unknown_field() {
1404        let schema = create_test_schema();
1405        let doc = Document {
1406            operations: vec![ast::Operation::Query(Query {
1407                name: None,
1408                variable_definitions: vec![],
1409                directives: vec![],
1410                selection_set: vec![Selection::Field(Field {
1411                    alias: None,
1412                    name: "users".to_string(),
1413                    arguments: vec![],
1414                    directives: vec![],
1415                    selection_set: vec![Selection::Field(Field {
1416                        alias: None,
1417                        name: "nonexistent_field".to_string(),
1418                        arguments: vec![],
1419                        directives: vec![],
1420                        selection_set: vec![],
1421                    })],
1422                })],
1423                variables_values: HashMap::new(),
1424            })],
1425        };
1426
1427        let result = validate_document(&doc, &schema, HashMap::new());
1428        assert!(result.is_err());
1429        let errors = result.unwrap_err();
1430        assert!(errors.iter().any(|e| e.code == ErrorCode::UnknownField));
1431    }
1432
1433    #[test]
1434    fn test_validate_missing_required_variable() {
1435        let schema = create_test_schema();
1436        let doc = Document {
1437            operations: vec![ast::Operation::Query(Query {
1438                name: Some("GetUsers".to_string()),
1439                variable_definitions: vec![ast::VariableDefinition {
1440                    name: "minAge".to_string(),
1441                    var_type: ast::TypeAnnotation {
1442                        name: "Int".to_string(),
1443                        is_array: false,
1444                        is_required: true,
1445                    },
1446                    default_value: None,
1447                }],
1448                directives: vec![],
1449                selection_set: vec![],
1450                variables_values: HashMap::new(),
1451            })],
1452        };
1453
1454        // No variables provided
1455        let result = validate_document(&doc, &schema, HashMap::new());
1456        assert!(result.is_err());
1457        let errors = result.unwrap_err();
1458        assert!(
1459            errors
1460                .iter()
1461                .any(|e| e.code == ErrorCode::MissingRequiredVariable)
1462        );
1463    }
1464
1465    #[test]
1466    fn test_validate_valid_query() {
1467        let schema = create_test_schema();
1468        let doc = Document {
1469            operations: vec![ast::Operation::Query(Query {
1470                name: Some("GetUsers".to_string()),
1471                variable_definitions: vec![],
1472                directives: vec![],
1473                selection_set: vec![Selection::Field(Field {
1474                    alias: None,
1475                    name: "users".to_string(),
1476                    arguments: vec![],
1477                    directives: vec![],
1478                    selection_set: vec![
1479                        Selection::Field(Field {
1480                            alias: None,
1481                            name: "id".to_string(),
1482                            arguments: vec![],
1483                            directives: vec![],
1484                            selection_set: vec![],
1485                        }),
1486                        Selection::Field(Field {
1487                            alias: None,
1488                            name: "name".to_string(),
1489                            arguments: vec![],
1490                            directives: vec![],
1491                            selection_set: vec![],
1492                        }),
1493                        Selection::Field(Field {
1494                            alias: None,
1495                            name: "email".to_string(),
1496                            arguments: vec![],
1497                            directives: vec![],
1498                            selection_set: vec![],
1499                        }),
1500                    ],
1501                })],
1502                variables_values: HashMap::new(),
1503            })],
1504        };
1505
1506        let result = validate_document(&doc, &schema, HashMap::new());
1507        assert!(result.is_ok());
1508    }
1509
1510    #[test]
1511    fn test_validate_filter_type_mismatch() {
1512        let schema = create_test_schema();
1513        let collection = schema.get_collection("users").unwrap();
1514        let mut ctx = ValidationContext::new(&schema);
1515
1516        // age is Int, but we're filtering with a string
1517        let filter = Filter::Eq("age".to_string(), Value::String("not a number".to_string()));
1518        validate_filter(&filter, collection, &mut ctx);
1519
1520        assert!(ctx.has_errors());
1521        assert!(ctx.errors.iter().any(|e| e.code == ErrorCode::TypeMismatch));
1522    }
1523
1524    #[test]
1525    fn test_validate_filter_string_operator_on_int() {
1526        let schema = create_test_schema();
1527        let collection = schema.get_collection("users").unwrap();
1528        let mut ctx = ValidationContext::new(&schema);
1529
1530        // contains on int field should fail
1531        let filter = Filter::Contains("age".to_string(), Value::String("10".to_string()));
1532        validate_filter(&filter, collection, &mut ctx);
1533
1534        assert!(ctx.has_errors());
1535        assert!(
1536            ctx.errors
1537                .iter()
1538                .any(|e| e.code == ErrorCode::InvalidFilterOperator)
1539        );
1540    }
1541
1542    #[test]
1543    fn test_resolve_variables() {
1544        let mut doc = Document {
1545            operations: vec![ast::Operation::Query(Query {
1546                name: None,
1547                variable_definitions: vec![],
1548                directives: vec![],
1549                selection_set: vec![Selection::Field(Field {
1550                    alias: None,
1551                    name: "users".to_string(),
1552                    arguments: vec![ast::Argument {
1553                        name: "limit".to_string(),
1554                        value: Value::Variable("pageSize".to_string()),
1555                    }],
1556                    directives: vec![],
1557                    selection_set: vec![],
1558                })],
1559                variables_values: HashMap::new(),
1560            })],
1561        };
1562
1563        let mut vars = HashMap::new();
1564        vars.insert("pageSize".to_string(), Value::Int(10));
1565
1566        let result = resolve_variables(&mut doc, &vars);
1567        assert!(result.is_ok());
1568
1569        // Check that variable was resolved
1570        if let ast::Operation::Query(query) = &doc.operations[0] {
1571            if let Selection::Field(user_field) = &query.selection_set[0] {
1572                let arg = &user_field.arguments[0];
1573                assert!(matches!(arg.value, Value::Int(10)));
1574            } else {
1575                panic!("Expected Selection::Field");
1576            }
1577        } else {
1578            panic!("Expected Query operation");
1579        }
1580    }
1581}