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, Selection, Subscription, Value};
10use crate::types::{Collection, FieldType, ScalarType};
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    /// Explicitly invalid input (e.g. null where not allowed)
22    InvalidInput,
23    /// Value type doesn't match field type
24    TypeMismatch,
25    /// Required variable not provided
26    MissingRequiredVariable,
27    /// Optional variable used without default value
28    MissingOptionalVariable,
29    /// Filter operator not valid for field type
30    InvalidFilterOperator,
31    /// Duplicate alias in selection set
32    DuplicateAlias,
33    /// Invalid argument provided
34    InvalidArgument,
35    /// Unknown directive
36    UnknownDirective,
37}
38
39/// Validation error with context
40#[derive(Debug, Clone)]
41pub struct ValidationError {
42    pub code: ErrorCode,
43    pub message: String,
44    pub path: Option<String>,
45}
46
47impl fmt::Display for ValidationError {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match &self.path {
50            Some(path) => write!(f, "[{}] {}", path, self.message),
51            None => write!(f, "{}", self.message),
52        }
53    }
54}
55
56impl std::error::Error for ValidationError {}
57
58impl ValidationError {
59    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
60        Self {
61            code,
62            message: message.into(),
63            path: None,
64        }
65    }
66
67    pub fn with_path(mut self, path: impl Into<String>) -> Self {
68        self.path = Some(path.into());
69        self
70    }
71}
72
73/// Validation result type
74pub type ValidationResult = Result<(), Vec<ValidationError>>;
75
76/// Schema provider trait for validation
77/// Allows validation without direct Aurora dependency
78pub trait SchemaProvider {
79    /// Get a collection definition by name
80    fn get_collection(&self, name: &str) -> Option<&Collection>;
81
82    /// Check if a collection exists
83    fn collection_exists(&self, name: &str) -> bool {
84        self.get_collection(name).is_some()
85    }
86}
87
88/// Simple in-memory schema for testing
89#[derive(Debug, Default)]
90pub struct InMemorySchema {
91    collections: HashMap<String, Collection>,
92}
93
94impl InMemorySchema {
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    pub fn add_collection(&mut self, collection: Collection) {
100        self.collections.insert(collection.name.clone(), collection);
101    }
102}
103
104impl SchemaProvider for InMemorySchema {
105    fn get_collection(&self, name: &str) -> Option<&Collection> {
106        self.collections.get(name)
107    }
108}
109
110/// Validation context holding schema and variable values
111pub struct ValidationContext<'a, S: SchemaProvider> {
112    pub schema: &'a S,
113    pub variables: HashMap<String, ast::Value>,
114    pub errors: Vec<ValidationError>,
115    current_path: Vec<String>,
116}
117
118impl<'a, S: SchemaProvider> ValidationContext<'a, S> {
119    pub fn new(schema: &'a S) -> Self {
120        Self {
121            schema,
122            variables: HashMap::new(),
123            errors: Vec::new(),
124            current_path: Vec::new(),
125        }
126    }
127
128    pub fn with_variables(mut self, variables: HashMap<String, ast::Value>) -> Self {
129        self.variables = variables;
130        self
131    }
132
133    fn push_path(&mut self, segment: &str) {
134        self.current_path.push(segment.to_string());
135    }
136
137    fn pop_path(&mut self) {
138        self.current_path.pop();
139    }
140
141    fn current_path_string(&self) -> Option<String> {
142        if self.current_path.is_empty() {
143            None
144        } else {
145            Some(self.current_path.join("."))
146        }
147    }
148
149    fn add_error(&mut self, code: ErrorCode, message: impl Into<String>) {
150        let mut error = ValidationError::new(code, message);
151        error.path = self.current_path_string();
152        self.errors.push(error);
153    }
154
155    pub fn has_errors(&self) -> bool {
156        !self.errors.is_empty()
157    }
158
159    pub fn into_result(self) -> ValidationResult {
160        if self.errors.is_empty() {
161            Ok(())
162        } else {
163            Err(self.errors)
164        }
165    }
166}
167
168/// Validate a complete AQL document
169pub fn validate_document<S: SchemaProvider>(
170    doc: &Document,
171    schema: &S,
172    variables: HashMap<String, ast::Value>,
173) -> ValidationResult {
174    let mut ctx = ValidationContext::new(schema).with_variables(variables);
175
176    for (i, op) in doc.operations.iter().enumerate() {
177        ctx.push_path(&format!("operation[{}]", i));
178        match op {
179            ast::Operation::Query(query) => validate_query(query, &mut ctx),
180            ast::Operation::Mutation(mutation) => validate_mutation(mutation, &mut ctx),
181            ast::Operation::Subscription(sub) => validate_subscription(sub, &mut ctx),
182            ast::Operation::Schema(_) => {} // Schema definitions don't need validation
183            ast::Operation::Migration(_) => {}
184            ast::Operation::FragmentDefinition(_) => {} // Fragment definitions validated when used
185            ast::Operation::Introspection(_) => {}      // Introspection is always valid
186            ast::Operation::Handler(_) => {}            // Handler definitions validated separately
187        }
188        ctx.pop_path();
189    }
190
191    ctx.into_result()
192}
193
194/// Validate a query operation
195fn validate_query<S: SchemaProvider>(query: &Query, ctx: &mut ValidationContext<'_, S>) {
196    if let Some(name) = &query.name {
197        ctx.push_path(name);
198    }
199
200    // Validate variable definitions
201    validate_variable_definitions(&query.variable_definitions, ctx);
202
203    // Validate selection set
204    for selection in &query.selection_set {
205        if let Selection::Field(field) = selection {
206            validate_field(field, ctx);
207        } else if let Selection::InlineFragment(inline) = selection {
208            validate_inline_fragment(inline, ctx);
209        }
210    }
211
212    if query.name.is_some() {
213        ctx.pop_path();
214    }
215}
216
217/// Validate a mutation operation
218fn validate_mutation<S: SchemaProvider>(mutation: &Mutation, ctx: &mut ValidationContext<'_, S>) {
219    if let Some(name) = &mutation.name {
220        ctx.push_path(name);
221    }
222
223    // Validate variable definitions
224    validate_variable_definitions(&mutation.variable_definitions, ctx);
225
226    // Validate each mutation operation
227    for (i, op) in mutation.operations.iter().enumerate() {
228        ctx.push_path(&format!("mutation[{}]", i));
229        validate_mutation_operation(op, ctx);
230        ctx.pop_path();
231    }
232
233    if mutation.name.is_some() {
234        ctx.pop_path();
235    }
236}
237
238/// Validate an inline fragment
239fn validate_inline_fragment<S: SchemaProvider>(
240    inline: &ast::InlineFragment,
241    ctx: &mut ValidationContext<'_, S>,
242) {
243    // Check if type condition refers to a valid collection
244    if !ctx.schema.collection_exists(&inline.type_condition) {
245        ctx.add_error(
246            ErrorCode::UnknownCollection,
247            format!(
248                "Unknown collection '{}' in inline fragment",
249                inline.type_condition
250            ),
251        );
252        return;
253    }
254
255    // Validate inner selection set using the type condition as context
256    validate_selection_set(&inline.selection_set, &inline.type_condition, ctx);
257}
258
259/// Validate a subscription operation
260fn validate_subscription<S: SchemaProvider>(
261    sub: &Subscription,
262    ctx: &mut ValidationContext<'_, S>,
263) {
264    if let Some(name) = &sub.name {
265        ctx.push_path(name);
266    }
267
268    // Validate variable definitions
269    validate_variable_definitions(&sub.variable_definitions, ctx);
270
271    // Validate selection set
272    for selection in &sub.selection_set {
273        if let Selection::Field(field) = selection {
274            validate_field(field, ctx);
275        }
276    }
277
278    if sub.name.is_some() {
279        ctx.pop_path();
280    }
281}
282
283/// Validate variable definitions
284fn validate_variable_definitions<S: SchemaProvider>(
285    definitions: &[ast::VariableDefinition],
286    ctx: &mut ValidationContext<'_, S>,
287) {
288    for def in definitions {
289        let var_name = &def.name;
290
291        // Check if required variable is provided
292        if def.var_type.is_required && def.default_value.is_none() {
293            if !ctx.variables.contains_key(var_name) {
294                ctx.add_error(
295                    ErrorCode::MissingRequiredVariable,
296                    format!("Required variable '{}' is not provided", var_name),
297                );
298            }
299        }
300    }
301}
302
303/// Validate a field selection
304fn validate_field<S: SchemaProvider>(field: &Field, ctx: &mut ValidationContext<'_, S>) {
305    let field_name = field.alias.as_ref().unwrap_or(&field.name);
306    ctx.push_path(field_name);
307
308    // Check for collection reference (top-level field)
309    if field.selection_set.is_empty() {
310        // Leaf field - no further validation needed for now
311    } else {
312        // This is a collection query
313        let collection_name = &field.name;
314
315        if !ctx.schema.collection_exists(collection_name) {
316            ctx.add_error(
317                ErrorCode::UnknownCollection,
318                format!("Collection '{}' does not exist", collection_name),
319            );
320        } else {
321            // Validate nested fields against collection schema
322            if let Some(collection) = ctx.schema.get_collection(collection_name) {
323                validate_selection_set(&field.selection_set, &collection.name, ctx);
324            }
325        }
326
327        // Validate filter if present
328        for arg in &field.arguments {
329            if arg.name == "where" || arg.name == "filter" {
330                if let Some(filter) = extract_filter_from_value(&arg.value) {
331                    if let Some(collection) = ctx.schema.get_collection(collection_name) {
332                        validate_filter(&filter, collection, ctx);
333                    }
334                }
335            }
336        }
337    }
338
339    ctx.pop_path();
340}
341
342/// Validate selection set against collection schema
343fn validate_selection_set<S: SchemaProvider>(
344    selections: &[Selection],
345    collection_name: &str,
346    ctx: &mut ValidationContext<'_, S>,
347) {
348    let mut aliases_seen: HashMap<String, bool> = HashMap::new();
349
350    for selection in selections {
351        // Only validate Fields for now, fragments skipped
352        if let Selection::Field(field) = selection {
353            let display_name = field.alias.as_ref().unwrap_or(&field.name);
354
355            // Check for duplicate aliases
356            if aliases_seen.contains_key(display_name) {
357                ctx.add_error(
358                    ErrorCode::DuplicateAlias,
359                    format!("Duplicate field/alias '{}' in selection", display_name),
360                );
361            }
362            aliases_seen.insert(display_name.clone(), true);
363
364            // Check if field exists in collection schema
365            // We need to look up the collection definition from the schema provider
366            if let Some(collection) = ctx.schema.get_collection(collection_name) {
367                let field_name = &field.name;
368                if !field_name.starts_with("__") && field_name != "id" {
369                    if !collection.fields.contains_key(field_name) {
370                        ctx.add_error(
371                            ErrorCode::UnknownField,
372                            format!(
373                                "Field '{}' does not exist in collection '{}'",
374                                field_name, collection.name
375                            ),
376                        );
377                    }
378                }
379
380                // Recursively validate nested selection
381                validate_field(field, ctx);
382            }
383        } else if let Selection::InlineFragment(inline) = selection {
384            validate_inline_fragment(inline, ctx);
385        }
386    }
387}
388
389/// Validate a mutation operation
390fn validate_mutation_operation<S: SchemaProvider>(
391    op: &ast::MutationOperation,
392    ctx: &mut ValidationContext<'_, S>,
393) {
394    match &op.operation {
395        ast::MutationOp::Insert { collection, data } => {
396            if let Some(col_def) = ctx.schema.get_collection(collection) {
397                validate_object_against_schema(data, col_def, ctx);
398            } else {
399                ctx.add_error(
400                    ErrorCode::UnknownCollection,
401                    format!("Collection '{}' does not exist", collection),
402                );
403            }
404        }
405        ast::MutationOp::InsertMany { collection, data } => {
406            if let Some(col_def) = ctx.schema.get_collection(collection) {
407                for item in data {
408                    validate_object_against_schema(item, col_def, ctx);
409                }
410            } else {
411                ctx.add_error(
412                    ErrorCode::UnknownCollection,
413                    format!("Collection '{}' does not exist", collection),
414                );
415            }
416        }
417        ast::MutationOp::Update {
418            collection, data, ..
419        }
420        | ast::MutationOp::Upsert {
421            collection, data, ..
422        } => {
423            if let Some(col_def) = ctx.schema.get_collection(collection) {
424                // Validate partial object for updates (skipping required checks)
425                validate_partial_object(data, col_def, ctx);
426            } else {
427                ctx.add_error(
428                    ErrorCode::UnknownCollection,
429                    format!("Collection '{}' does not exist", collection),
430                );
431            }
432        }
433        ast::MutationOp::Delete { collection, .. } => {
434            if !ctx.schema.collection_exists(collection) {
435                ctx.add_error(
436                    ErrorCode::UnknownCollection,
437                    format!("Collection '{}' does not exist", collection),
438                );
439            }
440        }
441        ast::MutationOp::EnqueueJob { .. } => {
442            // Job validation - minimal for now
443        }
444        ast::MutationOp::Transaction { operations } => {
445            for (i, inner_op) in operations.iter().enumerate() {
446                ctx.push_path(&format!("tx[{}]", i));
447                validate_mutation_operation(inner_op, ctx);
448                ctx.pop_path();
449            }
450        }
451    }
452}
453
454/// Validate an object value against a collection schema
455fn validate_object_against_schema<S: SchemaProvider>(
456    value: &Value,
457    collection: &Collection,
458    ctx: &mut ValidationContext<'_, S>,
459) {
460    match value {
461        Value::Object(map) => {
462            // 1. Check provided fields validity
463            for (key, val) in map {
464                if let Some(field_def) = collection.fields.get(key) {
465                    // Check if null is allowed
466                    if matches!(val, Value::Null) && !field_def.nullable {
467                        ctx.add_error(
468                            ErrorCode::InvalidInput,
469                            format!("Field '{}' cannot be null", key),
470                        );
471                    } else if !matches!(val, Value::Null)
472                        && !validate_value_against_type(val, &field_def.field_type)
473                    {
474                        ctx.add_error(
475                            ErrorCode::TypeMismatch,
476                            format!(
477                                "Type mismatch for field '{}': expected {:?}, got {:?}",
478                                key,
479                                field_def.field_type,
480                                value_type_name(val)
481                            ),
482                        );
483                    }
484                } else if key != "id" {
485                    ctx.add_error(
486                        ErrorCode::UnknownField,
487                        format!(
488                            "Field '{}' not defined in collection '{}'",
489                            key, collection.name
490                        ),
491                    );
492                }
493            }
494
495            // 2. Check for missing required fields
496            for (name, def) in &collection.fields {
497                if !def.nullable && !map.contains_key(name) {
498                    ctx.add_error(
499                        ErrorCode::InvalidInput,
500                        format!("Missing required field '{}'", name),
501                    );
502                }
503            }
504        }
505        _ => {
506            ctx.add_error(
507                ErrorCode::TypeMismatch,
508                format!(
509                    "Expected object for collection '{}', got {:?}",
510                    collection.name,
511                    value_type_name(value)
512                ),
513            );
514        }
515    }
516}
517
518/// Validate a partial object (field subset) against a collection schema
519/// Used for Update operations where missing required fields are allowed
520fn validate_partial_object<S: SchemaProvider>(
521    value: &Value,
522    collection: &Collection,
523    ctx: &mut ValidationContext<'_, S>,
524) {
525    match value {
526        Value::Object(map) => {
527            // Check provided fields validity
528            for (key, val) in map {
529                if let Some(field_def) = collection.fields.get(key) {
530                    // Check if null is allowed
531                    if matches!(val, Value::Null) && !field_def.nullable {
532                        ctx.add_error(
533                            ErrorCode::InvalidInput,
534                            format!("Field '{}' cannot be null", key),
535                        );
536                    } else if !matches!(val, Value::Null)
537                        && !validate_value_against_type(val, &field_def.field_type)
538                    {
539                        ctx.add_error(
540                            ErrorCode::TypeMismatch,
541                            format!(
542                                "Type mismatch for field '{}': expected {:?}, got {:?}",
543                                key,
544                                field_def.field_type,
545                                value_type_name(val)
546                            ),
547                        );
548                    }
549                } else if key != "id" {
550                    ctx.add_error(
551                        ErrorCode::UnknownField,
552                        format!(
553                            "Field '{}' not defined in collection '{}'",
554                            key, collection.name
555                        ),
556                    );
557                }
558            }
559        }
560        _ => {
561            ctx.add_error(
562                ErrorCode::TypeMismatch,
563                format!(
564                    "Expected object for collection '{}', got {:?}",
565                    collection.name,
566                    value_type_name(value)
567                ),
568            );
569        }
570    }
571}
572
573/// Recursively validate value against type
574fn validate_value_against_type(value: &Value, expected: &FieldType) -> bool {
575    use crate::types::ScalarType;
576    match (expected, value) {
577        // Handle Scalar Types
578        (FieldType::Scalar(ScalarType::Any), _) => true,
579        (_, Value::Null) => true,
580        (_, Value::Variable(_)) => true,
581
582        (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
583        (FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
584        (FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
585        (FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true,
586        (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
587        (FieldType::Scalar(ScalarType::Uuid), Value::String(_)) => true,
588
589        // Handle Array Types
590        (FieldType::Array(scalar_inner), Value::Array(items)) => {
591            // Check each item matches the scalar inner type
592            let inner_field_type = FieldType::Scalar(scalar_inner.clone());
593            items
594                .iter()
595                .all(|item| validate_value_against_type(item, &inner_field_type))
596        }
597
598        // Handle Objects
599        (FieldType::Object, Value::Object(_)) => true,
600        // Handle Nested Objects (deep validation)
601        (FieldType::Nested(schema), Value::Object(map)) => {
602            // 1. Check all provided fields exist in schema and match type
603            for (key, val) in map {
604                if let Some(def) = schema.get(key) {
605                    if matches!(val, Value::Null) {
606                        if !def.nullable {
607                            return false; // Null not allowed
608                        }
609                    } else if !validate_value_against_type(val, &def.field_type) {
610                        return false; // Type mismatch in nested field
611                    }
612                } else {
613                    return false; // Unknown field in nested object
614                }
615            }
616            // 2. Check all required fields are provided
617            for (key, def) in schema.iter() {
618                if !def.nullable && !map.contains_key(key) {
619                    return false; // Missing required nested field
620                }
621            }
622            true
623        }
624        (FieldType::Nested(_), _) => false, // Not an object
625
626        // Handle ScalarType::Object (e.g. inside Array<Object>)
627        (FieldType::Scalar(ScalarType::Object), Value::Object(_)) => true,
628        // Handle ScalarType::Array (e.g. inside Array<Array>)
629        (FieldType::Scalar(ScalarType::Array), Value::Array(_)) => true,
630
631        _ => false,
632    }
633}
634
635/// Validate a filter expression
636fn validate_filter<S: SchemaProvider>(
637    filter: &Filter,
638    collection: &Collection,
639    ctx: &mut ValidationContext<'_, S>,
640) {
641    match filter {
642        Filter::Eq(field, value)
643        | Filter::Ne(field, value)
644        | Filter::Gt(field, value)
645        | Filter::Gte(field, value)
646        | Filter::Lt(field, value)
647        | Filter::Lte(field, value) => {
648            validate_filter_field(field, value, collection, ctx);
649        }
650        Filter::In(field, value) | Filter::NotIn(field, value) => {
651            // In/NotIn expects an array value
652            if !matches!(value, Value::Array(_)) {
653                ctx.add_error(
654                    ErrorCode::TypeMismatch,
655                    format!("Filter 'in'/'notIn' on '{}' requires an array value", field),
656                );
657            }
658            validate_filter_field_exists(field, collection, ctx);
659        }
660        Filter::Contains(field, _)
661        | Filter::StartsWith(field, _)
662        | Filter::EndsWith(field, _)
663        | Filter::Matches(field, _) => {
664            // String operations - check field is a string type
665            if let Some(field_def) = collection.fields.get(field) {
666                if field_def.field_type != FieldType::String {
667                    ctx.add_error(
668                        ErrorCode::InvalidFilterOperator,
669                        format!("String operator on non-string field '{}'", field),
670                    );
671                }
672            } else {
673                validate_filter_field_exists(field, collection, ctx);
674            }
675        }
676        Filter::IsNull(field) | Filter::IsNotNull(field) => {
677            validate_filter_field_exists(field, collection, ctx);
678        }
679        Filter::And(filters) | Filter::Or(filters) => {
680            for f in filters {
681                validate_filter(f, collection, ctx);
682            }
683        }
684        Filter::Not(inner) => {
685            validate_filter(inner, collection, ctx);
686        }
687    }
688}
689
690/// Validate a filter field exists
691fn validate_filter_field_exists<S: SchemaProvider>(
692    field: &str,
693    collection: &Collection,
694    ctx: &mut ValidationContext<'_, S>,
695) {
696    if field != "id" && !collection.fields.contains_key(field) {
697        ctx.add_error(
698            ErrorCode::UnknownField,
699            format!(
700                "Filter field '{}' does not exist in collection '{}'",
701                field, collection.name
702            ),
703        );
704    }
705}
706
707/// Validate a filter field with value type checking
708fn validate_filter_field<S: SchemaProvider>(
709    field: &str,
710    value: &Value,
711    collection: &Collection,
712    ctx: &mut ValidationContext<'_, S>,
713) {
714    if field == "id" {
715        return; // id field always exists
716    }
717
718    if let Some(field_def) = collection.fields.get(field) {
719        // Check type compatibility
720        if !is_type_compatible(&field_def.field_type, value) {
721            ctx.add_error(
722                ErrorCode::TypeMismatch,
723                format!(
724                    "Type mismatch: field '{}' expects {:?}, got {:?}",
725                    field,
726                    field_def.field_type,
727                    value_type_name(value)
728                ),
729            );
730        }
731    } else {
732        ctx.add_error(
733            ErrorCode::UnknownField,
734            format!(
735                "Filter field '{}' does not exist in collection '{}'",
736                field, collection.name
737            ),
738        );
739    }
740}
741
742/// Check if a value is compatible with a field type
743fn is_type_compatible(field_type: &FieldType, value: &Value) -> bool {
744    match (field_type, value) {
745        (_, Value::Null) => true,        // Null is compatible with any type
746        (_, Value::Variable(_)) => true, // Variables are resolved later
747        (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
748        (FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
749        (FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
750        (FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true, // Int can be used where Float expected
751        (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
752        (FieldType::Array(_), Value::Array(_)) => true,
753        (FieldType::Object, Value::Object(_)) => true,
754        (FieldType::Nested(_), Value::Object(_)) => true, // Structural compatibility for filters (deep check might be too expensive/complex here)
755        (FieldType::Scalar(ScalarType::Object), Value::Object(_)) => true, // Support for ScalarType::Object
756        (FieldType::Scalar(ScalarType::Array), Value::Array(_)) => true, // Support for ScalarType::Array
757        (FieldType::Scalar(ScalarType::Any), _) => true, // Any type accepts all values
758        (FieldType::Scalar(ScalarType::Uuid), Value::String(_)) => true, // UUIDs are often passed as strings
759        _ => false,
760    }
761}
762
763/// Get a human-readable type name for a value
764fn value_type_name(value: &Value) -> &'static str {
765    match value {
766        Value::Null => "null",
767        Value::Boolean(_) => "boolean",
768        Value::Int(_) => "int",
769        Value::Float(_) => "float",
770        Value::String(_) => "string",
771        Value::Array(_) => "array",
772        Value::Object(_) => "object",
773        Value::Variable(_) => "variable",
774        Value::Enum(_) => "enum",
775    }
776}
777
778/// Extract a Filter from an AST Value (for parsing where arguments)
779fn extract_filter_from_value(value: &Value) -> Option<Filter> {
780    match value {
781        Value::Object(map) => {
782            let mut filters = Vec::new();
783
784            for (key, val) in map {
785                match key.as_str() {
786                    "and" => {
787                        if let Value::Array(arr) = val {
788                            let sub_filters: Vec<Filter> =
789                                arr.iter().filter_map(extract_filter_from_value).collect();
790                            if !sub_filters.is_empty() {
791                                filters.push(Filter::And(sub_filters));
792                            }
793                        }
794                    }
795                    "or" => {
796                        if let Value::Array(arr) = val {
797                            let sub_filters: Vec<Filter> =
798                                arr.iter().filter_map(extract_filter_from_value).collect();
799                            if !sub_filters.is_empty() {
800                                filters.push(Filter::Or(sub_filters));
801                            }
802                        }
803                    }
804                    "not" => {
805                        if let Some(inner) = extract_filter_from_value(val) {
806                            filters.push(Filter::Not(Box::new(inner)));
807                        }
808                    }
809                    field => {
810                        // Field-level filter: { field: { eq: value } }
811                        if let Value::Object(ops) = val {
812                            for (op, op_val) in ops {
813                                let filter = match op.as_str() {
814                                    "eq" => Some(Filter::Eq(field.to_string(), op_val.clone())),
815                                    "ne" => Some(Filter::Ne(field.to_string(), op_val.clone())),
816                                    "gt" => Some(Filter::Gt(field.to_string(), op_val.clone())),
817                                    "gte" => Some(Filter::Gte(field.to_string(), op_val.clone())),
818                                    "lt" => Some(Filter::Lt(field.to_string(), op_val.clone())),
819                                    "lte" => Some(Filter::Lte(field.to_string(), op_val.clone())),
820                                    "in" => Some(Filter::In(field.to_string(), op_val.clone())),
821                                    "nin" => Some(Filter::NotIn(field.to_string(), op_val.clone())),
822                                    "contains" => {
823                                        Some(Filter::Contains(field.to_string(), op_val.clone()))
824                                    }
825                                    "startsWith" => {
826                                        Some(Filter::StartsWith(field.to_string(), op_val.clone()))
827                                    }
828                                    "endsWith" => {
829                                        Some(Filter::EndsWith(field.to_string(), op_val.clone()))
830                                    }
831                                    "isNull" => Some(Filter::IsNull(field.to_string())),
832                                    "isNotNull" => Some(Filter::IsNotNull(field.to_string())),
833                                    _ => None,
834                                };
835                                if let Some(f) = filter {
836                                    filters.push(f);
837                                }
838                            }
839                        }
840                    }
841                }
842            }
843
844            match filters.len() {
845                0 => None,
846                1 => Some(filters.remove(0)),
847                _ => Some(Filter::And(filters)),
848            }
849        }
850        _ => None,
851    }
852}
853
854/// Resolve variables in a document, replacing Value::Variable with actual values
855pub fn resolve_variables(
856    doc: &mut Document,
857    variables: &HashMap<String, ast::Value>,
858) -> Result<(), ValidationError> {
859    for op in &mut doc.operations {
860        match op {
861            ast::Operation::Query(query) => {
862                resolve_in_fields(&mut query.selection_set, variables)?;
863            }
864            ast::Operation::Mutation(mutation) => {
865                for mut_op in &mut mutation.operations {
866                    resolve_in_mutation_op(mut_op, variables)?;
867                }
868            }
869            ast::Operation::Subscription(sub) => {
870                resolve_in_fields(&mut sub.selection_set, variables)?;
871            }
872            ast::Operation::Schema(_) => {}
873            ast::Operation::Migration(_) => {}
874            ast::Operation::FragmentDefinition(_) => {} // Handled when fragment is used
875            ast::Operation::Introspection(_) => {}      // No variables in introspection
876            ast::Operation::Handler(_) => {}            // Handlers don't have variable resolution
877        }
878    }
879    Ok(())
880}
881
882fn resolve_in_fields(
883    fields: &mut [Selection],
884    variables: &HashMap<String, ast::Value>,
885) -> Result<(), ValidationError> {
886    for selection in fields {
887        match selection {
888            Selection::Field(field) => {
889                // Resolve variables in arguments
890                for arg in &mut field.arguments {
891                    resolve_in_value(&mut arg.value, variables)?;
892                }
893                // Recursively resolve in nested selection
894                resolve_in_fields(&mut field.selection_set, variables)?;
895            }
896            Selection::InlineFragment(inline) => {
897                resolve_in_fields(&mut inline.selection_set, variables)?;
898            }
899            Selection::FragmentSpread(_) => {
900                // Nothing to resolve in fragment spread itself (name)
901            }
902        }
903    }
904    Ok(())
905}
906
907fn resolve_in_mutation_op(
908    op: &mut ast::MutationOperation,
909    variables: &HashMap<String, ast::Value>,
910) -> Result<(), ValidationError> {
911    match &mut op.operation {
912        ast::MutationOp::Insert { data, .. }
913        | ast::MutationOp::Update { data, .. }
914        | ast::MutationOp::Upsert { data, .. } => {
915            resolve_in_value(data, variables)?;
916        }
917        ast::MutationOp::InsertMany { data, .. } => {
918            for item in data {
919                resolve_in_value(item, variables)?;
920            }
921        }
922        ast::MutationOp::Delete { .. } => {}
923        ast::MutationOp::EnqueueJob { payload, .. } => {
924            resolve_in_value(payload, variables)?;
925        }
926        ast::MutationOp::Transaction { operations } => {
927            for inner in operations {
928                resolve_in_mutation_op(inner, variables)?;
929            }
930        }
931    }
932    resolve_in_fields(&mut op.selection_set, variables)
933}
934
935fn resolve_in_value(
936    value: &mut Value,
937    variables: &HashMap<String, ast::Value>,
938) -> Result<(), ValidationError> {
939    match value {
940        Value::Variable(name) => {
941            if let Some(resolved) = variables.get(name) {
942                *value = resolved.clone();
943            } else {
944                return Err(ValidationError::new(
945                    ErrorCode::MissingRequiredVariable,
946                    format!("Variable '{}' is not provided", name),
947                ));
948            }
949        }
950        Value::Array(items) => {
951            for item in items {
952                resolve_in_value(item, variables)?;
953            }
954        }
955        Value::Object(map) => {
956            for v in map.values_mut() {
957                resolve_in_value(v, variables)?;
958            }
959        }
960        _ => {}
961    }
962    Ok(())
963}
964
965#[cfg(test)]
966mod tests {
967    use super::*;
968    use crate::types::FieldDefinition;
969
970    fn create_test_schema() -> InMemorySchema {
971        let mut schema = InMemorySchema::new();
972
973        let mut users_fields = HashMap::new();
974        users_fields.insert(
975            "name".to_string(),
976            FieldDefinition {
977                field_type: FieldType::String,
978                unique: false,
979                indexed: false,
980                nullable: false,
981            },
982        );
983        users_fields.insert(
984            "email".to_string(),
985            FieldDefinition {
986                field_type: FieldType::String,
987                unique: true,
988                indexed: true,
989                nullable: false,
990            },
991        );
992        users_fields.insert(
993            "age".to_string(),
994            FieldDefinition {
995                field_type: FieldType::Int,
996                unique: false,
997                indexed: false,
998                nullable: false,
999            },
1000        );
1001        users_fields.insert(
1002            "active".to_string(),
1003            FieldDefinition {
1004                field_type: FieldType::Bool,
1005                unique: false,
1006                indexed: false,
1007                nullable: false,
1008            },
1009        );
1010
1011        schema.add_collection(Collection {
1012            name: "users".to_string(),
1013            fields: users_fields,
1014        });
1015
1016        schema
1017    }
1018
1019    #[test]
1020    fn test_validate_unknown_collection() {
1021        let schema = create_test_schema();
1022        let doc = Document {
1023            operations: vec![ast::Operation::Query(Query {
1024                name: None,
1025                variable_definitions: vec![],
1026                directives: vec![],
1027                selection_set: vec![Selection::Field(Field {
1028                    alias: None,
1029                    name: "nonexistent".to_string(),
1030                    arguments: vec![],
1031                    directives: vec![],
1032                    selection_set: vec![Selection::Field(Field {
1033                        alias: None,
1034                        name: "id".to_string(),
1035                        arguments: vec![],
1036                        directives: vec![],
1037                        selection_set: vec![],
1038                    })],
1039                })],
1040                variables_values: HashMap::new(),
1041            })],
1042        };
1043
1044        let result = validate_document(&doc, &schema, HashMap::new());
1045        assert!(result.is_err());
1046        let errors = result.unwrap_err();
1047        assert!(
1048            errors
1049                .iter()
1050                .any(|e| e.code == ErrorCode::UnknownCollection)
1051        );
1052    }
1053
1054    #[test]
1055    fn test_validate_unknown_field() {
1056        let schema = create_test_schema();
1057        let doc = Document {
1058            operations: vec![ast::Operation::Query(Query {
1059                name: None,
1060                variable_definitions: vec![],
1061                directives: vec![],
1062                selection_set: vec![Selection::Field(Field {
1063                    alias: None,
1064                    name: "users".to_string(),
1065                    arguments: vec![],
1066                    directives: vec![],
1067                    selection_set: vec![Selection::Field(Field {
1068                        alias: None,
1069                        name: "nonexistent_field".to_string(),
1070                        arguments: vec![],
1071                        directives: vec![],
1072                        selection_set: vec![],
1073                    })],
1074                })],
1075                variables_values: HashMap::new(),
1076            })],
1077        };
1078
1079        let result = validate_document(&doc, &schema, HashMap::new());
1080        assert!(result.is_err());
1081        let errors = result.unwrap_err();
1082        assert!(errors.iter().any(|e| e.code == ErrorCode::UnknownField));
1083    }
1084
1085    #[test]
1086    fn test_validate_missing_required_variable() {
1087        let schema = create_test_schema();
1088        let doc = Document {
1089            operations: vec![ast::Operation::Query(Query {
1090                name: Some("GetUsers".to_string()),
1091                variable_definitions: vec![ast::VariableDefinition {
1092                    name: "minAge".to_string(),
1093                    var_type: ast::TypeAnnotation {
1094                        name: "Int".to_string(),
1095                        is_array: false,
1096                        is_required: true,
1097                    },
1098                    default_value: None,
1099                }],
1100                directives: vec![],
1101                selection_set: vec![],
1102                variables_values: HashMap::new(),
1103            })],
1104        };
1105
1106        // No variables provided
1107        let result = validate_document(&doc, &schema, HashMap::new());
1108        assert!(result.is_err());
1109        let errors = result.unwrap_err();
1110        assert!(
1111            errors
1112                .iter()
1113                .any(|e| e.code == ErrorCode::MissingRequiredVariable)
1114        );
1115    }
1116
1117    #[test]
1118    fn test_validate_valid_query() {
1119        let schema = create_test_schema();
1120        let doc = Document {
1121            operations: vec![ast::Operation::Query(Query {
1122                name: Some("GetUsers".to_string()),
1123                variable_definitions: vec![],
1124                directives: vec![],
1125                selection_set: vec![Selection::Field(Field {
1126                    alias: None,
1127                    name: "users".to_string(),
1128                    arguments: vec![],
1129                    directives: vec![],
1130                    selection_set: vec![
1131                        Selection::Field(Field {
1132                            alias: None,
1133                            name: "id".to_string(),
1134                            arguments: vec![],
1135                            directives: vec![],
1136                            selection_set: vec![],
1137                        }),
1138                        Selection::Field(Field {
1139                            alias: None,
1140                            name: "name".to_string(),
1141                            arguments: vec![],
1142                            directives: vec![],
1143                            selection_set: vec![],
1144                        }),
1145                        Selection::Field(Field {
1146                            alias: None,
1147                            name: "email".to_string(),
1148                            arguments: vec![],
1149                            directives: vec![],
1150                            selection_set: vec![],
1151                        }),
1152                    ],
1153                })],
1154                variables_values: HashMap::new(),
1155            })],
1156        };
1157
1158        let result = validate_document(&doc, &schema, HashMap::new());
1159        assert!(result.is_ok());
1160    }
1161
1162    #[test]
1163    fn test_validate_filter_type_mismatch() {
1164        let schema = create_test_schema();
1165        let collection = schema.get_collection("users").unwrap();
1166        let mut ctx = ValidationContext::new(&schema);
1167
1168        // age is Int, but we're filtering with a string
1169        let filter = Filter::Eq("age".to_string(), Value::String("not a number".to_string()));
1170        validate_filter(&filter, collection, &mut ctx);
1171
1172        assert!(ctx.has_errors());
1173        assert!(ctx.errors.iter().any(|e| e.code == ErrorCode::TypeMismatch));
1174    }
1175
1176    #[test]
1177    fn test_validate_filter_string_operator_on_int() {
1178        let schema = create_test_schema();
1179        let collection = schema.get_collection("users").unwrap();
1180        let mut ctx = ValidationContext::new(&schema);
1181
1182        // contains on int field should fail
1183        let filter = Filter::Contains("age".to_string(), Value::String("10".to_string()));
1184        validate_filter(&filter, collection, &mut ctx);
1185
1186        assert!(ctx.has_errors());
1187        assert!(
1188            ctx.errors
1189                .iter()
1190                .any(|e| e.code == ErrorCode::InvalidFilterOperator)
1191        );
1192    }
1193
1194    #[test]
1195    fn test_resolve_variables() {
1196        let mut doc = Document {
1197            operations: vec![ast::Operation::Query(Query {
1198                name: None,
1199                variable_definitions: vec![],
1200                directives: vec![],
1201                selection_set: vec![Selection::Field(Field {
1202                    alias: None,
1203                    name: "users".to_string(),
1204                    arguments: vec![ast::Argument {
1205                        name: "limit".to_string(),
1206                        value: Value::Variable("pageSize".to_string()),
1207                    }],
1208                    directives: vec![],
1209                    selection_set: vec![],
1210                })],
1211                variables_values: HashMap::new(),
1212            })],
1213        };
1214
1215        let mut vars = HashMap::new();
1216        vars.insert("pageSize".to_string(), Value::Int(10));
1217
1218        let result = resolve_variables(&mut doc, &vars);
1219        assert!(result.is_ok());
1220
1221        // Check that variable was resolved
1222        if let ast::Operation::Query(query) = &doc.operations[0] {
1223            if let Selection::Field(user_field) = &query.selection_set[0] {
1224                let arg = &user_field.arguments[0];
1225                assert!(matches!(arg.value, Value::Int(10)));
1226            } else {
1227                panic!("Expected Selection::Field");
1228            }
1229        } else {
1230            panic!("Expected Query operation");
1231        }
1232    }
1233}