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