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 self.resolve_field_types(&mut schema);
40
41 for model in schema.models.values() {
43 self.validate_model(model, &schema);
44 }
45
46 for e in schema.enums.values() {
48 self.validate_enum(e);
49 }
50
51 for t in schema.types.values() {
53 self.validate_composite_type(t, &schema);
54 }
55
56 for v in schema.views.values() {
58 self.validate_view(v, &schema);
59 }
60
61 for sg in schema.server_groups.values() {
63 self.validate_server_group(sg);
64 }
65
66 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 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 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 fn resolve_field_types(&self, schema: &mut Schema) {
127 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 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 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 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 fn validate_model(&mut self, model: &Model, schema: &Schema) {
178 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 for field in model.fields.values() {
188 self.validate_field(field, model.name(), schema);
189 }
190
191 for attr in &model.attributes {
193 self.validate_model_attribute(attr, model);
194 }
195 }
196
197 fn has_composite_id(&self, model: &Model) -> bool {
199 model.attributes.iter().any(|a| a.is("id"))
200 }
201
202 fn validate_field(&mut self, field: &Field, model_name: &str, schema: &Schema) {
204 match &field.field_type {
206 FieldType::Model(name) => {
207 if schema.models.contains_key(name.as_str()) {
209 } else if schema.enums.contains_key(name.as_str()) {
211 } else if schema.types.contains_key(name.as_str()) {
214 } 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 for attr in &field.attributes {
246 self.validate_field_attribute(attr, field, model_name, schema);
247 }
248
249 if let FieldType::Model(ref target_name) = field.field_type {
252 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 let attrs = field.extract_attributes();
260 if attrs.relation.is_some() {
261 let rel = attrs.relation.as_ref().unwrap();
262 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 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 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 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 if let Some(value) = attr.first_arg() {
323 self.validate_default_value(value, field, model_name, schema);
324 }
325 }
326 "relation" => {
327 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 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 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 (_, AttributeValue::Function(_, _)) => {}
369
370 (FieldType::Scalar(ScalarType::Int), AttributeValue::Int(_)) => {}
372 (FieldType::Scalar(ScalarType::BigInt), AttributeValue::Int(_)) => {}
373
374 (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 (FieldType::Scalar(ScalarType::String), AttributeValue::String(_)) => {}
382
383 (FieldType::Scalar(ScalarType::Boolean), AttributeValue::Boolean(_)) => {}
385
386 (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 (FieldType::Model(type_name), AttributeValue::Ident(variant)) => {
404 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 }
420
421 _ => {
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 fn validate_model_attribute(&mut self, attr: &Attribute, model: &Model) {
437 match attr.name() {
438 "index" | "unique" => {
439 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 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 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 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 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 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 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 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 fn validate_view(&mut self, v: &View, schema: &Schema) {
561 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 for field in v.fields.values() {
571 self.validate_field(field, v.name(), schema);
572 }
573 }
574
575 fn validate_server_group(&mut self, sg: &ServerGroup) {
577 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 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 for server in sg.servers.values() {
598 self.validate_server(server, sg.name.name.as_str());
599 }
600
601 for attr in &sg.attributes {
603 self.validate_server_group_attribute(attr, sg);
604 }
605
606 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 fn validate_server(&mut self, server: &Server, group_name: &str) {
626 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 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 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 fn validate_server_group_attribute(&mut self, attr: &Attribute, sg: &ServerGroup) {
663 match attr.name() {
664 "strategy" => {
665 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 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 _ => {} }
708 }
709
710 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 if schema.enums.contains_key(target_model.as_str()) {
719 continue;
720 }
721
722 if schema.types.contains_key(target_model.as_str()) {
724 continue;
725 }
726
727 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 RelationType::OneToMany
737 } else {
738 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
776pub 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 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 assert!(schema.is_ok() || schema.is_err());
1025 }
1026
1027 #[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}