1use crate::ast::*;
10use crate::error::{SchemaError, SchemaResult};
11
12#[derive(Debug)]
14pub struct Validator {
15 errors: Vec<SchemaError>,
17}
18
19impl Default for Validator {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl Validator {
26 pub fn new() -> Self {
28 Self { errors: vec![] }
29 }
30
31 pub fn validate(&mut self, mut schema: Schema) -> SchemaResult<Schema> {
33 self.errors.clear();
34
35 self.check_duplicates(&schema);
37
38 for model in schema.models.values() {
40 self.validate_model(model, &schema);
41 }
42
43 for e in schema.enums.values() {
45 self.validate_enum(e);
46 }
47
48 for t in schema.types.values() {
50 self.validate_composite_type(t, &schema);
51 }
52
53 for v in schema.views.values() {
55 self.validate_view(v, &schema);
56 }
57
58 for sg in schema.server_groups.values() {
60 self.validate_server_group(sg);
61 }
62
63 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 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 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 fn validate_model(&mut self, model: &Model, schema: &Schema) {
121 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 for field in model.fields.values() {
131 self.validate_field(field, model.name(), schema);
132 }
133
134 for attr in &model.attributes {
136 self.validate_model_attribute(attr, model);
137 }
138 }
139
140 fn has_composite_id(&self, model: &Model) -> bool {
142 model.attributes.iter().any(|a| a.is("id"))
143 }
144
145 fn validate_field(&mut self, field: &Field, model_name: &str, schema: &Schema) {
147 match &field.field_type {
149 FieldType::Model(name) => {
150 if schema.models.contains_key(name.as_str()) {
152 } else if schema.enums.contains_key(name.as_str()) {
154 } else if schema.types.contains_key(name.as_str()) {
157 } 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 for attr in &field.attributes {
189 self.validate_field_attribute(attr, field, model_name, schema);
190 }
191
192 if field.field_type.is_relation() && !field.is_list() {
194 let attrs = field.extract_attributes();
196 if attrs.relation.is_some() {
197 let rel = attrs.relation.as_ref().unwrap();
198 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 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 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 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 if let Some(value) = attr.first_arg() {
258 self.validate_default_value(value, field, model_name, schema);
259 }
260 }
261 "relation" => {
262 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 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 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 (_, AttributeValue::Function(_, _)) => {}
302
303 (FieldType::Scalar(ScalarType::Int), AttributeValue::Int(_)) => {}
305 (FieldType::Scalar(ScalarType::BigInt), AttributeValue::Int(_)) => {}
306
307 (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 (FieldType::Scalar(ScalarType::String), AttributeValue::String(_)) => {}
315
316 (FieldType::Scalar(ScalarType::Boolean), AttributeValue::Boolean(_)) => {}
318
319 (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 (FieldType::Model(type_name), AttributeValue::Ident(variant)) => {
337 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 }
353
354 _ => {
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 fn validate_model_attribute(&mut self, attr: &Attribute, model: &Model) {
370 match attr.name() {
371 "index" | "unique" => {
372 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 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 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 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 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 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 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 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 fn validate_view(&mut self, v: &View, schema: &Schema) {
494 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 for field in v.fields.values() {
504 self.validate_field(field, v.name(), schema);
505 }
506 }
507
508 fn validate_server_group(&mut self, sg: &ServerGroup) {
510 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 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 for server in sg.servers.values() {
531 self.validate_server(server, sg.name.name.as_str());
532 }
533
534 for attr in &sg.attributes {
536 self.validate_server_group_attribute(attr, sg);
537 }
538
539 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 fn validate_server(&mut self, server: &Server, group_name: &str) {
559 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 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 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 fn validate_server_group_attribute(&mut self, attr: &Attribute, sg: &ServerGroup) {
596 match attr.name() {
597 "strategy" => {
598 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 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 _ => {} }
641 }
642
643 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 RelationType::OneToMany
655 } else {
656 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
694pub 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 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 assert!(schema.is_ok() || schema.is_err());
943 }
944
945 #[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}