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::{self, Document, Field, Filter, Mutation, Query, Subscription, Value};
10use crate::types::{Collection, FieldType};
11use std::collections::HashMap;
12use std::fmt;
13
14/// Error codes for validation failures
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ErrorCode {
17    /// Referenced collection does not exist
18    UnknownCollection,
19    /// Referenced field does not exist in collection
20    UnknownField,
21    /// Value type doesn't match field type
22    TypeMismatch,
23    /// Required variable not provided
24    MissingRequiredVariable,
25    /// Optional variable used without default value
26    MissingOptionalVariable,
27    /// Filter operator not valid for field type
28    InvalidFilterOperator,
29    /// Duplicate alias in selection set
30    DuplicateAlias,
31    /// Invalid argument provided
32    InvalidArgument,
33    /// Unknown directive
34    UnknownDirective,
35}
36
37/// Validation error with context
38#[derive(Debug, Clone)]
39pub struct ValidationError {
40    pub code: ErrorCode,
41    pub message: String,
42    pub path: Option<String>,
43}
44
45impl fmt::Display for ValidationError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match &self.path {
48            Some(path) => write!(f, "[{}] {}", path, self.message),
49            None => write!(f, "{}", self.message),
50        }
51    }
52}
53
54impl std::error::Error for ValidationError {}
55
56impl ValidationError {
57    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
58        Self {
59            code,
60            message: message.into(),
61            path: None,
62        }
63    }
64
65    pub fn with_path(mut self, path: impl Into<String>) -> Self {
66        self.path = Some(path.into());
67        self
68    }
69}
70
71/// Validation result type
72pub type ValidationResult = Result<(), Vec<ValidationError>>;
73
74/// Schema provider trait for validation
75/// Allows validation without direct Aurora dependency
76pub trait SchemaProvider {
77    /// Get a collection definition by name
78    fn get_collection(&self, name: &str) -> Option<&Collection>;
79
80    /// Check if a collection exists
81    fn collection_exists(&self, name: &str) -> bool {
82        self.get_collection(name).is_some()
83    }
84}
85
86/// Simple in-memory schema for testing
87#[derive(Debug, Default)]
88pub struct InMemorySchema {
89    collections: HashMap<String, Collection>,
90}
91
92impl InMemorySchema {
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    pub fn add_collection(&mut self, collection: Collection) {
98        self.collections.insert(collection.name.clone(), collection);
99    }
100}
101
102impl SchemaProvider for InMemorySchema {
103    fn get_collection(&self, name: &str) -> Option<&Collection> {
104        self.collections.get(name)
105    }
106}
107
108/// Validation context holding schema and variable values
109pub struct ValidationContext<'a, S: SchemaProvider> {
110    pub schema: &'a S,
111    pub variables: HashMap<String, ast::Value>,
112    pub errors: Vec<ValidationError>,
113    current_path: Vec<String>,
114}
115
116impl<'a, S: SchemaProvider> ValidationContext<'a, S> {
117    pub fn new(schema: &'a S) -> Self {
118        Self {
119            schema,
120            variables: HashMap::new(),
121            errors: Vec::new(),
122            current_path: Vec::new(),
123        }
124    }
125
126    pub fn with_variables(mut self, variables: HashMap<String, ast::Value>) -> Self {
127        self.variables = variables;
128        self
129    }
130
131    fn push_path(&mut self, segment: &str) {
132        self.current_path.push(segment.to_string());
133    }
134
135    fn pop_path(&mut self) {
136        self.current_path.pop();
137    }
138
139    fn current_path_string(&self) -> Option<String> {
140        if self.current_path.is_empty() {
141            None
142        } else {
143            Some(self.current_path.join("."))
144        }
145    }
146
147    fn add_error(&mut self, code: ErrorCode, message: impl Into<String>) {
148        let mut error = ValidationError::new(code, message);
149        error.path = self.current_path_string();
150        self.errors.push(error);
151    }
152
153    pub fn has_errors(&self) -> bool {
154        !self.errors.is_empty()
155    }
156
157    pub fn into_result(self) -> ValidationResult {
158        if self.errors.is_empty() {
159            Ok(())
160        } else {
161            Err(self.errors)
162        }
163    }
164}
165
166/// Validate a complete AQL document
167pub fn validate_document<S: SchemaProvider>(
168    doc: &Document,
169    schema: &S,
170    variables: HashMap<String, ast::Value>,
171) -> ValidationResult {
172    let mut ctx = ValidationContext::new(schema).with_variables(variables);
173
174    for (i, op) in doc.operations.iter().enumerate() {
175        ctx.push_path(&format!("operation[{}]", i));
176        match op {
177            ast::Operation::Query(query) => validate_query(query, &mut ctx),
178            ast::Operation::Mutation(mutation) => validate_mutation(mutation, &mut ctx),
179            ast::Operation::Subscription(sub) => validate_subscription(sub, &mut ctx),
180            ast::Operation::Schema(_) => {} // Schema definitions don't need validation
181            ast::Operation::Migration(_) => {}
182            ast::Operation::FragmentDefinition(_) => {} // Fragment definitions validated when used
183            ast::Operation::Introspection(_) => {}      // Introspection is always valid
184            ast::Operation::Handler(_) => {}            // Handler definitions validated separately
185        }
186        ctx.pop_path();
187    }
188
189    ctx.into_result()
190}
191
192/// Validate a query operation
193fn validate_query<S: SchemaProvider>(query: &Query, ctx: &mut ValidationContext<'_, S>) {
194    if let Some(name) = &query.name {
195        ctx.push_path(name);
196    }
197
198    // Validate variable definitions
199    validate_variable_definitions(&query.variable_definitions, ctx);
200
201    // Validate selection set
202    for field in &query.selection_set {
203        validate_field(field, ctx);
204    }
205
206    if query.name.is_some() {
207        ctx.pop_path();
208    }
209}
210
211/// Validate a mutation operation
212fn validate_mutation<S: SchemaProvider>(mutation: &Mutation, ctx: &mut ValidationContext<'_, S>) {
213    if let Some(name) = &mutation.name {
214        ctx.push_path(name);
215    }
216
217    // Validate variable definitions
218    validate_variable_definitions(&mutation.variable_definitions, ctx);
219
220    // Validate each mutation operation
221    for (i, op) in mutation.operations.iter().enumerate() {
222        ctx.push_path(&format!("mutation[{}]", i));
223        validate_mutation_operation(op, ctx);
224        ctx.pop_path();
225    }
226
227    if mutation.name.is_some() {
228        ctx.pop_path();
229    }
230}
231
232/// Validate a subscription operation
233fn validate_subscription<S: SchemaProvider>(
234    sub: &Subscription,
235    ctx: &mut ValidationContext<'_, S>,
236) {
237    if let Some(name) = &sub.name {
238        ctx.push_path(name);
239    }
240
241    // Validate variable definitions
242    validate_variable_definitions(&sub.variable_definitions, ctx);
243
244    // Validate selection set
245    for field in &sub.selection_set {
246        validate_field(field, ctx);
247    }
248
249    if sub.name.is_some() {
250        ctx.pop_path();
251    }
252}
253
254/// Validate variable definitions
255fn validate_variable_definitions<S: SchemaProvider>(
256    definitions: &[ast::VariableDefinition],
257    ctx: &mut ValidationContext<'_, S>,
258) {
259    for def in definitions {
260        let var_name = &def.name;
261
262        // Check if required variable is provided
263        if def.var_type.is_required && def.default_value.is_none() {
264            if !ctx.variables.contains_key(var_name) {
265                ctx.add_error(
266                    ErrorCode::MissingRequiredVariable,
267                    format!("Required variable '{}' is not provided", var_name),
268                );
269            }
270        }
271    }
272}
273
274/// Validate a field selection
275fn validate_field<S: SchemaProvider>(field: &Field, ctx: &mut ValidationContext<'_, S>) {
276    let field_name = field.alias.as_ref().unwrap_or(&field.name);
277    ctx.push_path(field_name);
278
279    // Check for collection reference (top-level field)
280    if field.selection_set.is_empty() {
281        // Leaf field - no further validation needed for now
282    } else {
283        // This is a collection query
284        let collection_name = &field.name;
285
286        if !ctx.schema.collection_exists(collection_name) {
287            ctx.add_error(
288                ErrorCode::UnknownCollection,
289                format!("Collection '{}' does not exist", collection_name),
290            );
291        } else {
292            // Validate nested fields against collection schema
293            if let Some(collection) = ctx.schema.get_collection(collection_name) {
294                validate_selection_set(&field.selection_set, collection, ctx);
295            }
296        }
297
298        // Validate filter if present
299        for arg in &field.arguments {
300            if arg.name == "where" || arg.name == "filter" {
301                if let Some(filter) = extract_filter_from_value(&arg.value) {
302                    if let Some(collection) = ctx.schema.get_collection(collection_name) {
303                        validate_filter(&filter, collection, ctx);
304                    }
305                }
306            }
307        }
308    }
309
310    ctx.pop_path();
311}
312
313/// Validate selection set against collection schema
314fn validate_selection_set<S: SchemaProvider>(
315    fields: &[Field],
316    collection: &Collection,
317    ctx: &mut ValidationContext<'_, S>,
318) {
319    let mut aliases_seen: HashMap<String, bool> = HashMap::new();
320
321    for field in fields {
322        let display_name = field.alias.as_ref().unwrap_or(&field.name);
323
324        // Check for duplicate aliases
325        if aliases_seen.contains_key(display_name) {
326            ctx.add_error(
327                ErrorCode::DuplicateAlias,
328                format!("Duplicate field/alias '{}' in selection", display_name),
329            );
330        }
331        aliases_seen.insert(display_name.clone(), true);
332
333        // Check if field exists in collection (skip special fields)
334        let field_name = &field.name;
335        if !field_name.starts_with("__") && field_name != "id" {
336            if !collection.fields.contains_key(field_name) {
337                ctx.add_error(
338                    ErrorCode::UnknownField,
339                    format!(
340                        "Field '{}' does not exist in collection '{}'",
341                        field_name, collection.name
342                    ),
343                );
344            }
345        }
346    }
347}
348
349/// Validate a mutation operation
350fn validate_mutation_operation<S: SchemaProvider>(
351    op: &ast::MutationOperation,
352    ctx: &mut ValidationContext<'_, S>,
353) {
354    match &op.operation {
355        ast::MutationOp::Insert { collection, .. }
356        | ast::MutationOp::Update { collection, .. }
357        | ast::MutationOp::Upsert { collection, .. }
358        | ast::MutationOp::Delete { collection, .. } => {
359            if !ctx.schema.collection_exists(collection) {
360                ctx.add_error(
361                    ErrorCode::UnknownCollection,
362                    format!("Collection '{}' does not exist", collection),
363                );
364            }
365        }
366        ast::MutationOp::InsertMany { collection, .. } => {
367            if !ctx.schema.collection_exists(collection) {
368                ctx.add_error(
369                    ErrorCode::UnknownCollection,
370                    format!("Collection '{}' does not exist", collection),
371                );
372            }
373        }
374        ast::MutationOp::EnqueueJob { .. } => {
375            // Job validation - minimal for now
376        }
377        ast::MutationOp::Transaction { operations } => {
378            for (i, inner_op) in operations.iter().enumerate() {
379                ctx.push_path(&format!("tx[{}]", i));
380                validate_mutation_operation(inner_op, ctx);
381                ctx.pop_path();
382            }
383        }
384    }
385}
386
387/// Validate a filter expression
388fn validate_filter<S: SchemaProvider>(
389    filter: &Filter,
390    collection: &Collection,
391    ctx: &mut ValidationContext<'_, S>,
392) {
393    match filter {
394        Filter::Eq(field, value)
395        | Filter::Ne(field, value)
396        | Filter::Gt(field, value)
397        | Filter::Gte(field, value)
398        | Filter::Lt(field, value)
399        | Filter::Lte(field, value) => {
400            validate_filter_field(field, value, collection, ctx);
401        }
402        Filter::In(field, value) | Filter::NotIn(field, value) => {
403            // In/NotIn expects an array value
404            if !matches!(value, Value::Array(_)) {
405                ctx.add_error(
406                    ErrorCode::TypeMismatch,
407                    format!("Filter 'in'/'notIn' on '{}' requires an array value", field),
408                );
409            }
410            validate_filter_field_exists(field, collection, ctx);
411        }
412        Filter::Contains(field, _)
413        | Filter::StartsWith(field, _)
414        | Filter::EndsWith(field, _)
415        | Filter::Matches(field, _) => {
416            // String operations - check field is a string type
417            if let Some(field_def) = collection.fields.get(field) {
418                if field_def.field_type != FieldType::String {
419                    ctx.add_error(
420                        ErrorCode::InvalidFilterOperator,
421                        format!("String operator on non-string field '{}'", field),
422                    );
423                }
424            } else {
425                validate_filter_field_exists(field, collection, ctx);
426            }
427        }
428        Filter::IsNull(field) | Filter::IsNotNull(field) => {
429            validate_filter_field_exists(field, collection, ctx);
430        }
431        Filter::And(filters) | Filter::Or(filters) => {
432            for f in filters {
433                validate_filter(f, collection, ctx);
434            }
435        }
436        Filter::Not(inner) => {
437            validate_filter(inner, collection, ctx);
438        }
439    }
440}
441
442/// Validate a filter field exists
443fn validate_filter_field_exists<S: SchemaProvider>(
444    field: &str,
445    collection: &Collection,
446    ctx: &mut ValidationContext<'_, S>,
447) {
448    if field != "id" && !collection.fields.contains_key(field) {
449        ctx.add_error(
450            ErrorCode::UnknownField,
451            format!(
452                "Filter field '{}' does not exist in collection '{}'",
453                field, collection.name
454            ),
455        );
456    }
457}
458
459/// Validate a filter field with value type checking
460fn validate_filter_field<S: SchemaProvider>(
461    field: &str,
462    value: &Value,
463    collection: &Collection,
464    ctx: &mut ValidationContext<'_, S>,
465) {
466    if field == "id" {
467        return; // id field always exists
468    }
469
470    if let Some(field_def) = collection.fields.get(field) {
471        // Check type compatibility
472        if !is_type_compatible(&field_def.field_type, value) {
473            ctx.add_error(
474                ErrorCode::TypeMismatch,
475                format!(
476                    "Type mismatch: field '{}' expects {:?}, got {:?}",
477                    field,
478                    field_def.field_type,
479                    value_type_name(value)
480                ),
481            );
482        }
483    } else {
484        ctx.add_error(
485            ErrorCode::UnknownField,
486            format!(
487                "Filter field '{}' does not exist in collection '{}'",
488                field, collection.name
489            ),
490        );
491    }
492}
493
494/// Check if a value is compatible with a field type
495fn is_type_compatible(field_type: &FieldType, value: &Value) -> bool {
496    match (field_type, value) {
497        (_, Value::Null) => true,        // Null is compatible with any type
498        (_, Value::Variable(_)) => true, // Variables are resolved later
499        (FieldType::String, Value::String(_)) => true,
500        (FieldType::Int, Value::Int(_)) => true,
501        (FieldType::Float, Value::Float(_)) => true,
502        (FieldType::Float, Value::Int(_)) => true, // Int can be used where Float expected
503        (FieldType::Bool, Value::Boolean(_)) => true,
504        (FieldType::Array, Value::Array(_)) => true,
505        (FieldType::Object, Value::Object(_)) => true,
506        (FieldType::Any, _) => true, // Any type accepts all values
507        (FieldType::Uuid, Value::String(_)) => true, // UUIDs are often passed as strings
508        _ => false,
509    }
510}
511
512/// Get a human-readable type name for a value
513fn value_type_name(value: &Value) -> &'static str {
514    match value {
515        Value::Null => "null",
516        Value::Boolean(_) => "boolean",
517        Value::Int(_) => "int",
518        Value::Float(_) => "float",
519        Value::String(_) => "string",
520        Value::Array(_) => "array",
521        Value::Object(_) => "object",
522        Value::Variable(_) => "variable",
523        Value::Enum(_) => "enum",
524    }
525}
526
527/// Extract a Filter from an AST Value (for parsing where arguments)
528fn extract_filter_from_value(value: &Value) -> Option<Filter> {
529    match value {
530        Value::Object(map) => {
531            let mut filters = Vec::new();
532
533            for (key, val) in map {
534                match key.as_str() {
535                    "and" => {
536                        if let Value::Array(arr) = val {
537                            let sub_filters: Vec<Filter> =
538                                arr.iter().filter_map(extract_filter_from_value).collect();
539                            if !sub_filters.is_empty() {
540                                filters.push(Filter::And(sub_filters));
541                            }
542                        }
543                    }
544                    "or" => {
545                        if let Value::Array(arr) = val {
546                            let sub_filters: Vec<Filter> =
547                                arr.iter().filter_map(extract_filter_from_value).collect();
548                            if !sub_filters.is_empty() {
549                                filters.push(Filter::Or(sub_filters));
550                            }
551                        }
552                    }
553                    "not" => {
554                        if let Some(inner) = extract_filter_from_value(val) {
555                            filters.push(Filter::Not(Box::new(inner)));
556                        }
557                    }
558                    field => {
559                        // Field-level filter: { field: { eq: value } }
560                        if let Value::Object(ops) = val {
561                            for (op, op_val) in ops {
562                                let filter = match op.as_str() {
563                                    "eq" => Some(Filter::Eq(field.to_string(), op_val.clone())),
564                                    "ne" => Some(Filter::Ne(field.to_string(), op_val.clone())),
565                                    "gt" => Some(Filter::Gt(field.to_string(), op_val.clone())),
566                                    "gte" => Some(Filter::Gte(field.to_string(), op_val.clone())),
567                                    "lt" => Some(Filter::Lt(field.to_string(), op_val.clone())),
568                                    "lte" => Some(Filter::Lte(field.to_string(), op_val.clone())),
569                                    "in" => Some(Filter::In(field.to_string(), op_val.clone())),
570                                    "nin" => Some(Filter::NotIn(field.to_string(), op_val.clone())),
571                                    "contains" => {
572                                        Some(Filter::Contains(field.to_string(), op_val.clone()))
573                                    }
574                                    "startsWith" => {
575                                        Some(Filter::StartsWith(field.to_string(), op_val.clone()))
576                                    }
577                                    "endsWith" => {
578                                        Some(Filter::EndsWith(field.to_string(), op_val.clone()))
579                                    }
580                                    "isNull" => Some(Filter::IsNull(field.to_string())),
581                                    "isNotNull" => Some(Filter::IsNotNull(field.to_string())),
582                                    _ => None,
583                                };
584                                if let Some(f) = filter {
585                                    filters.push(f);
586                                }
587                            }
588                        }
589                    }
590                }
591            }
592
593            match filters.len() {
594                0 => None,
595                1 => Some(filters.remove(0)),
596                _ => Some(Filter::And(filters)),
597            }
598        }
599        _ => None,
600    }
601}
602
603/// Resolve variables in a document, replacing Value::Variable with actual values
604pub fn resolve_variables(
605    doc: &mut Document,
606    variables: &HashMap<String, ast::Value>,
607) -> Result<(), ValidationError> {
608    for op in &mut doc.operations {
609        match op {
610            ast::Operation::Query(query) => {
611                resolve_in_fields(&mut query.selection_set, variables)?;
612            }
613            ast::Operation::Mutation(mutation) => {
614                for mut_op in &mut mutation.operations {
615                    resolve_in_mutation_op(mut_op, variables)?;
616                }
617            }
618            ast::Operation::Subscription(sub) => {
619                resolve_in_fields(&mut sub.selection_set, variables)?;
620            }
621            ast::Operation::Schema(_) => {}
622            ast::Operation::Migration(_) => {}
623            ast::Operation::FragmentDefinition(_) => {} // Handled when fragment is used
624            ast::Operation::Introspection(_) => {}      // No variables in introspection
625            ast::Operation::Handler(_) => {}            // Handlers don't have variable resolution
626        }
627    }
628    Ok(())
629}
630
631fn resolve_in_fields(
632    fields: &mut [Field],
633    variables: &HashMap<String, ast::Value>,
634) -> Result<(), ValidationError> {
635    for field in fields {
636        // Resolve variables in arguments
637        for arg in &mut field.arguments {
638            resolve_in_value(&mut arg.value, variables)?;
639        }
640        // Recursively resolve in nested selection
641        resolve_in_fields(&mut field.selection_set, variables)?;
642    }
643    Ok(())
644}
645
646fn resolve_in_mutation_op(
647    op: &mut ast::MutationOperation,
648    variables: &HashMap<String, ast::Value>,
649) -> Result<(), ValidationError> {
650    match &mut op.operation {
651        ast::MutationOp::Insert { data, .. }
652        | ast::MutationOp::Update { data, .. }
653        | ast::MutationOp::Upsert { data, .. } => {
654            resolve_in_value(data, variables)?;
655        }
656        ast::MutationOp::InsertMany { data, .. } => {
657            for item in data {
658                resolve_in_value(item, variables)?;
659            }
660        }
661        ast::MutationOp::Delete { .. } => {}
662        ast::MutationOp::EnqueueJob { payload, .. } => {
663            resolve_in_value(payload, variables)?;
664        }
665        ast::MutationOp::Transaction { operations } => {
666            for inner in operations {
667                resolve_in_mutation_op(inner, variables)?;
668            }
669        }
670    }
671    resolve_in_fields(&mut op.selection_set, variables)
672}
673
674fn resolve_in_value(
675    value: &mut Value,
676    variables: &HashMap<String, ast::Value>,
677) -> Result<(), ValidationError> {
678    match value {
679        Value::Variable(name) => {
680            if let Some(resolved) = variables.get(name) {
681                *value = resolved.clone();
682            } else {
683                return Err(ValidationError::new(
684                    ErrorCode::MissingRequiredVariable,
685                    format!("Variable '{}' is not provided", name),
686                ));
687            }
688        }
689        Value::Array(items) => {
690            for item in items {
691                resolve_in_value(item, variables)?;
692            }
693        }
694        Value::Object(map) => {
695            for v in map.values_mut() {
696                resolve_in_value(v, variables)?;
697            }
698        }
699        _ => {}
700    }
701    Ok(())
702}
703
704#[cfg(test)]
705mod tests {
706    use super::*;
707    use crate::types::FieldDefinition;
708
709    fn create_test_schema() -> InMemorySchema {
710        let mut schema = InMemorySchema::new();
711
712        let mut users_fields = HashMap::new();
713        users_fields.insert(
714            "name".to_string(),
715            FieldDefinition {
716                field_type: FieldType::String,
717                unique: false,
718                indexed: false,
719            },
720        );
721        users_fields.insert(
722            "email".to_string(),
723            FieldDefinition {
724                field_type: FieldType::String,
725                unique: true,
726                indexed: true,
727            },
728        );
729        users_fields.insert(
730            "age".to_string(),
731            FieldDefinition {
732                field_type: FieldType::Int,
733                unique: false,
734                indexed: false,
735            },
736        );
737        users_fields.insert(
738            "active".to_string(),
739            FieldDefinition {
740                field_type: FieldType::Bool,
741                unique: false,
742                indexed: false,
743            },
744        );
745
746        schema.add_collection(Collection {
747            name: "users".to_string(),
748            fields: users_fields,
749        });
750
751        schema
752    }
753
754    #[test]
755    fn test_validate_unknown_collection() {
756        let schema = create_test_schema();
757        let doc = Document {
758            operations: vec![ast::Operation::Query(Query {
759                name: None,
760                variable_definitions: vec![],
761                directives: vec![],
762                selection_set: vec![Field {
763                    alias: None,
764                    name: "nonexistent".to_string(),
765                    arguments: vec![],
766                    directives: vec![],
767                    selection_set: vec![Field {
768                        alias: None,
769                        name: "id".to_string(),
770                        arguments: vec![],
771                        directives: vec![],
772                        selection_set: vec![],
773                    }],
774                }],
775                variables_values: HashMap::new(),
776            })],
777        };
778
779        let result = validate_document(&doc, &schema, HashMap::new());
780        assert!(result.is_err());
781        let errors = result.unwrap_err();
782        assert!(
783            errors
784                .iter()
785                .any(|e| e.code == ErrorCode::UnknownCollection)
786        );
787    }
788
789    #[test]
790    fn test_validate_unknown_field() {
791        let schema = create_test_schema();
792        let doc = Document {
793            operations: vec![ast::Operation::Query(Query {
794                name: None,
795                variable_definitions: vec![],
796                directives: vec![],
797                selection_set: vec![Field {
798                    alias: None,
799                    name: "users".to_string(),
800                    arguments: vec![],
801                    directives: vec![],
802                    selection_set: vec![Field {
803                        alias: None,
804                        name: "nonexistent_field".to_string(),
805                        arguments: vec![],
806                        directives: vec![],
807                        selection_set: vec![],
808                    }],
809                }],
810                variables_values: HashMap::new(),
811            })],
812        };
813
814        let result = validate_document(&doc, &schema, HashMap::new());
815        assert!(result.is_err());
816        let errors = result.unwrap_err();
817        assert!(errors.iter().any(|e| e.code == ErrorCode::UnknownField));
818    }
819
820    #[test]
821    fn test_validate_missing_required_variable() {
822        let schema = create_test_schema();
823        let doc = Document {
824            operations: vec![ast::Operation::Query(Query {
825                name: Some("GetUsers".to_string()),
826                variable_definitions: vec![ast::VariableDefinition {
827                    name: "minAge".to_string(),
828                    var_type: ast::TypeAnnotation {
829                        name: "Int".to_string(),
830                        is_array: false,
831                        is_required: true,
832                    },
833                    default_value: None,
834                }],
835                directives: vec![],
836                selection_set: vec![],
837                variables_values: HashMap::new(),
838            })],
839        };
840
841        // No variables provided
842        let result = validate_document(&doc, &schema, HashMap::new());
843        assert!(result.is_err());
844        let errors = result.unwrap_err();
845        assert!(
846            errors
847                .iter()
848                .any(|e| e.code == ErrorCode::MissingRequiredVariable)
849        );
850    }
851
852    #[test]
853    fn test_validate_valid_query() {
854        let schema = create_test_schema();
855        let doc = Document {
856            operations: vec![ast::Operation::Query(Query {
857                name: Some("GetUsers".to_string()),
858                variable_definitions: vec![],
859                directives: vec![],
860                selection_set: vec![Field {
861                    alias: None,
862                    name: "users".to_string(),
863                    arguments: vec![],
864                    directives: vec![],
865                    selection_set: vec![
866                        Field {
867                            alias: None,
868                            name: "id".to_string(),
869                            arguments: vec![],
870                            directives: vec![],
871                            selection_set: vec![],
872                        },
873                        Field {
874                            alias: None,
875                            name: "name".to_string(),
876                            arguments: vec![],
877                            directives: vec![],
878                            selection_set: vec![],
879                        },
880                        Field {
881                            alias: None,
882                            name: "email".to_string(),
883                            arguments: vec![],
884                            directives: vec![],
885                            selection_set: vec![],
886                        },
887                    ],
888                }],
889                variables_values: HashMap::new(),
890            })],
891        };
892
893        let result = validate_document(&doc, &schema, HashMap::new());
894        assert!(result.is_ok());
895    }
896
897    #[test]
898    fn test_validate_filter_type_mismatch() {
899        let schema = create_test_schema();
900        let collection = schema.get_collection("users").unwrap();
901        let mut ctx = ValidationContext::new(&schema);
902
903        // age is Int, but we're filtering with a string
904        let filter = Filter::Eq("age".to_string(), Value::String("not a number".to_string()));
905        validate_filter(&filter, collection, &mut ctx);
906
907        assert!(ctx.has_errors());
908        assert!(ctx.errors.iter().any(|e| e.code == ErrorCode::TypeMismatch));
909    }
910
911    #[test]
912    fn test_validate_filter_string_operator_on_int() {
913        let schema = create_test_schema();
914        let collection = schema.get_collection("users").unwrap();
915        let mut ctx = ValidationContext::new(&schema);
916
917        // contains on int field should fail
918        let filter = Filter::Contains("age".to_string(), Value::String("10".to_string()));
919        validate_filter(&filter, collection, &mut ctx);
920
921        assert!(ctx.has_errors());
922        assert!(
923            ctx.errors
924                .iter()
925                .any(|e| e.code == ErrorCode::InvalidFilterOperator)
926        );
927    }
928
929    #[test]
930    fn test_resolve_variables() {
931        let mut doc = Document {
932            operations: vec![ast::Operation::Query(Query {
933                name: None,
934                variable_definitions: vec![],
935                directives: vec![],
936                selection_set: vec![Field {
937                    alias: None,
938                    name: "users".to_string(),
939                    arguments: vec![ast::Argument {
940                        name: "limit".to_string(),
941                        value: Value::Variable("pageSize".to_string()),
942                    }],
943                    directives: vec![],
944                    selection_set: vec![],
945                }],
946                variables_values: HashMap::new(),
947            })],
948        };
949
950        let mut vars = HashMap::new();
951        vars.insert("pageSize".to_string(), Value::Int(10));
952
953        let result = resolve_variables(&mut doc, &vars);
954        assert!(result.is_ok());
955
956        // Check that variable was resolved
957        if let ast::Operation::Query(query) = &doc.operations[0] {
958            let arg = &query.selection_set[0].arguments[0];
959            assert!(matches!(arg.value, Value::Int(10)));
960        } else {
961            panic!("Expected Query operation");
962        }
963    }
964}