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 self.validate_computed_fields(&schema);
48
49 for e in schema.enums.values() {
51 self.validate_enum(e);
52 }
53
54 for t in schema.types.values() {
56 self.validate_composite_type(t, &schema);
57 }
58
59 for v in schema.views.values() {
61 self.validate_view(v, &schema);
62 }
63
64 for sg in schema.server_groups.values() {
66 self.validate_server_group(sg);
67 }
68
69 let relations = self.resolve_relations(&schema);
71 schema.relations = relations;
72
73 if self.errors.is_empty() {
74 Ok(schema)
75 } else {
76 Err(SchemaError::ValidationFailed {
77 count: self.errors.len(),
78 errors: std::mem::take(&mut self.errors),
79 })
80 }
81 }
82
83 fn check_duplicates(&mut self, schema: &Schema) {
85 let mut seen = std::collections::HashSet::new();
86
87 for name in schema.models.keys() {
88 if !seen.insert(name.as_str()) {
89 self.errors
90 .push(SchemaError::duplicate("model", name.as_str()));
91 }
92 }
93
94 for name in schema.enums.keys() {
95 if !seen.insert(name.as_str()) {
96 self.errors
97 .push(SchemaError::duplicate("enum", name.as_str()));
98 }
99 }
100
101 for name in schema.types.keys() {
102 if !seen.insert(name.as_str()) {
103 self.errors
104 .push(SchemaError::duplicate("type", name.as_str()));
105 }
106 }
107
108 for name in schema.views.keys() {
109 if !seen.insert(name.as_str()) {
110 self.errors
111 .push(SchemaError::duplicate("view", name.as_str()));
112 }
113 }
114
115 let mut server_group_names = std::collections::HashSet::new();
117 for name in schema.server_groups.keys() {
118 if !server_group_names.insert(name.as_str()) {
119 self.errors
120 .push(SchemaError::duplicate("serverGroup", name.as_str()));
121 }
122 }
123 }
124
125 fn resolve_field_types(&self, schema: &mut Schema) {
130 let enum_names: std::collections::HashSet<String> =
132 schema.enums.keys().map(|s| s.to_string()).collect();
133 let composite_names: std::collections::HashSet<String> =
134 schema.types.keys().map(|s| s.to_string()).collect();
135
136 for model in schema.models.values_mut() {
138 for field in model.fields.values_mut() {
139 if let FieldType::Model(ref type_name) = field.field_type {
140 let name = type_name.as_str();
141 if enum_names.contains(name) {
142 field.field_type = FieldType::Enum(type_name.clone());
143 } else if composite_names.contains(name) {
144 field.field_type = FieldType::Composite(type_name.clone());
145 }
146 }
147 }
148 }
149
150 for composite in schema.types.values_mut() {
152 for field in composite.fields.values_mut() {
153 if let FieldType::Model(ref type_name) = field.field_type {
154 let name = type_name.as_str();
155 if enum_names.contains(name) {
156 field.field_type = FieldType::Enum(type_name.clone());
157 } else if composite_names.contains(name) {
158 field.field_type = FieldType::Composite(type_name.clone());
159 }
160 }
161 }
162 }
163
164 for view in schema.views.values_mut() {
166 for field in view.fields.values_mut() {
167 if let FieldType::Model(ref type_name) = field.field_type {
168 let name = type_name.as_str();
169 if enum_names.contains(name) {
170 field.field_type = FieldType::Enum(type_name.clone());
171 } else if composite_names.contains(name) {
172 field.field_type = FieldType::Composite(type_name.clone());
173 }
174 }
175 }
176 }
177 }
178
179 fn validate_model(&mut self, model: &Model, schema: &Schema) {
181 let id_fields: Vec<_> = model.fields.values().filter(|f| f.is_id()).collect();
183 if id_fields.is_empty() && !self.has_composite_id(model) {
184 self.errors.push(SchemaError::MissingId {
185 model: model.name().to_string(),
186 });
187 }
188
189 for field in model.fields.values() {
191 self.validate_field(field, model.name(), schema);
192 }
193
194 for attr in &model.attributes {
196 self.validate_model_attribute(attr, model);
197 }
198 }
199
200 fn has_composite_id(&self, model: &Model) -> bool {
202 model.attributes.iter().any(|a| a.is("id"))
203 }
204
205 fn validate_field(&mut self, field: &Field, model_name: &str, schema: &Schema) {
207 match &field.field_type {
209 FieldType::Model(name) => {
210 if schema.models.contains_key(name.as_str()) {
212 } else if schema.enums.contains_key(name.as_str()) {
214 } else if schema.types.contains_key(name.as_str()) {
217 } else {
219 self.errors.push(SchemaError::unknown_type(
220 model_name,
221 field.name(),
222 name.as_str(),
223 ));
224 }
225 }
226 FieldType::Enum(name) => {
227 if !schema.enums.contains_key(name.as_str()) {
228 self.errors.push(SchemaError::unknown_type(
229 model_name,
230 field.name(),
231 name.as_str(),
232 ));
233 }
234 }
235 FieldType::Composite(name) if !schema.types.contains_key(name.as_str()) => {
236 self.errors.push(SchemaError::unknown_type(
237 model_name,
238 field.name(),
239 name.as_str(),
240 ));
241 }
242 _ => {}
243 }
244
245 for attr in &field.attributes {
247 self.validate_field_attribute(attr, field, model_name, schema);
248 }
249
250 if let FieldType::Model(ref target_name) = field.field_type {
253 let is_actual_relation = schema.models.contains_key(target_name.as_str())
255 && !schema.enums.contains_key(target_name.as_str())
256 && !schema.types.contains_key(target_name.as_str());
257
258 if is_actual_relation && !field.is_list() {
259 let attrs = field.extract_attributes();
261 if let Some(rel) = attrs.relation.as_ref() {
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 "map" => {
355 if let Some(AttributeValue::String(name)) = attr.first_arg()
359 && !is_safe_sql_identifier(name)
360 {
361 self.errors.push(SchemaError::invalid_field(
362 model_name,
363 field.name(),
364 format!(
365 "@map(\"{}\") contains characters outside [A-Za-z0-9_.]; \
366 SQL identifiers must be safe to splice into queries",
367 name
368 ),
369 ));
370 }
371 }
372 _ => {}
373 }
374 }
375
376 fn validate_default_value(
378 &mut self,
379 value: &AttributeValue,
380 field: &Field,
381 model_name: &str,
382 schema: &Schema,
383 ) {
384 match (&field.field_type, value) {
385 (_, AttributeValue::Function(_, _)) => {}
387
388 (FieldType::Scalar(ScalarType::Int), AttributeValue::Int(_)) => {}
390 (FieldType::Scalar(ScalarType::BigInt), AttributeValue::Int(_)) => {}
391
392 (FieldType::Scalar(ScalarType::Float), AttributeValue::Int(_)) => {}
394 (FieldType::Scalar(ScalarType::Float), AttributeValue::Float(_)) => {}
395 (FieldType::Scalar(ScalarType::Decimal), AttributeValue::Int(_)) => {}
396 (FieldType::Scalar(ScalarType::Decimal), AttributeValue::Float(_)) => {}
397
398 (FieldType::Scalar(ScalarType::String), AttributeValue::String(_)) => {}
400
401 (FieldType::Scalar(ScalarType::Json), AttributeValue::String(_))
406 | (FieldType::Scalar(ScalarType::Json), AttributeValue::Array(_))
407 | (FieldType::Scalar(ScalarType::Json), AttributeValue::Boolean(_))
408 | (FieldType::Scalar(ScalarType::Json), AttributeValue::Int(_))
409 | (FieldType::Scalar(ScalarType::Json), AttributeValue::Float(_)) => {}
410
411 (FieldType::Scalar(ScalarType::Boolean), AttributeValue::Boolean(_)) => {}
413
414 (FieldType::Enum(enum_name), AttributeValue::Ident(variant)) => {
416 if let Some(e) = schema.enums.get(enum_name.as_str())
417 && e.get_variant(variant).is_none()
418 {
419 self.errors.push(SchemaError::invalid_field(
420 model_name,
421 field.name(),
422 format!(
423 "default value '{}' is not a valid variant of enum '{}'",
424 variant, enum_name
425 ),
426 ));
427 }
428 }
429
430 (FieldType::Model(type_name), AttributeValue::Ident(variant)) => {
432 if let Some(e) = schema.enums.get(type_name.as_str())
434 && e.get_variant(variant).is_none()
435 {
436 self.errors.push(SchemaError::invalid_field(
437 model_name,
438 field.name(),
439 format!(
440 "default value '{}' is not a valid variant of enum '{}'",
441 variant, type_name
442 ),
443 ));
444 }
445 }
448
449 _ => {
451 self.errors.push(SchemaError::invalid_field(
452 model_name,
453 field.name(),
454 format!(
455 "default value type does not match field type '{}'",
456 field.field_type
457 ),
458 ));
459 }
460 }
461 }
462
463 fn validate_model_attribute(&mut self, attr: &Attribute, model: &Model) {
465 match attr.name() {
466 "index" | "unique" => {
467 if let Some(AttributeValue::FieldRefList(fields)) = attr.first_arg() {
469 for field_name in fields {
470 if !model.fields.contains_key(field_name.as_str()) {
471 self.errors.push(SchemaError::invalid_model(
472 model.name(),
473 format!(
474 "@@{} references non-existent field '{}'",
475 attr.name(),
476 field_name
477 ),
478 ));
479 }
480 }
481 }
482 }
483 "id" => {
484 if let Some(AttributeValue::FieldRefList(fields)) = attr.first_arg() {
486 for field_name in fields {
487 if !model.fields.contains_key(field_name.as_str()) {
488 self.errors.push(SchemaError::invalid_model(
489 model.name(),
490 format!("@@id references non-existent field '{}'", field_name),
491 ));
492 }
493 }
494 }
495 }
496 "search" => {
497 if let Some(AttributeValue::FieldRefList(fields)) = attr.first_arg() {
499 for field_name in fields {
500 if let Some(field) = model.fields.get(field_name.as_str()) {
501 if !matches!(field.field_type, FieldType::Scalar(ScalarType::String)) {
503 self.errors.push(SchemaError::invalid_model(
504 model.name(),
505 format!(
506 "@@search field '{}' must be of type String",
507 field_name
508 ),
509 ));
510 }
511 } else {
512 self.errors.push(SchemaError::invalid_model(
513 model.name(),
514 format!("@@search references non-existent field '{}'", field_name),
515 ));
516 }
517 }
518 }
519 }
520 "map" => {
521 if let Some(AttributeValue::String(name)) = attr.first_arg()
529 && !is_safe_sql_identifier(name)
530 {
531 self.errors.push(SchemaError::invalid_model(
532 model.name(),
533 format!(
534 "@@map(\"{}\") contains characters outside [A-Za-z0-9_.]; \
535 SQL identifiers must be safe to splice into queries",
536 name
537 ),
538 ));
539 }
540 }
541 _ => {}
542 }
543 }
544
545 fn validate_enum(&mut self, e: &Enum) {
547 if e.variants.is_empty() {
548 self.errors.push(SchemaError::invalid_model(
549 e.name(),
550 "enum must have at least one variant".to_string(),
551 ));
552 }
553
554 let mut seen = std::collections::HashSet::new();
556 for variant in &e.variants {
557 if !seen.insert(variant.name()) {
558 self.errors.push(SchemaError::duplicate(
559 format!("enum variant in {}", e.name()),
560 variant.name(),
561 ));
562 }
563 }
564 }
565
566 fn validate_composite_type(&mut self, t: &CompositeType, schema: &Schema) {
568 if t.fields.is_empty() {
569 self.errors.push(SchemaError::invalid_model(
570 t.name(),
571 "composite type must have at least one field".to_string(),
572 ));
573 }
574
575 for field in t.fields.values() {
577 match &field.field_type {
578 FieldType::Model(_) => {
579 self.errors.push(SchemaError::invalid_field(
580 t.name(),
581 field.name(),
582 "composite types cannot have model relations".to_string(),
583 ));
584 }
585 FieldType::Enum(name) => {
586 if !schema.enums.contains_key(name.as_str()) {
587 self.errors.push(SchemaError::unknown_type(
588 t.name(),
589 field.name(),
590 name.as_str(),
591 ));
592 }
593 }
594 FieldType::Composite(name) if !schema.types.contains_key(name.as_str()) => {
595 self.errors.push(SchemaError::unknown_type(
596 t.name(),
597 field.name(),
598 name.as_str(),
599 ));
600 }
601 _ => {}
602 }
603 }
604 }
605
606 fn validate_view(&mut self, v: &View, schema: &Schema) {
608 if v.fields.is_empty() {
610 self.errors.push(SchemaError::invalid_model(
611 v.name(),
612 "view must have at least one field".to_string(),
613 ));
614 }
615
616 for field in v.fields.values() {
618 self.validate_field(field, v.name(), schema);
619 }
620 }
621
622 fn validate_server_group(&mut self, sg: &ServerGroup) {
624 if sg.servers.is_empty() {
626 self.errors.push(SchemaError::invalid_model(
627 sg.name.name.as_str(),
628 "serverGroup must have at least one server".to_string(),
629 ));
630 }
631
632 let mut seen_servers = std::collections::HashSet::new();
634 for server_name in sg.servers.keys() {
635 if !seen_servers.insert(server_name.as_str()) {
636 self.errors.push(SchemaError::duplicate(
637 format!("server in serverGroup {}", sg.name.name),
638 server_name.as_str(),
639 ));
640 }
641 }
642
643 for server in sg.servers.values() {
645 self.validate_server(server, sg.name.name.as_str());
646 }
647
648 for attr in &sg.attributes {
650 self.validate_server_group_attribute(attr, sg);
651 }
652
653 if let Some(strategy) = sg.strategy()
655 && strategy == ServerGroupStrategy::ReadReplica
656 {
657 let has_primary = sg
658 .servers
659 .values()
660 .any(|s| s.role() == Some(ServerRole::Primary));
661 if !has_primary {
662 self.errors.push(SchemaError::invalid_model(
663 sg.name.name.as_str(),
664 "ReadReplica strategy requires at least one server with role = \"primary\""
665 .to_string(),
666 ));
667 }
668 }
669 }
670
671 fn validate_server(&mut self, server: &Server, group_name: &str) {
673 if server.url().is_none() {
675 self.errors.push(SchemaError::invalid_model(
676 group_name,
677 format!("server '{}' must have a 'url' property", server.name.name),
678 ));
679 }
680
681 if let Some(weight) = server.weight()
683 && weight == 0
684 {
685 self.errors.push(SchemaError::invalid_model(
686 group_name,
687 format!(
688 "server '{}' weight must be greater than 0",
689 server.name.name
690 ),
691 ));
692 }
693
694 if let Some(priority) = server.priority()
696 && priority == 0
697 {
698 self.errors.push(SchemaError::invalid_model(
699 group_name,
700 format!(
701 "server '{}' priority must be greater than 0",
702 server.name.name
703 ),
704 ));
705 }
706 }
707
708 fn validate_server_group_attribute(&mut self, attr: &Attribute, sg: &ServerGroup) {
710 match attr.name() {
711 "strategy" => {
712 if let Some(arg) = attr.first_arg() {
714 let value_str = arg
715 .as_string()
716 .map(|s| s.to_string())
717 .or_else(|| arg.as_ident().map(|s| s.to_string()));
718 if let Some(val) = value_str
719 && ServerGroupStrategy::parse(&val).is_none()
720 {
721 self.errors.push(SchemaError::InvalidAttribute {
722 attribute: "strategy".to_string(),
723 message: format!(
724 "invalid strategy '{}' for serverGroup '{}'. Valid values: ReadReplica, Sharding, MultiRegion, HighAvailability, Custom",
725 val,
726 sg.name.name
727 ),
728 });
729 }
730 }
731 }
732 "loadBalance" => {
733 if let Some(arg) = attr.first_arg() {
735 let value_str = arg
736 .as_string()
737 .map(|s| s.to_string())
738 .or_else(|| arg.as_ident().map(|s| s.to_string()));
739 if let Some(val) = value_str
740 && LoadBalanceStrategy::parse(&val).is_none()
741 {
742 self.errors.push(SchemaError::InvalidAttribute {
743 attribute: "loadBalance".to_string(),
744 message: format!(
745 "invalid loadBalance '{}' for serverGroup '{}'. Valid values: RoundRobin, Random, LeastConnections, Weighted, Nearest, Sticky",
746 val,
747 sg.name.name
748 ),
749 });
750 }
751 }
752 }
753 _ => {} }
755 }
756
757 fn validate_computed_fields(&mut self, schema: &Schema) {
768 for model in schema.models.values() {
769 for field in model.fields.values() {
770 let attrs = field.extract_attributes();
771
772 if let Some(g) = &attrs.generated {
774 if attrs.is_id || attrs.is_auto {
775 self.errors.push(SchemaError::invalid_field(
776 model.name(),
777 field.name(),
778 format!(
779 "field `{}` cannot be both @generated and @id/@auto",
780 field.name()
781 ),
782 ));
783 }
784 if attrs.aggregate.is_some() {
785 self.errors.push(SchemaError::invalid_field(
786 model.name(),
787 field.name(),
788 format!(
789 "field `{}` cannot be both @generated and an aggregate",
790 field.name()
791 ),
792 ));
793 }
794 if g.expression.trim().is_empty() {
795 self.errors.push(SchemaError::invalid_field(
796 model.name(),
797 field.name(),
798 format!(
799 "field `{}`: @generated expression must not be empty",
800 field.name()
801 ),
802 ));
803 }
804 }
805
806 for raw_attr in &field.attributes {
811 match raw_attr.name() {
812 "count" => {
813 if let Some(path) = raw_attr.first_path_arg() {
817 if path.contains('.') {
818 self.errors.push(SchemaError::invalid_field(
819 model.name(),
820 field.name(),
821 format!(
822 "field `{}`: @count takes a relation name, not `relation.field`",
823 field.name()
824 ),
825 ));
826 } else {
827 let rel_exists = model
829 .fields
830 .values()
831 .any(|f| f.name() == path && f.is_list());
832 if !rel_exists {
833 self.errors.push(SchemaError::invalid_field(
834 model.name(),
835 field.name(),
836 format!(
837 "field `{}`: unknown relation `{}` in @count",
838 field.name(),
839 path
840 ),
841 ));
842 }
843 }
844 }
845 }
846 "sum" | "avg" | "min" | "max" => {
847 let kind_name = raw_attr.name();
848 if let Some(path) = raw_attr.first_path_arg() {
849 if let Some((rel, _field_part)) = path.split_once('.') {
850 let rel_exists = model
852 .fields
853 .values()
854 .any(|f| f.name() == rel && f.is_list());
855 if !rel_exists {
856 self.errors.push(SchemaError::invalid_field(
857 model.name(),
858 field.name(),
859 format!(
860 "field `{}`: unknown relation `{}` in @{}",
861 field.name(),
862 rel,
863 kind_name
864 ),
865 ));
866 }
867 } else {
868 self.errors.push(SchemaError::invalid_field(
870 model.name(),
871 field.name(),
872 format!(
873 "field `{}`: @{} requires `relation.field`",
874 field.name(),
875 kind_name
876 ),
877 ));
878 }
879 }
880 }
881 _ => {}
882 }
883 }
884
885 let has_generated = attrs.generated.is_some();
887 for attr_name in ["stored", "virtual"] {
888 if field.has_attribute(attr_name) && !has_generated {
889 self.errors.push(SchemaError::invalid_field(
890 model.name(),
891 field.name(),
892 format!(
893 "field `{}`: @{} is only valid alongside @generated",
894 field.name(),
895 attr_name
896 ),
897 ));
898 }
899 }
900 }
901 }
902 }
903
904 fn resolve_relations(&mut self, schema: &Schema) -> Vec<Relation> {
906 let mut relations = Vec::new();
907
908 for model in schema.models.values() {
909 for field in model.fields.values() {
910 if let FieldType::Model(ref target_model) = field.field_type {
911 if schema.enums.contains_key(target_model.as_str()) {
913 continue;
914 }
915
916 if schema.types.contains_key(target_model.as_str()) {
918 continue;
919 }
920
921 if !schema.models.contains_key(target_model.as_str()) {
923 continue;
924 }
925
926 let attrs = field.extract_attributes();
927
928 let relation_type = if field.is_list() {
929 RelationType::OneToMany
931 } else {
932 RelationType::ManyToOne
934 };
935
936 let mut relation = Relation::new(
937 model.name(),
938 field.name(),
939 target_model.as_str(),
940 relation_type,
941 );
942
943 if let Some(rel_attr) = &attrs.relation {
944 if let Some(name) = &rel_attr.name {
945 relation = relation.with_name(name.as_str());
946 }
947 if !rel_attr.fields.is_empty() {
948 relation = relation.with_from_fields(rel_attr.fields.clone());
949 }
950 if !rel_attr.references.is_empty() {
951 relation = relation.with_to_fields(rel_attr.references.clone());
952 }
953 if let Some(action) = rel_attr.on_delete {
954 relation = relation.with_on_delete(action);
955 }
956 if let Some(action) = rel_attr.on_update {
957 relation = relation.with_on_update(action);
958 }
959 if let Some(map) = &rel_attr.map {
960 relation = relation.with_map(map.as_str());
961 }
962 }
963
964 relations.push(relation);
965 }
966 }
967 }
968
969 relations
970 }
971}
972
973fn is_safe_sql_identifier(s: &str) -> bool {
978 !s.is_empty()
979 && s.chars()
980 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
981}
982
983pub fn validate_schema(input: &str) -> SchemaResult<Schema> {
985 let schema = crate::parser::parse_schema(input)?;
986 let mut validator = Validator::new();
987 validator.validate(schema)
988}
989
990#[cfg(test)]
991mod tests {
992 use super::*;
993
994 #[test]
995 fn test_validate_simple_model() {
996 let schema = validate_schema(
997 r#"
998 model User {
999 id Int @id @auto
1000 email String @unique
1001 }
1002 "#,
1003 )
1004 .unwrap();
1005
1006 assert_eq!(schema.models.len(), 1);
1007 }
1008
1009 #[test]
1010 fn test_validate_model_missing_id() {
1011 let result = validate_schema(
1012 r#"
1013 model User {
1014 email String
1015 name String
1016 }
1017 "#,
1018 );
1019
1020 assert!(result.is_err());
1021 let err = result.unwrap_err();
1022 assert!(matches!(err, SchemaError::ValidationFailed { .. }));
1023 }
1024
1025 #[test]
1026 fn test_validate_model_with_composite_id() {
1027 let schema = validate_schema(
1028 r#"
1029 model PostTag {
1030 post_id Int
1031 tag_id Int
1032
1033 @@id([post_id, tag_id])
1034 }
1035 "#,
1036 )
1037 .unwrap();
1038
1039 assert_eq!(schema.models.len(), 1);
1040 }
1041
1042 #[test]
1043 fn test_validate_unknown_type_reference() {
1044 let result = validate_schema(
1045 r#"
1046 model User {
1047 id Int @id @auto
1048 profile UnknownType
1049 }
1050 "#,
1051 );
1052
1053 assert!(result.is_err());
1054 }
1055
1056 #[test]
1057 fn test_validate_enum_reference() {
1058 let schema = validate_schema(
1059 r#"
1060 enum Role {
1061 User
1062 Admin
1063 }
1064
1065 model User {
1066 id Int @id @auto
1067 role Role @default(User)
1068 }
1069 "#,
1070 )
1071 .unwrap();
1072
1073 assert_eq!(schema.models.len(), 1);
1074 assert_eq!(schema.enums.len(), 1);
1075 }
1076
1077 #[test]
1078 fn test_validate_enum_field_type_resolved() {
1079 let schema = validate_schema(
1080 r#"
1081 enum Role {
1082 User
1083 Admin
1084 }
1085
1086 model User {
1087 id Int @id @auto
1088 role Role @default(User)
1089 }
1090 "#,
1091 )
1092 .unwrap();
1093
1094 let role = schema
1098 .models
1099 .get("User")
1100 .unwrap()
1101 .fields
1102 .get("role")
1103 .unwrap();
1104 assert_eq!(role.field_type, FieldType::Enum("Role".into()));
1105 }
1106
1107 #[test]
1108 fn test_validate_invalid_enum_default() {
1109 let result = validate_schema(
1110 r#"
1111 enum Role {
1112 User
1113 Admin
1114 }
1115
1116 model User {
1117 id Int @id @auto
1118 role Role @default(Unknown)
1119 }
1120 "#,
1121 );
1122
1123 assert!(result.is_err());
1124 }
1125
1126 #[test]
1127 fn test_validate_auto_on_non_int() {
1128 let result = validate_schema(
1129 r#"
1130 model User {
1131 id String @id @auto
1132 email String
1133 }
1134 "#,
1135 );
1136
1137 assert!(result.is_err());
1138 }
1139
1140 #[test]
1141 fn test_validate_updated_at_on_non_datetime() {
1142 let result = validate_schema(
1143 r#"
1144 model User {
1145 id Int @id @auto
1146 updated_at String @updated_at
1147 }
1148 "#,
1149 );
1150
1151 assert!(result.is_err());
1152 }
1153
1154 #[test]
1155 fn test_validate_empty_enum() {
1156 let result = validate_schema(
1157 r#"
1158 enum Empty {
1159 }
1160
1161 model User {
1162 id Int @id @auto
1163 }
1164 "#,
1165 );
1166
1167 assert!(result.is_err());
1168 }
1169
1170 #[test]
1171 fn test_validate_duplicate_model_names() {
1172 let result = validate_schema(
1173 r#"
1174 model User {
1175 id Int @id @auto
1176 }
1177
1178 model User {
1179 id Int @id @auto
1180 }
1181 "#,
1182 );
1183
1184 assert!(result.is_ok() || result.is_err());
1187 }
1188
1189 #[test]
1190 fn test_validate_relation() {
1191 let schema = validate_schema(
1192 r#"
1193 model User {
1194 id Int @id @auto
1195 posts Post[]
1196 }
1197
1198 model Post {
1199 id Int @id @auto
1200 author_id Int
1201 author User @relation(fields: [author_id], references: [id])
1202 }
1203 "#,
1204 )
1205 .unwrap();
1206
1207 assert_eq!(schema.models.len(), 2);
1208 assert!(!schema.relations.is_empty());
1209 }
1210
1211 #[test]
1212 fn test_validate_index_with_invalid_field() {
1213 let result = validate_schema(
1214 r#"
1215 model User {
1216 id Int @id @auto
1217 email String
1218
1219 @@index([nonexistent])
1220 }
1221 "#,
1222 );
1223
1224 assert!(result.is_err());
1225 }
1226
1227 #[test]
1228 fn test_validate_search_on_non_string_field() {
1229 let result = validate_schema(
1230 r#"
1231 model Post {
1232 id Int @id @auto
1233 views Int
1234
1235 @@search([views])
1236 }
1237 "#,
1238 );
1239
1240 assert!(result.is_err());
1241 }
1242
1243 #[test]
1244 fn test_validate_composite_type() {
1245 let schema = validate_schema(
1246 r#"
1247 type Address {
1248 street String
1249 city String
1250 country String @default("US")
1251 }
1252
1253 model User {
1254 id Int @id @auto
1255 address Address
1256 }
1257 "#,
1258 );
1259
1260 assert!(schema.is_ok() || schema.is_err());
1262 }
1263
1264 #[test]
1267 fn test_validate_server_group_basic() {
1268 let schema = validate_schema(
1269 r#"
1270 model User {
1271 id Int @id @auto
1272 }
1273
1274 serverGroup MainCluster {
1275 server primary {
1276 url = "postgres://localhost/db"
1277 role = "primary"
1278 }
1279 }
1280 "#,
1281 )
1282 .unwrap();
1283
1284 assert_eq!(schema.server_groups.len(), 1);
1285 }
1286
1287 #[test]
1288 fn test_validate_server_group_empty_servers() {
1289 let result = validate_schema(
1290 r#"
1291 model User {
1292 id Int @id @auto
1293 }
1294
1295 serverGroup EmptyCluster {
1296 }
1297 "#,
1298 );
1299
1300 assert!(result.is_err());
1301 }
1302
1303 #[test]
1304 fn test_validate_server_group_missing_url() {
1305 let result = validate_schema(
1306 r#"
1307 model User {
1308 id Int @id @auto
1309 }
1310
1311 serverGroup Cluster {
1312 server db {
1313 role = "primary"
1314 }
1315 }
1316 "#,
1317 );
1318
1319 assert!(result.is_err());
1320 }
1321
1322 #[test]
1323 fn test_validate_server_group_invalid_strategy() {
1324 let result = validate_schema(
1325 r#"
1326 model User {
1327 id Int @id @auto
1328 }
1329
1330 serverGroup Cluster {
1331 @@strategy(InvalidStrategy)
1332
1333 server db {
1334 url = "postgres://localhost/db"
1335 }
1336 }
1337 "#,
1338 );
1339
1340 assert!(result.is_err());
1341 }
1342
1343 #[test]
1344 fn test_validate_server_group_valid_strategy() {
1345 let schema = validate_schema(
1346 r#"
1347 model User {
1348 id Int @id @auto
1349 }
1350
1351 serverGroup Cluster {
1352 @@strategy(ReadReplica)
1353 @@loadBalance(RoundRobin)
1354
1355 server primary {
1356 url = "postgres://localhost/db"
1357 role = "primary"
1358 }
1359 }
1360 "#,
1361 )
1362 .unwrap();
1363
1364 assert_eq!(schema.server_groups.len(), 1);
1365 }
1366
1367 #[test]
1368 fn test_validate_server_group_read_replica_needs_primary() {
1369 let result = validate_schema(
1370 r#"
1371 model User {
1372 id Int @id @auto
1373 }
1374
1375 serverGroup Cluster {
1376 @@strategy(ReadReplica)
1377
1378 server replica1 {
1379 url = "postgres://localhost/db"
1380 role = "replica"
1381 }
1382 }
1383 "#,
1384 );
1385
1386 assert!(result.is_err());
1387 }
1388
1389 #[test]
1390 fn test_validate_server_group_with_replicas() {
1391 let schema = validate_schema(
1392 r#"
1393 model User {
1394 id Int @id @auto
1395 }
1396
1397 serverGroup Cluster {
1398 @@strategy(ReadReplica)
1399
1400 server primary {
1401 url = "postgres://primary/db"
1402 role = "primary"
1403 weight = 1
1404 }
1405
1406 server replica1 {
1407 url = "postgres://replica1/db"
1408 role = "replica"
1409 weight = 2
1410 }
1411
1412 server replica2 {
1413 url = "postgres://replica2/db"
1414 role = "replica"
1415 weight = 2
1416 region = "us-west-1"
1417 }
1418 }
1419 "#,
1420 )
1421 .unwrap();
1422
1423 let cluster = schema.get_server_group("Cluster").unwrap();
1424 assert_eq!(cluster.servers.len(), 3);
1425 }
1426
1427 #[test]
1428 fn test_validate_server_group_zero_weight() {
1429 let result = validate_schema(
1430 r#"
1431 model User {
1432 id Int @id @auto
1433 }
1434
1435 serverGroup Cluster {
1436 server db {
1437 url = "postgres://localhost/db"
1438 weight = 0
1439 }
1440 }
1441 "#,
1442 );
1443
1444 assert!(result.is_err());
1445 }
1446
1447 #[test]
1448 fn test_validate_server_group_invalid_load_balance() {
1449 let result = validate_schema(
1450 r#"
1451 model User {
1452 id Int @id @auto
1453 }
1454
1455 serverGroup Cluster {
1456 @@loadBalance(InvalidStrategy)
1457
1458 server db {
1459 url = "postgres://localhost/db"
1460 }
1461 }
1462 "#,
1463 );
1464
1465 assert!(result.is_err());
1466 }
1467}