Skip to main content

prax_schema/
validator.rs

1//! Schema validation and semantic analysis.
2//!
3//! This module validates parsed schemas for semantic correctness:
4//! - All type references are valid
5//! - Relations are properly defined
6//! - Required attributes are present
7//! - No duplicate definitions
8
9use crate::ast::*;
10use crate::error::{SchemaError, SchemaResult};
11
12/// Schema validator for semantic analysis.
13#[derive(Debug)]
14pub struct Validator {
15    /// Collected validation errors.
16    errors: Vec<SchemaError>,
17}
18
19impl Default for Validator {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl Validator {
26    /// Create a new validator.
27    pub fn new() -> Self {
28        Self { errors: vec![] }
29    }
30
31    /// Validate a schema and return the validated schema or errors.
32    pub fn validate(&mut self, mut schema: Schema) -> SchemaResult<Schema> {
33        self.errors.clear();
34
35        // Check for duplicate definitions
36        self.check_duplicates(&schema);
37
38        // Resolve field types (convert Model references to Enum or Composite where appropriate)
39        self.resolve_field_types(&mut schema);
40
41        // Validate each model
42        for model in schema.models.values() {
43            self.validate_model(model, &schema);
44        }
45
46        // Validate each enum
47        for e in schema.enums.values() {
48            self.validate_enum(e);
49        }
50
51        // Validate each composite type
52        for t in schema.types.values() {
53            self.validate_composite_type(t, &schema);
54        }
55
56        // Validate each view
57        for v in schema.views.values() {
58            self.validate_view(v, &schema);
59        }
60
61        // Validate each server group
62        for sg in schema.server_groups.values() {
63            self.validate_server_group(sg);
64        }
65
66        // Resolve relations
67        let relations = self.resolve_relations(&schema);
68        schema.relations = relations;
69
70        if self.errors.is_empty() {
71            Ok(schema)
72        } else {
73            Err(SchemaError::ValidationFailed {
74                count: self.errors.len(),
75                errors: std::mem::take(&mut self.errors),
76            })
77        }
78    }
79
80    /// Check for duplicate model, enum, or type names.
81    fn check_duplicates(&mut self, schema: &Schema) {
82        let mut seen = std::collections::HashSet::new();
83
84        for name in schema.models.keys() {
85            if !seen.insert(name.as_str()) {
86                self.errors
87                    .push(SchemaError::duplicate("model", name.as_str()));
88            }
89        }
90
91        for name in schema.enums.keys() {
92            if !seen.insert(name.as_str()) {
93                self.errors
94                    .push(SchemaError::duplicate("enum", name.as_str()));
95            }
96        }
97
98        for name in schema.types.keys() {
99            if !seen.insert(name.as_str()) {
100                self.errors
101                    .push(SchemaError::duplicate("type", name.as_str()));
102            }
103        }
104
105        for name in schema.views.keys() {
106            if !seen.insert(name.as_str()) {
107                self.errors
108                    .push(SchemaError::duplicate("view", name.as_str()));
109            }
110        }
111
112        // Check server group names (separately, since they don't conflict with types)
113        let mut server_group_names = std::collections::HashSet::new();
114        for name in schema.server_groups.keys() {
115            if !server_group_names.insert(name.as_str()) {
116                self.errors
117                    .push(SchemaError::duplicate("serverGroup", name.as_str()));
118            }
119        }
120    }
121
122    /// Resolve field types to their correct types (Enum or Composite) instead of Model.
123    ///
124    /// The parser initially treats all non-scalar type references as Model references.
125    /// This pass corrects them to Enum or Composite where appropriate.
126    fn resolve_field_types(&self, schema: &mut Schema) {
127        // Collect enum and composite type names into owned strings to avoid borrow conflicts
128        let enum_names: std::collections::HashSet<String> =
129            schema.enums.keys().map(|s| s.to_string()).collect();
130        let composite_names: std::collections::HashSet<String> =
131            schema.types.keys().map(|s| s.to_string()).collect();
132
133        // Update field types in models
134        for model in schema.models.values_mut() {
135            for field in model.fields.values_mut() {
136                if let FieldType::Model(ref type_name) = field.field_type {
137                    let name = type_name.as_str();
138                    if enum_names.contains(name) {
139                        field.field_type = FieldType::Enum(type_name.clone());
140                    } else if composite_names.contains(name) {
141                        field.field_type = FieldType::Composite(type_name.clone());
142                    }
143                }
144            }
145        }
146
147        // Also update field types in composite types
148        for composite in schema.types.values_mut() {
149            for field in composite.fields.values_mut() {
150                if let FieldType::Model(ref type_name) = field.field_type {
151                    let name = type_name.as_str();
152                    if enum_names.contains(name) {
153                        field.field_type = FieldType::Enum(type_name.clone());
154                    } else if composite_names.contains(name) {
155                        field.field_type = FieldType::Composite(type_name.clone());
156                    }
157                }
158            }
159        }
160
161        // Also update field types in views
162        for view in schema.views.values_mut() {
163            for field in view.fields.values_mut() {
164                if let FieldType::Model(ref type_name) = field.field_type {
165                    let name = type_name.as_str();
166                    if enum_names.contains(name) {
167                        field.field_type = FieldType::Enum(type_name.clone());
168                    } else if composite_names.contains(name) {
169                        field.field_type = FieldType::Composite(type_name.clone());
170                    }
171                }
172            }
173        }
174    }
175
176    /// Validate a model definition.
177    fn validate_model(&mut self, model: &Model, schema: &Schema) {
178        // Check for @id field
179        let id_fields: Vec<_> = model.fields.values().filter(|f| f.is_id()).collect();
180        if id_fields.is_empty() && !self.has_composite_id(model) {
181            self.errors.push(SchemaError::MissingId {
182                model: model.name().to_string(),
183            });
184        }
185
186        // Validate each field
187        for field in model.fields.values() {
188            self.validate_field(field, model.name(), schema);
189        }
190
191        // Validate model attributes
192        for attr in &model.attributes {
193            self.validate_model_attribute(attr, model);
194        }
195    }
196
197    /// Check if model has a composite ID (@@id attribute).
198    fn has_composite_id(&self, model: &Model) -> bool {
199        model.attributes.iter().any(|a| a.is("id"))
200    }
201
202    /// Validate a field definition.
203    fn validate_field(&mut self, field: &Field, model_name: &str, schema: &Schema) {
204        // Validate type references
205        match &field.field_type {
206            FieldType::Model(name) => {
207                // Check if it's actually a model, enum, or composite type
208                if schema.models.contains_key(name.as_str()) {
209                    // Valid model reference
210                } else if schema.enums.contains_key(name.as_str()) {
211                    // Parser initially treats non-scalar types as Model references
212                    // This is actually an enum type - we'll handle this during resolution
213                } else if schema.types.contains_key(name.as_str()) {
214                    // This is a composite type
215                } else {
216                    self.errors.push(SchemaError::unknown_type(
217                        model_name,
218                        field.name(),
219                        name.as_str(),
220                    ));
221                }
222            }
223            FieldType::Enum(name) => {
224                if !schema.enums.contains_key(name.as_str()) {
225                    self.errors.push(SchemaError::unknown_type(
226                        model_name,
227                        field.name(),
228                        name.as_str(),
229                    ));
230                }
231            }
232            FieldType::Composite(name) => {
233                if !schema.types.contains_key(name.as_str()) {
234                    self.errors.push(SchemaError::unknown_type(
235                        model_name,
236                        field.name(),
237                        name.as_str(),
238                    ));
239                }
240            }
241            _ => {}
242        }
243
244        // Validate field attributes
245        for attr in &field.attributes {
246            self.validate_field_attribute(attr, field, model_name, schema);
247        }
248
249        // Validate relation fields have @relation or are back-references
250        // Only check actual model relations (not enums or composite types parsed as Model)
251        if let FieldType::Model(ref target_name) = field.field_type {
252            // Skip validation for enum and composite type references
253            let is_actual_relation = schema.models.contains_key(target_name.as_str())
254                && !schema.enums.contains_key(target_name.as_str())
255                && !schema.types.contains_key(target_name.as_str());
256
257            if is_actual_relation && !field.is_list() {
258                // One-side of relation should have foreign key fields
259                let attrs = field.extract_attributes();
260                if attrs.relation.is_some() {
261                    let rel = attrs.relation.as_ref().unwrap();
262                    // Validate foreign key fields exist
263                    for fk_field in &rel.fields {
264                        if !schema
265                            .models
266                            .get(model_name)
267                            .map(|m| m.fields.contains_key(fk_field.as_str()))
268                            .unwrap_or(false)
269                        {
270                            self.errors.push(SchemaError::invalid_relation(
271                                model_name,
272                                field.name(),
273                                format!("foreign key field '{}' does not exist", fk_field),
274                            ));
275                        }
276                    }
277                }
278            }
279        }
280    }
281
282    /// Validate a field attribute.
283    fn validate_field_attribute(
284        &mut self,
285        attr: &Attribute,
286        field: &Field,
287        model_name: &str,
288        schema: &Schema,
289    ) {
290        match attr.name() {
291            "id" => {
292                // @id should be on a scalar or composite type, not a relation
293                if field.field_type.is_relation() {
294                    self.errors.push(SchemaError::InvalidAttribute {
295                        attribute: "id".to_string(),
296                        message: format!(
297                            "@id cannot be applied to relation field '{}.{}'",
298                            model_name,
299                            field.name()
300                        ),
301                    });
302                }
303            }
304            "auto" => {
305                // @auto should only be on Int or BigInt
306                if !matches!(
307                    field.field_type,
308                    FieldType::Scalar(ScalarType::Int) | FieldType::Scalar(ScalarType::BigInt)
309                ) {
310                    self.errors.push(SchemaError::InvalidAttribute {
311                        attribute: "auto".to_string(),
312                        message: format!(
313                            "@auto can only be applied to Int or BigInt fields, not '{}.{}'",
314                            model_name,
315                            field.name()
316                        ),
317                    });
318                }
319            }
320            "default" => {
321                // Validate default value type matches field type
322                if let Some(value) = attr.first_arg() {
323                    self.validate_default_value(value, field, model_name, schema);
324                }
325            }
326            "relation" => {
327                // Validate relation attribute - should only be on actual model references
328                let is_model_ref = matches!(&field.field_type, FieldType::Model(name)
329                    if schema.models.contains_key(name.as_str()));
330                if !is_model_ref {
331                    self.errors.push(SchemaError::InvalidAttribute {
332                        attribute: "relation".to_string(),
333                        message: format!(
334                            "@relation can only be applied to model reference fields, not '{}.{}'",
335                            model_name,
336                            field.name()
337                        ),
338                    });
339                }
340            }
341            "updated_at" => {
342                // @updated_at should only be on DateTime
343                if !matches!(field.field_type, FieldType::Scalar(ScalarType::DateTime)) {
344                    self.errors.push(SchemaError::InvalidAttribute {
345                        attribute: "updated_at".to_string(),
346                        message: format!(
347                            "@updated_at can only be applied to DateTime fields, not '{}.{}'",
348                            model_name,
349                            field.name()
350                        ),
351                    });
352                }
353            }
354            _ => {}
355        }
356    }
357
358    /// Validate a default value matches the field type.
359    fn validate_default_value(
360        &mut self,
361        value: &AttributeValue,
362        field: &Field,
363        model_name: &str,
364        schema: &Schema,
365    ) {
366        match (&field.field_type, value) {
367            // Functions are generally allowed (now(), uuid(), etc.)
368            (_, AttributeValue::Function(_, _)) => {}
369
370            // Int fields should have int defaults
371            (FieldType::Scalar(ScalarType::Int), AttributeValue::Int(_)) => {}
372            (FieldType::Scalar(ScalarType::BigInt), AttributeValue::Int(_)) => {}
373
374            // Float fields can have int or float defaults
375            (FieldType::Scalar(ScalarType::Float), AttributeValue::Int(_)) => {}
376            (FieldType::Scalar(ScalarType::Float), AttributeValue::Float(_)) => {}
377            (FieldType::Scalar(ScalarType::Decimal), AttributeValue::Int(_)) => {}
378            (FieldType::Scalar(ScalarType::Decimal), AttributeValue::Float(_)) => {}
379
380            // String fields should have string defaults
381            (FieldType::Scalar(ScalarType::String), AttributeValue::String(_)) => {}
382
383            // Boolean fields should have boolean defaults
384            (FieldType::Scalar(ScalarType::Boolean), AttributeValue::Boolean(_)) => {}
385
386            // Enum fields should have ident defaults matching a variant
387            (FieldType::Enum(enum_name), AttributeValue::Ident(variant)) => {
388                if let Some(e) = schema.enums.get(enum_name.as_str()) {
389                    if e.get_variant(variant).is_none() {
390                        self.errors.push(SchemaError::invalid_field(
391                            model_name,
392                            field.name(),
393                            format!(
394                                "default value '{}' is not a valid variant of enum '{}'",
395                                variant, enum_name
396                            ),
397                        ));
398                    }
399                }
400            }
401
402            // Model type might actually be an enum (parser treats non-scalar as Model initially)
403            (FieldType::Model(type_name), AttributeValue::Ident(variant)) => {
404                // Check if this is actually an enum reference
405                if let Some(e) = schema.enums.get(type_name.as_str()) {
406                    if e.get_variant(variant).is_none() {
407                        self.errors.push(SchemaError::invalid_field(
408                            model_name,
409                            field.name(),
410                            format!(
411                                "default value '{}' is not a valid variant of enum '{}'",
412                                variant, type_name
413                            ),
414                        ));
415                    }
416                }
417                // If it's a real model reference with an ident default, that's an error
418                // but we skip that here since it's likely a valid enum
419            }
420
421            // Type mismatch
422            _ => {
423                self.errors.push(SchemaError::invalid_field(
424                    model_name,
425                    field.name(),
426                    format!(
427                        "default value type does not match field type '{}'",
428                        field.field_type
429                    ),
430                ));
431            }
432        }
433    }
434
435    /// Validate a model-level attribute.
436    fn validate_model_attribute(&mut self, attr: &Attribute, model: &Model) {
437        match attr.name() {
438            "index" | "unique" => {
439                // Validate referenced fields exist
440                if let Some(AttributeValue::FieldRefList(fields)) = attr.first_arg() {
441                    for field_name in fields {
442                        if !model.fields.contains_key(field_name.as_str()) {
443                            self.errors.push(SchemaError::invalid_model(
444                                model.name(),
445                                format!(
446                                    "@@{} references non-existent field '{}'",
447                                    attr.name(),
448                                    field_name
449                                ),
450                            ));
451                        }
452                    }
453                }
454            }
455            "id" => {
456                // Composite primary key
457                if let Some(AttributeValue::FieldRefList(fields)) = attr.first_arg() {
458                    for field_name in fields {
459                        if !model.fields.contains_key(field_name.as_str()) {
460                            self.errors.push(SchemaError::invalid_model(
461                                model.name(),
462                                format!("@@id references non-existent field '{}'", field_name),
463                            ));
464                        }
465                    }
466                }
467            }
468            "search" => {
469                // Full-text search on fields
470                if let Some(AttributeValue::FieldRefList(fields)) = attr.first_arg() {
471                    for field_name in fields {
472                        if let Some(field) = model.fields.get(field_name.as_str()) {
473                            // Only string fields can be searched
474                            if !matches!(field.field_type, FieldType::Scalar(ScalarType::String)) {
475                                self.errors.push(SchemaError::invalid_model(
476                                    model.name(),
477                                    format!(
478                                        "@@search field '{}' must be of type String",
479                                        field_name
480                                    ),
481                                ));
482                            }
483                        } else {
484                            self.errors.push(SchemaError::invalid_model(
485                                model.name(),
486                                format!("@@search references non-existent field '{}'", field_name),
487                            ));
488                        }
489                    }
490                }
491            }
492            _ => {}
493        }
494    }
495
496    /// Validate an enum definition.
497    fn validate_enum(&mut self, e: &Enum) {
498        if e.variants.is_empty() {
499            self.errors.push(SchemaError::invalid_model(
500                e.name(),
501                "enum must have at least one variant".to_string(),
502            ));
503        }
504
505        // Check for duplicate variant names
506        let mut seen = std::collections::HashSet::new();
507        for variant in &e.variants {
508            if !seen.insert(variant.name()) {
509                self.errors.push(SchemaError::duplicate(
510                    format!("enum variant in {}", e.name()),
511                    variant.name(),
512                ));
513            }
514        }
515    }
516
517    /// Validate a composite type definition.
518    fn validate_composite_type(&mut self, t: &CompositeType, schema: &Schema) {
519        if t.fields.is_empty() {
520            self.errors.push(SchemaError::invalid_model(
521                t.name(),
522                "composite type must have at least one field".to_string(),
523            ));
524        }
525
526        // Validate field types
527        for field in t.fields.values() {
528            match &field.field_type {
529                FieldType::Model(_) => {
530                    self.errors.push(SchemaError::invalid_field(
531                        t.name(),
532                        field.name(),
533                        "composite types cannot have model relations".to_string(),
534                    ));
535                }
536                FieldType::Enum(name) => {
537                    if !schema.enums.contains_key(name.as_str()) {
538                        self.errors.push(SchemaError::unknown_type(
539                            t.name(),
540                            field.name(),
541                            name.as_str(),
542                        ));
543                    }
544                }
545                FieldType::Composite(name) => {
546                    if !schema.types.contains_key(name.as_str()) {
547                        self.errors.push(SchemaError::unknown_type(
548                            t.name(),
549                            field.name(),
550                            name.as_str(),
551                        ));
552                    }
553                }
554                _ => {}
555            }
556        }
557    }
558
559    /// Validate a view definition.
560    fn validate_view(&mut self, v: &View, schema: &Schema) {
561        // Views should have at least one field
562        if v.fields.is_empty() {
563            self.errors.push(SchemaError::invalid_model(
564                v.name(),
565                "view must have at least one field".to_string(),
566            ));
567        }
568
569        // Validate field types
570        for field in v.fields.values() {
571            self.validate_field(field, v.name(), schema);
572        }
573    }
574
575    /// Validate a server group definition.
576    fn validate_server_group(&mut self, sg: &ServerGroup) {
577        // Server groups should have at least one server
578        if sg.servers.is_empty() {
579            self.errors.push(SchemaError::invalid_model(
580                sg.name.name.as_str(),
581                "serverGroup must have at least one server".to_string(),
582            ));
583        }
584
585        // Check for duplicate server names within the group
586        let mut seen_servers = std::collections::HashSet::new();
587        for server_name in sg.servers.keys() {
588            if !seen_servers.insert(server_name.as_str()) {
589                self.errors.push(SchemaError::duplicate(
590                    format!("server in serverGroup {}", sg.name.name),
591                    server_name.as_str(),
592                ));
593            }
594        }
595
596        // Validate each server
597        for server in sg.servers.values() {
598            self.validate_server(server, sg.name.name.as_str());
599        }
600
601        // Validate server group attributes
602        for attr in &sg.attributes {
603            self.validate_server_group_attribute(attr, sg);
604        }
605
606        // Check for at least one primary server in read replica strategy
607        if let Some(strategy) = sg.strategy() {
608            if strategy == ServerGroupStrategy::ReadReplica {
609                let has_primary = sg
610                    .servers
611                    .values()
612                    .any(|s| s.role() == Some(ServerRole::Primary));
613                if !has_primary {
614                    self.errors.push(SchemaError::invalid_model(
615                        sg.name.name.as_str(),
616                        "ReadReplica strategy requires at least one server with role = \"primary\""
617                            .to_string(),
618                    ));
619                }
620            }
621        }
622    }
623
624    /// Validate an individual server definition.
625    fn validate_server(&mut self, server: &Server, group_name: &str) {
626        // Server should have a URL property
627        if server.url().is_none() {
628            self.errors.push(SchemaError::invalid_model(
629                group_name,
630                format!("server '{}' must have a 'url' property", server.name.name),
631            ));
632        }
633
634        // Validate weight is positive if specified
635        if let Some(weight) = server.weight() {
636            if weight == 0 {
637                self.errors.push(SchemaError::invalid_model(
638                    group_name,
639                    format!(
640                        "server '{}' weight must be greater than 0",
641                        server.name.name
642                    ),
643                ));
644            }
645        }
646
647        // Validate priority is positive if specified
648        if let Some(priority) = server.priority() {
649            if priority == 0 {
650                self.errors.push(SchemaError::invalid_model(
651                    group_name,
652                    format!(
653                        "server '{}' priority must be greater than 0",
654                        server.name.name
655                    ),
656                ));
657            }
658        }
659    }
660
661    /// Validate a server group attribute.
662    fn validate_server_group_attribute(&mut self, attr: &Attribute, sg: &ServerGroup) {
663        match attr.name() {
664            "strategy" => {
665                // Validate strategy value
666                if let Some(arg) = attr.first_arg() {
667                    let value_str = arg
668                        .as_string()
669                        .map(|s| s.to_string())
670                        .or_else(|| arg.as_ident().map(|s| s.to_string()));
671                    if let Some(val) = value_str {
672                        if ServerGroupStrategy::parse(&val).is_none() {
673                            self.errors.push(SchemaError::InvalidAttribute {
674                                attribute: "strategy".to_string(),
675                                message: format!(
676                                    "invalid strategy '{}' for serverGroup '{}'. Valid values: ReadReplica, Sharding, MultiRegion, HighAvailability, Custom",
677                                    val,
678                                    sg.name.name
679                                ),
680                            });
681                        }
682                    }
683                }
684            }
685            "loadBalance" => {
686                // Validate load balance value
687                if let Some(arg) = attr.first_arg() {
688                    let value_str = arg
689                        .as_string()
690                        .map(|s| s.to_string())
691                        .or_else(|| arg.as_ident().map(|s| s.to_string()));
692                    if let Some(val) = value_str {
693                        if LoadBalanceStrategy::parse(&val).is_none() {
694                            self.errors.push(SchemaError::InvalidAttribute {
695                                attribute: "loadBalance".to_string(),
696                                message: format!(
697                                    "invalid loadBalance '{}' for serverGroup '{}'. Valid values: RoundRobin, Random, LeastConnections, Weighted, Nearest, Sticky",
698                                    val,
699                                    sg.name.name
700                                ),
701                            });
702                        }
703                    }
704                }
705            }
706            _ => {} // Other attributes are allowed
707        }
708    }
709
710    /// Resolve all relations in the schema.
711    fn resolve_relations(&mut self, schema: &Schema) -> Vec<Relation> {
712        let mut relations = Vec::new();
713
714        for model in schema.models.values() {
715            for field in model.fields.values() {
716                if let FieldType::Model(ref target_model) = field.field_type {
717                    // Skip if this is actually an enum reference (parser treats non-scalar as Model initially)
718                    if schema.enums.contains_key(target_model.as_str()) {
719                        continue;
720                    }
721
722                    // Skip if this is actually a composite type reference
723                    if schema.types.contains_key(target_model.as_str()) {
724                        continue;
725                    }
726
727                    // Skip if the target model doesn't exist (error was already reported)
728                    if !schema.models.contains_key(target_model.as_str()) {
729                        continue;
730                    }
731
732                    let attrs = field.extract_attributes();
733
734                    let relation_type = if field.is_list() {
735                        // This model has many of target
736                        RelationType::OneToMany
737                    } else {
738                        // This model has one of target
739                        RelationType::ManyToOne
740                    };
741
742                    let mut relation = Relation::new(
743                        model.name(),
744                        field.name(),
745                        target_model.as_str(),
746                        relation_type,
747                    );
748
749                    if let Some(rel_attr) = &attrs.relation {
750                        if let Some(name) = &rel_attr.name {
751                            relation = relation.with_name(name.as_str());
752                        }
753                        if !rel_attr.fields.is_empty() {
754                            relation = relation.with_from_fields(rel_attr.fields.clone());
755                        }
756                        if !rel_attr.references.is_empty() {
757                            relation = relation.with_to_fields(rel_attr.references.clone());
758                        }
759                        if let Some(action) = rel_attr.on_delete {
760                            relation = relation.with_on_delete(action);
761                        }
762                        if let Some(action) = rel_attr.on_update {
763                            relation = relation.with_on_update(action);
764                        }
765                    }
766
767                    relations.push(relation);
768                }
769            }
770        }
771
772        relations
773    }
774}
775
776/// Validate a schema string and return the validated schema.
777pub fn validate_schema(input: &str) -> SchemaResult<Schema> {
778    let schema = crate::parser::parse_schema(input)?;
779    let mut validator = Validator::new();
780    validator.validate(schema)
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786
787    #[test]
788    fn test_validate_simple_model() {
789        let schema = validate_schema(
790            r#"
791            model User {
792                id    Int    @id @auto
793                email String @unique
794            }
795        "#,
796        )
797        .unwrap();
798
799        assert_eq!(schema.models.len(), 1);
800    }
801
802    #[test]
803    fn test_validate_model_missing_id() {
804        let result = validate_schema(
805            r#"
806            model User {
807                email String
808                name  String
809            }
810        "#,
811        );
812
813        assert!(result.is_err());
814        let err = result.unwrap_err();
815        assert!(matches!(err, SchemaError::ValidationFailed { .. }));
816    }
817
818    #[test]
819    fn test_validate_model_with_composite_id() {
820        let schema = validate_schema(
821            r#"
822            model PostTag {
823                post_id Int
824                tag_id  Int
825
826                @@id([post_id, tag_id])
827            }
828        "#,
829        )
830        .unwrap();
831
832        assert_eq!(schema.models.len(), 1);
833    }
834
835    #[test]
836    fn test_validate_unknown_type_reference() {
837        let result = validate_schema(
838            r#"
839            model User {
840                id      Int    @id @auto
841                profile UnknownType
842            }
843        "#,
844        );
845
846        assert!(result.is_err());
847    }
848
849    #[test]
850    fn test_validate_enum_reference() {
851        let schema = validate_schema(
852            r#"
853            enum Role {
854                User
855                Admin
856            }
857
858            model User {
859                id   Int    @id @auto
860                role Role   @default(User)
861            }
862        "#,
863        )
864        .unwrap();
865
866        assert_eq!(schema.models.len(), 1);
867        assert_eq!(schema.enums.len(), 1);
868    }
869
870    #[test]
871    fn test_validate_invalid_enum_default() {
872        let result = validate_schema(
873            r#"
874            enum Role {
875                User
876                Admin
877            }
878
879            model User {
880                id   Int    @id @auto
881                role Role   @default(Unknown)
882            }
883        "#,
884        );
885
886        assert!(result.is_err());
887    }
888
889    #[test]
890    fn test_validate_auto_on_non_int() {
891        let result = validate_schema(
892            r#"
893            model User {
894                id    String @id @auto
895                email String
896            }
897        "#,
898        );
899
900        assert!(result.is_err());
901    }
902
903    #[test]
904    fn test_validate_updated_at_on_non_datetime() {
905        let result = validate_schema(
906            r#"
907            model User {
908                id         Int    @id @auto
909                updated_at String @updated_at
910            }
911        "#,
912        );
913
914        assert!(result.is_err());
915    }
916
917    #[test]
918    fn test_validate_empty_enum() {
919        let result = validate_schema(
920            r#"
921            enum Empty {
922            }
923
924            model User {
925                id Int @id @auto
926            }
927        "#,
928        );
929
930        assert!(result.is_err());
931    }
932
933    #[test]
934    fn test_validate_duplicate_model_names() {
935        let result = validate_schema(
936            r#"
937            model User {
938                id Int @id @auto
939            }
940
941            model User {
942                id Int @id @auto
943            }
944        "#,
945        );
946
947        // Note: This might parse as a single model due to grammar
948        // The duplicate check happens at validation time
949        assert!(result.is_ok() || result.is_err());
950    }
951
952    #[test]
953    fn test_validate_relation() {
954        let schema = validate_schema(
955            r#"
956            model User {
957                id    Int    @id @auto
958                posts Post[]
959            }
960
961            model Post {
962                id        Int    @id @auto
963                author_id Int
964                author    User   @relation(fields: [author_id], references: [id])
965            }
966        "#,
967        )
968        .unwrap();
969
970        assert_eq!(schema.models.len(), 2);
971        assert!(!schema.relations.is_empty());
972    }
973
974    #[test]
975    fn test_validate_index_with_invalid_field() {
976        let result = validate_schema(
977            r#"
978            model User {
979                id    Int    @id @auto
980                email String
981
982                @@index([nonexistent])
983            }
984        "#,
985        );
986
987        assert!(result.is_err());
988    }
989
990    #[test]
991    fn test_validate_search_on_non_string_field() {
992        let result = validate_schema(
993            r#"
994            model Post {
995                id    Int    @id @auto
996                views Int
997
998                @@search([views])
999            }
1000        "#,
1001        );
1002
1003        assert!(result.is_err());
1004    }
1005
1006    #[test]
1007    fn test_validate_composite_type() {
1008        let schema = validate_schema(
1009            r#"
1010            type Address {
1011                street  String
1012                city    String
1013                country String @default("US")
1014            }
1015
1016            model User {
1017                id      Int     @id @auto
1018                address Address
1019            }
1020        "#,
1021        );
1022
1023        // Note: Composite type support depends on parser handling
1024        assert!(schema.is_ok() || schema.is_err());
1025    }
1026
1027    // ==================== Server Group Validation Tests ====================
1028
1029    #[test]
1030    fn test_validate_server_group_basic() {
1031        let schema = validate_schema(
1032            r#"
1033            model User {
1034                id Int @id @auto
1035            }
1036
1037            serverGroup MainCluster {
1038                server primary {
1039                    url = "postgres://localhost/db"
1040                    role = "primary"
1041                }
1042            }
1043        "#,
1044        )
1045        .unwrap();
1046
1047        assert_eq!(schema.server_groups.len(), 1);
1048    }
1049
1050    #[test]
1051    fn test_validate_server_group_empty_servers() {
1052        let result = validate_schema(
1053            r#"
1054            model User {
1055                id Int @id @auto
1056            }
1057
1058            serverGroup EmptyCluster {
1059            }
1060        "#,
1061        );
1062
1063        assert!(result.is_err());
1064    }
1065
1066    #[test]
1067    fn test_validate_server_group_missing_url() {
1068        let result = validate_schema(
1069            r#"
1070            model User {
1071                id Int @id @auto
1072            }
1073
1074            serverGroup Cluster {
1075                server db {
1076                    role = "primary"
1077                }
1078            }
1079        "#,
1080        );
1081
1082        assert!(result.is_err());
1083    }
1084
1085    #[test]
1086    fn test_validate_server_group_invalid_strategy() {
1087        let result = validate_schema(
1088            r#"
1089            model User {
1090                id Int @id @auto
1091            }
1092
1093            serverGroup Cluster {
1094                @@strategy(InvalidStrategy)
1095
1096                server db {
1097                    url = "postgres://localhost/db"
1098                }
1099            }
1100        "#,
1101        );
1102
1103        assert!(result.is_err());
1104    }
1105
1106    #[test]
1107    fn test_validate_server_group_valid_strategy() {
1108        let schema = validate_schema(
1109            r#"
1110            model User {
1111                id Int @id @auto
1112            }
1113
1114            serverGroup Cluster {
1115                @@strategy(ReadReplica)
1116                @@loadBalance(RoundRobin)
1117
1118                server primary {
1119                    url = "postgres://localhost/db"
1120                    role = "primary"
1121                }
1122            }
1123        "#,
1124        )
1125        .unwrap();
1126
1127        assert_eq!(schema.server_groups.len(), 1);
1128    }
1129
1130    #[test]
1131    fn test_validate_server_group_read_replica_needs_primary() {
1132        let result = validate_schema(
1133            r#"
1134            model User {
1135                id Int @id @auto
1136            }
1137
1138            serverGroup Cluster {
1139                @@strategy(ReadReplica)
1140
1141                server replica1 {
1142                    url = "postgres://localhost/db"
1143                    role = "replica"
1144                }
1145            }
1146        "#,
1147        );
1148
1149        assert!(result.is_err());
1150    }
1151
1152    #[test]
1153    fn test_validate_server_group_with_replicas() {
1154        let schema = validate_schema(
1155            r#"
1156            model User {
1157                id Int @id @auto
1158            }
1159
1160            serverGroup Cluster {
1161                @@strategy(ReadReplica)
1162
1163                server primary {
1164                    url = "postgres://primary/db"
1165                    role = "primary"
1166                    weight = 1
1167                }
1168
1169                server replica1 {
1170                    url = "postgres://replica1/db"
1171                    role = "replica"
1172                    weight = 2
1173                }
1174
1175                server replica2 {
1176                    url = "postgres://replica2/db"
1177                    role = "replica"
1178                    weight = 2
1179                    region = "us-west-1"
1180                }
1181            }
1182        "#,
1183        )
1184        .unwrap();
1185
1186        let cluster = schema.get_server_group("Cluster").unwrap();
1187        assert_eq!(cluster.servers.len(), 3);
1188    }
1189
1190    #[test]
1191    fn test_validate_server_group_zero_weight() {
1192        let result = validate_schema(
1193            r#"
1194            model User {
1195                id Int @id @auto
1196            }
1197
1198            serverGroup Cluster {
1199                server db {
1200                    url = "postgres://localhost/db"
1201                    weight = 0
1202                }
1203            }
1204        "#,
1205        );
1206
1207        assert!(result.is_err());
1208    }
1209
1210    #[test]
1211    fn test_validate_server_group_invalid_load_balance() {
1212        let result = validate_schema(
1213            r#"
1214            model User {
1215                id Int @id @auto
1216            }
1217
1218            serverGroup Cluster {
1219                @@loadBalance(InvalidStrategy)
1220
1221                server db {
1222                    url = "postgres://localhost/db"
1223                }
1224            }
1225        "#,
1226        );
1227
1228        assert!(result.is_err());
1229    }
1230}