1use super::ir::AuthoringIR;
13use crate::{
14 error::{FraiseQLError, Result},
15 schema::is_known_scalar,
16};
17
18fn extract_base_type(type_str: &str) -> &str {
27 let s = type_str.trim();
28
29 let s = s.trim_start_matches('[').trim_end_matches(']');
31 let s = s.trim_end_matches('!').trim_start_matches('!');
32
33 let s = s.trim_start_matches('[').trim_end_matches(']');
35 let s = s.trim_end_matches('!');
36
37 s.trim()
38}
39
40fn is_valid_type(base_type: &str, defined_types: &std::collections::HashSet<&str>) -> bool {
42 is_known_scalar(base_type) || defined_types.contains(base_type)
43}
44
45#[derive(Debug, Clone, PartialEq)]
47pub struct ValidationError {
48 pub message: String,
50 pub location: String,
52}
53
54pub struct SchemaValidator {
56 }
58
59impl SchemaValidator {
60 #[must_use]
62 pub fn new() -> Self {
63 Self {}
64 }
65
66 pub fn validate(&self, ir: AuthoringIR) -> Result<AuthoringIR> {
80 self.validate_types(&ir)?;
84 self.validate_queries(&ir)?;
85
86 if !ir.fact_tables.is_empty() {
88 self.validate_fact_tables(&ir)?;
89 }
90
91 self.validate_aggregate_types(&ir)?;
94
95 Ok(ir)
96 }
97
98 fn validate_types(&self, ir: &AuthoringIR) -> Result<()> {
100 let defined_types: std::collections::HashSet<&str> =
102 ir.types.iter().map(|t| t.name.as_str()).collect();
103
104 for ir_type in &ir.types {
106 if ir_type.name.is_empty() {
108 return Err(FraiseQLError::Validation {
109 message: "Type name cannot be empty".to_string(),
110 path: Some("types".to_string()),
111 });
112 }
113
114 for field in &ir_type.fields {
116 let base_type = extract_base_type(&field.field_type);
117
118 if !base_type.is_empty() && !is_valid_type(base_type, &defined_types) {
120 return Err(FraiseQLError::Validation {
121 message: format!(
122 "Type '{}' field '{}' references unknown type '{}'",
123 ir_type.name, field.name, base_type
124 ),
125 path: Some(format!("types.{}.fields.{}", ir_type.name, field.name)),
126 });
127 }
128 }
129 }
130
131 Ok(())
132 }
133
134 fn validate_queries(&self, ir: &AuthoringIR) -> Result<()> {
136 let defined_types: std::collections::HashSet<&str> =
138 ir.types.iter().map(|t| t.name.as_str()).collect();
139
140 for query in &ir.queries {
142 if query.name.is_empty() {
144 return Err(FraiseQLError::Validation {
145 message: "Query name cannot be empty".to_string(),
146 path: Some("queries".to_string()),
147 });
148 }
149
150 let base_type = extract_base_type(&query.return_type);
152 if !is_valid_type(base_type, &defined_types) {
153 return Err(FraiseQLError::Validation {
154 message: format!(
155 "Query '{}' returns unknown type '{}'",
156 query.name, query.return_type
157 ),
158 path: Some(format!("queries.{}.return_type", query.name)),
159 });
160 }
161
162 for arg in &query.arguments {
164 let base_type = extract_base_type(&arg.arg_type);
165 if !is_valid_type(base_type, &defined_types) {
166 return Err(FraiseQLError::Validation {
167 message: format!(
168 "Query '{}' argument '{}' has unknown type '{}'",
169 query.name, arg.name, arg.arg_type
170 ),
171 path: Some(format!("queries.{}.arguments.{}", query.name, arg.name)),
172 });
173 }
174 }
175 }
176
177 for mutation in &ir.mutations {
179 if mutation.name.is_empty() {
180 return Err(FraiseQLError::Validation {
181 message: "Mutation name cannot be empty".to_string(),
182 path: Some("mutations".to_string()),
183 });
184 }
185
186 let base_type = extract_base_type(&mutation.return_type);
187 if !is_valid_type(base_type, &defined_types) {
188 return Err(FraiseQLError::Validation {
189 message: format!(
190 "Mutation '{}' returns unknown type '{}'",
191 mutation.name, mutation.return_type
192 ),
193 path: Some(format!("mutations.{}.return_type", mutation.name)),
194 });
195 }
196 }
197
198 for subscription in &ir.subscriptions {
200 if subscription.name.is_empty() {
201 return Err(FraiseQLError::Validation {
202 message: "Subscription name cannot be empty".to_string(),
203 path: Some("subscriptions".to_string()),
204 });
205 }
206
207 let base_type = extract_base_type(&subscription.return_type);
208 if !is_valid_type(base_type, &defined_types) {
209 return Err(FraiseQLError::Validation {
210 message: format!(
211 "Subscription '{}' returns unknown type '{}'",
212 subscription.name, subscription.return_type
213 ),
214 path: Some(format!("subscriptions.{}.return_type", subscription.name)),
215 });
216 }
217 }
218
219 Ok(())
220 }
221
222 fn validate_fact_tables(&self, ir: &AuthoringIR) -> Result<()> {
230 for (table_name, metadata) in &ir.fact_tables {
231 if !table_name.starts_with("tf_") {
233 return Err(FraiseQLError::Validation {
234 message: format!("Fact table '{}' must start with 'tf_' prefix", table_name),
235 path: Some(format!("fact_tables.{}", table_name)),
236 });
237 }
238
239 let obj = metadata.as_object().ok_or_else(|| FraiseQLError::Validation {
241 message: format!("Fact table '{}' metadata must be an object", table_name),
242 path: Some(format!("fact_tables.{}", table_name)),
243 })?;
244
245 let measures = obj.get("measures").ok_or_else(|| FraiseQLError::Validation {
247 message: format!("Fact table '{}' missing 'measures' field", table_name),
248 path: Some(format!("fact_tables.{}.measures", table_name)),
249 })?;
250
251 let measures_arr = measures.as_array().ok_or_else(|| FraiseQLError::Validation {
252 message: format!("Fact table '{}' measures must be an array", table_name),
253 path: Some(format!("fact_tables.{}.measures", table_name)),
254 })?;
255
256 if measures_arr.is_empty() {
257 return Err(FraiseQLError::Validation {
258 message: format!("Fact table '{}' must have at least one measure", table_name),
259 path: Some(format!("fact_tables.{}.measures", table_name)),
260 });
261 }
262
263 for (idx, measure) in measures_arr.iter().enumerate() {
265 let measure_obj = measure.as_object().ok_or_else(|| FraiseQLError::Validation {
266 message: format!(
267 "Fact table '{}' measure {} must be an object",
268 table_name, idx
269 ),
270 path: Some(format!("fact_tables.{}.measures[{}]", table_name, idx)),
271 })?;
272
273 if !measure_obj.contains_key("name") {
275 return Err(FraiseQLError::Validation {
276 message: format!(
277 "Fact table '{}' measure {} missing 'name' field",
278 table_name, idx
279 ),
280 path: Some(format!("fact_tables.{}.measures[{}]", table_name, idx)),
281 });
282 }
283
284 if !measure_obj.contains_key("sql_type") {
286 return Err(FraiseQLError::Validation {
287 message: format!(
288 "Fact table '{}' measure {} missing 'sql_type' field",
289 table_name, idx
290 ),
291 path: Some(format!("fact_tables.{}.measures[{}]", table_name, idx)),
292 });
293 }
294 }
295
296 let dimensions = obj.get("dimensions").ok_or_else(|| FraiseQLError::Validation {
298 message: format!("Fact table '{}' missing 'dimensions' field", table_name),
299 path: Some(format!("fact_tables.{}.dimensions", table_name)),
300 })?;
301
302 let dimensions_obj =
303 dimensions.as_object().ok_or_else(|| FraiseQLError::Validation {
304 message: format!("Fact table '{}' dimensions must be an object", table_name),
305 path: Some(format!("fact_tables.{}.dimensions", table_name)),
306 })?;
307
308 if !dimensions_obj.contains_key("name") {
310 return Err(FraiseQLError::Validation {
311 message: format!("Fact table '{}' dimensions missing 'name' field", table_name),
312 path: Some(format!("fact_tables.{}.dimensions", table_name)),
313 });
314 }
315
316 if let Some(filters) = obj.get("denormalized_filters") {
318 if !filters.is_array() {
319 return Err(FraiseQLError::Validation {
320 message: format!(
321 "Fact table '{}' denormalized_filters must be an array",
322 table_name
323 ),
324 path: Some(format!("fact_tables.{}.denormalized_filters", table_name)),
325 });
326 }
327 }
328 }
329
330 Ok(())
331 }
332
333 fn validate_aggregate_types(&self, ir: &AuthoringIR) -> Result<()> {
341 for ir_type in &ir.types {
343 if ir_type.name.ends_with("Aggregate") {
344 let has_count = ir_type.fields.iter().any(|f| f.name == "count");
346 if !has_count {
347 return Err(FraiseQLError::Validation {
348 message: format!(
349 "Aggregate type '{}' must have a 'count' field",
350 ir_type.name
351 ),
352 path: Some(format!("types.{}.fields", ir_type.name)),
353 });
354 }
355 }
356
357 if ir_type.name.ends_with("GroupByInput") {
359 for field in &ir_type.fields {
360 if field.field_type != "Boolean" && field.field_type != "Boolean!" {
362 return Err(FraiseQLError::Validation {
363 message: format!(
364 "GroupByInput type '{}' field '{}' must be Boolean, got '{}'",
365 ir_type.name, field.name, field.field_type
366 ),
367 path: Some(format!("types.{}.fields.{}", ir_type.name, field.name)),
368 });
369 }
370 }
371 }
372
373 if ir_type.name.ends_with("HavingInput") {
375 for field in &ir_type.fields {
376 let valid_suffixes = ["_eq", "_neq", "_gt", "_gte", "_lt", "_lte"];
378 let has_valid_suffix = valid_suffixes.iter().any(|s| field.name.ends_with(s));
379
380 if !has_valid_suffix {
381 return Err(FraiseQLError::Validation {
382 message: format!(
383 "HavingInput type '{}' field '{}' must have operator suffix (_eq, _neq, _gt, _gte, _lt, _lte)",
384 ir_type.name, field.name
385 ),
386 path: Some(format!("types.{}.fields.{}", ir_type.name, field.name)),
387 });
388 }
389 }
390 }
391 }
392
393 Ok(())
394 }
395}
396
397impl Default for SchemaValidator {
398 fn default() -> Self {
399 Self::new()
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use serde_json::json;
406
407 use super::{
408 super::ir::{IRField, IRType},
409 *,
410 };
411
412 #[test]
413 fn test_validator_new() {
414 let validator = SchemaValidator::new();
415 let ir = AuthoringIR::new();
416 let result = validator.validate(ir.clone());
417 assert!(result.is_ok());
418 }
419
420 #[test]
421 fn test_validate_empty_ir() {
422 let validator = SchemaValidator::new();
423 let ir = AuthoringIR::new();
424 let result = validator.validate(ir);
425 assert!(result.is_ok());
426 }
427
428 #[test]
429 fn test_validate_fact_table_with_valid_metadata() {
430 let validator = SchemaValidator::new();
431 let mut ir = AuthoringIR::new();
432
433 let metadata = json!({
434 "table_name": "tf_sales",
435 "measures": [
436 {"name": "revenue", "sql_type": "Decimal", "nullable": false}
437 ],
438 "dimensions": {
439 "name": "data",
440 "paths": []
441 },
442 "denormalized_filters": []
443 });
444
445 ir.fact_tables.insert("tf_sales".to_string(), metadata);
446
447 let result = validator.validate(ir);
448 assert!(result.is_ok());
449 }
450
451 #[test]
452 fn test_validate_fact_table_invalid_prefix() {
453 let validator = SchemaValidator::new();
454 let mut ir = AuthoringIR::new();
455
456 let metadata = json!({
457 "measures": [{"name": "revenue", "sql_type": "Decimal"}],
458 "dimensions": {"name": "data"}
459 });
460
461 ir.fact_tables.insert("sales".to_string(), metadata);
462
463 let result = validator.validate(ir);
464 assert!(result.is_err());
465 if let Err(FraiseQLError::Validation { message, .. }) = result {
466 assert!(message.contains("must start with 'tf_' prefix"));
467 }
468 }
469
470 #[test]
471 fn test_validate_fact_table_missing_measures() {
472 let validator = SchemaValidator::new();
473 let mut ir = AuthoringIR::new();
474
475 let metadata = json!({
476 "dimensions": {"name": "data"}
477 });
478
479 ir.fact_tables.insert("tf_sales".to_string(), metadata);
480
481 let result = validator.validate(ir);
482 assert!(result.is_err());
483 if let Err(FraiseQLError::Validation { message, .. }) = result {
484 assert!(message.contains("missing 'measures' field"));
485 }
486 }
487
488 #[test]
489 fn test_validate_fact_table_empty_measures() {
490 let validator = SchemaValidator::new();
491 let mut ir = AuthoringIR::new();
492
493 let metadata = json!({
494 "measures": [],
495 "dimensions": {"name": "data"}
496 });
497
498 ir.fact_tables.insert("tf_sales".to_string(), metadata);
499
500 let result = validator.validate(ir);
501 assert!(result.is_err());
502 if let Err(FraiseQLError::Validation { message, .. }) = result {
503 assert!(message.contains("must have at least one measure"));
504 }
505 }
506
507 #[test]
508 fn test_validate_fact_table_measure_missing_name() {
509 let validator = SchemaValidator::new();
510 let mut ir = AuthoringIR::new();
511
512 let metadata = json!({
513 "measures": [
514 {"sql_type": "Decimal"}
515 ],
516 "dimensions": {"name": "data"}
517 });
518
519 ir.fact_tables.insert("tf_sales".to_string(), metadata);
520
521 let result = validator.validate(ir);
522 assert!(result.is_err());
523 if let Err(FraiseQLError::Validation { message, .. }) = result {
524 assert!(message.contains("missing 'name' field"));
525 }
526 }
527
528 #[test]
529 fn test_validate_fact_table_measure_missing_sql_type() {
530 let validator = SchemaValidator::new();
531 let mut ir = AuthoringIR::new();
532
533 let metadata = json!({
534 "measures": [
535 {"name": "revenue"}
536 ],
537 "dimensions": {"name": "data"}
538 });
539
540 ir.fact_tables.insert("tf_sales".to_string(), metadata);
541
542 let result = validator.validate(ir);
543 assert!(result.is_err());
544 if let Err(FraiseQLError::Validation { message, .. }) = result {
545 assert!(message.contains("missing 'sql_type' field"));
546 }
547 }
548
549 #[test]
550 fn test_validate_fact_table_missing_dimensions() {
551 let validator = SchemaValidator::new();
552 let mut ir = AuthoringIR::new();
553
554 let metadata = json!({
555 "measures": [
556 {"name": "revenue", "sql_type": "Decimal"}
557 ]
558 });
559
560 ir.fact_tables.insert("tf_sales".to_string(), metadata);
561
562 let result = validator.validate(ir);
563 assert!(result.is_err());
564 if let Err(FraiseQLError::Validation { message, .. }) = result {
565 assert!(message.contains("missing 'dimensions' field"));
566 }
567 }
568
569 #[test]
570 fn test_validate_fact_table_dimensions_missing_name() {
571 let validator = SchemaValidator::new();
572 let mut ir = AuthoringIR::new();
573
574 let metadata = json!({
575 "measures": [
576 {"name": "revenue", "sql_type": "Decimal"}
577 ],
578 "dimensions": {
579 "paths": []
580 }
581 });
582
583 ir.fact_tables.insert("tf_sales".to_string(), metadata);
584
585 let result = validator.validate(ir);
586 assert!(result.is_err());
587 if let Err(FraiseQLError::Validation { message, .. }) = result {
588 assert!(message.contains("dimensions missing 'name' field"));
589 }
590 }
591
592 #[test]
593 fn test_validate_fact_table_invalid_filters() {
594 let validator = SchemaValidator::new();
595 let mut ir = AuthoringIR::new();
596
597 let metadata = json!({
598 "measures": [
599 {"name": "revenue", "sql_type": "Decimal"}
600 ],
601 "dimensions": {"name": "data"},
602 "denormalized_filters": "not an array"
603 });
604
605 ir.fact_tables.insert("tf_sales".to_string(), metadata);
606
607 let result = validator.validate(ir);
608 assert!(result.is_err());
609 if let Err(FraiseQLError::Validation { message, .. }) = result {
610 assert!(message.contains("denormalized_filters must be an array"));
611 }
612 }
613
614 #[test]
615 fn test_validate_aggregate_type_missing_count() {
616 let validator = SchemaValidator::new();
617 let mut ir = AuthoringIR::new();
618
619 ir.types.push(IRType {
620 name: "SalesAggregate".to_string(),
621 fields: vec![IRField {
622 name: "revenue_sum".to_string(),
623 field_type: "Float".to_string(),
624 nullable: true,
625 description: None,
626 sql_column: None,
627 }],
628 sql_source: None,
629 description: None,
630 });
631
632 let result = validator.validate(ir);
633 assert!(result.is_err());
634 if let Err(FraiseQLError::Validation { message, .. }) = result {
635 assert!(message.contains("must have a 'count' field"));
636 }
637 }
638
639 #[test]
640 fn test_validate_aggregate_type_with_count() {
641 let validator = SchemaValidator::new();
642 let mut ir = AuthoringIR::new();
643
644 ir.types.push(IRType {
645 name: "SalesAggregate".to_string(),
646 fields: vec![
647 IRField {
648 name: "count".to_string(),
649 field_type: "Int!".to_string(),
650 nullable: false,
651 description: None,
652 sql_column: None,
653 },
654 IRField {
655 name: "revenue_sum".to_string(),
656 field_type: "Float".to_string(),
657 nullable: true,
658 description: None,
659 sql_column: None,
660 },
661 ],
662 sql_source: None,
663 description: None,
664 });
665
666 let result = validator.validate(ir);
667 assert!(result.is_ok());
668 }
669
670 #[test]
671 fn test_validate_group_by_input_invalid_field_type() {
672 let validator = SchemaValidator::new();
673 let mut ir = AuthoringIR::new();
674
675 ir.types.push(IRType {
676 name: "SalesGroupByInput".to_string(),
677 fields: vec![IRField {
678 name: "category".to_string(),
679 field_type: "String".to_string(), nullable: true,
681 description: None,
682 sql_column: None,
683 }],
684 sql_source: None,
685 description: None,
686 });
687
688 let result = validator.validate(ir);
689 assert!(result.is_err());
690 if let Err(FraiseQLError::Validation { message, .. }) = result {
691 assert!(message.contains("must be Boolean"));
692 }
693 }
694
695 #[test]
696 fn test_validate_group_by_input_valid() {
697 let validator = SchemaValidator::new();
698 let mut ir = AuthoringIR::new();
699
700 ir.types.push(IRType {
701 name: "SalesGroupByInput".to_string(),
702 fields: vec![IRField {
703 name: "category".to_string(),
704 field_type: "Boolean".to_string(),
705 nullable: true,
706 description: None,
707 sql_column: None,
708 }],
709 sql_source: None,
710 description: None,
711 });
712
713 let result = validator.validate(ir);
714 assert!(result.is_ok());
715 }
716
717 #[test]
718 fn test_validate_having_input_invalid_suffix() {
719 let validator = SchemaValidator::new();
720 let mut ir = AuthoringIR::new();
721
722 ir.types.push(IRType {
723 name: "SalesHavingInput".to_string(),
724 fields: vec![IRField {
725 name: "count".to_string(), field_type: "Int".to_string(),
727 nullable: true,
728 description: None,
729 sql_column: None,
730 }],
731 sql_source: None,
732 description: None,
733 });
734
735 let result = validator.validate(ir);
736 assert!(result.is_err());
737 if let Err(FraiseQLError::Validation { message, .. }) = result {
738 assert!(message.contains("must have operator suffix"));
739 }
740 }
741
742 #[test]
743 fn test_validate_having_input_valid() {
744 let validator = SchemaValidator::new();
745 let mut ir = AuthoringIR::new();
746
747 ir.types.push(IRType {
748 name: "SalesHavingInput".to_string(),
749 fields: vec![
750 IRField {
751 name: "count_gt".to_string(),
752 field_type: "Int".to_string(),
753 nullable: true,
754 description: None,
755 sql_column: None,
756 },
757 IRField {
758 name: "revenue_sum_gte".to_string(),
759 field_type: "Float".to_string(),
760 nullable: true,
761 description: None,
762 sql_column: None,
763 },
764 ],
765 sql_source: None,
766 description: None,
767 });
768
769 let result = validator.validate(ir);
770 assert!(result.is_ok());
771 }
772
773 #[test]
778 fn test_extract_base_type() {
779 assert_eq!(extract_base_type("String"), "String");
780 assert_eq!(extract_base_type("String!"), "String");
781 assert_eq!(extract_base_type("[String]"), "String");
782 assert_eq!(extract_base_type("[String!]"), "String");
783 assert_eq!(extract_base_type("[String!]!"), "String");
784 assert_eq!(extract_base_type(" User "), "User");
785 }
786
787 #[test]
788 fn test_validate_type_with_valid_references() {
789 let validator = SchemaValidator::new();
790 let mut ir = AuthoringIR::new();
791
792 ir.types.push(IRType {
794 name: "User".to_string(),
795 fields: vec![
796 IRField {
797 name: "id".to_string(),
798 field_type: "ID!".to_string(),
799 nullable: false,
800 description: None,
801 sql_column: None,
802 },
803 IRField {
804 name: "name".to_string(),
805 field_type: "String!".to_string(),
806 nullable: false,
807 description: None,
808 sql_column: None,
809 },
810 ],
811 sql_source: Some("v_user".to_string()),
812 description: None,
813 });
814
815 ir.types.push(IRType {
817 name: "Post".to_string(),
818 fields: vec![
819 IRField {
820 name: "id".to_string(),
821 field_type: "ID!".to_string(),
822 nullable: false,
823 description: None,
824 sql_column: None,
825 },
826 IRField {
827 name: "author".to_string(),
828 field_type: "User".to_string(),
829 nullable: true,
830 description: None,
831 sql_column: None,
832 },
833 ],
834 sql_source: Some("v_post".to_string()),
835 description: None,
836 });
837
838 let result = validator.validate(ir);
839 assert!(result.is_ok());
840 }
841
842 #[test]
843 fn test_validate_type_with_invalid_reference() {
844 let validator = SchemaValidator::new();
845 let mut ir = AuthoringIR::new();
846
847 ir.types.push(IRType {
848 name: "Post".to_string(),
849 fields: vec![IRField {
850 name: "author".to_string(),
851 field_type: "NonExistentType".to_string(),
852 nullable: true,
853 description: None,
854 sql_column: None,
855 }],
856 sql_source: None,
857 description: None,
858 });
859
860 let result = validator.validate(ir);
861 assert!(result.is_err());
862 if let Err(FraiseQLError::Validation { message, .. }) = result {
863 assert!(message.contains("references unknown type"));
864 assert!(message.contains("NonExistentType"));
865 }
866 }
867
868 #[test]
869 fn test_validate_type_empty_name() {
870 let validator = SchemaValidator::new();
871 let mut ir = AuthoringIR::new();
872
873 ir.types.push(IRType {
874 name: String::new(),
875 fields: vec![],
876 sql_source: None,
877 description: None,
878 });
879
880 let result = validator.validate(ir);
881 assert!(result.is_err());
882 if let Err(FraiseQLError::Validation { message, .. }) = result {
883 assert!(message.contains("name cannot be empty"));
884 }
885 }
886
887 #[test]
888 fn test_validate_query_with_valid_return_type() {
889 use super::super::ir::{AutoParams, IRArgument, IRQuery};
890
891 let validator = SchemaValidator::new();
892 let mut ir = AuthoringIR::new();
893
894 ir.types.push(IRType {
896 name: "User".to_string(),
897 fields: vec![IRField {
898 name: "id".to_string(),
899 field_type: "ID!".to_string(),
900 nullable: false,
901 description: None,
902 sql_column: None,
903 }],
904 sql_source: Some("v_user".to_string()),
905 description: None,
906 });
907
908 ir.queries.push(IRQuery {
910 name: "user".to_string(),
911 return_type: "User".to_string(),
912 returns_list: false,
913 nullable: true,
914 arguments: vec![IRArgument {
915 name: "id".to_string(),
916 arg_type: "ID!".to_string(),
917 nullable: false,
918 default_value: None,
919 description: None,
920 }],
921 sql_source: Some("v_user".to_string()),
922 description: None,
923 auto_params: AutoParams::default(),
924 });
925
926 let result = validator.validate(ir);
927 assert!(result.is_ok());
928 }
929
930 #[test]
931 fn test_validate_query_with_invalid_return_type() {
932 use super::super::ir::{AutoParams, IRQuery};
933
934 let validator = SchemaValidator::new();
935 let mut ir = AuthoringIR::new();
936
937 ir.queries.push(IRQuery {
938 name: "unknownQuery".to_string(),
939 return_type: "NonExistentType".to_string(),
940 returns_list: false,
941 nullable: true,
942 arguments: vec![],
943 sql_source: None,
944 description: None,
945 auto_params: AutoParams::default(),
946 });
947
948 let result = validator.validate(ir);
949 assert!(result.is_err());
950 if let Err(FraiseQLError::Validation { message, .. }) = result {
951 assert!(message.contains("returns unknown type"));
952 assert!(message.contains("NonExistentType"));
953 }
954 }
955
956 #[test]
957 fn test_validate_query_with_scalar_return_type() {
958 use super::super::ir::{AutoParams, IRQuery};
959
960 let validator = SchemaValidator::new();
961 let mut ir = AuthoringIR::new();
962
963 ir.queries.push(IRQuery {
965 name: "serverTime".to_string(),
966 return_type: "DateTime".to_string(),
967 returns_list: false,
968 nullable: false,
969 arguments: vec![],
970 sql_source: None,
971 description: None,
972 auto_params: AutoParams::default(),
973 });
974
975 let result = validator.validate(ir);
976 assert!(result.is_ok());
977 }
978
979 #[test]
980 fn test_validate_query_empty_name() {
981 use super::super::ir::{AutoParams, IRQuery};
982
983 let validator = SchemaValidator::new();
984 let mut ir = AuthoringIR::new();
985
986 ir.queries.push(IRQuery {
987 name: String::new(),
988 return_type: "String".to_string(),
989 returns_list: false,
990 nullable: true,
991 arguments: vec![],
992 sql_source: None,
993 description: None,
994 auto_params: AutoParams::default(),
995 });
996
997 let result = validator.validate(ir);
998 assert!(result.is_err());
999 if let Err(FraiseQLError::Validation { message, .. }) = result {
1000 assert!(message.contains("Query name cannot be empty"));
1001 }
1002 }
1003
1004 #[test]
1005 fn test_validate_list_type_references() {
1006 let validator = SchemaValidator::new();
1007 let mut ir = AuthoringIR::new();
1008
1009 ir.types.push(IRType {
1011 name: "User".to_string(),
1012 fields: vec![
1013 IRField {
1014 name: "id".to_string(),
1015 field_type: "ID!".to_string(),
1016 nullable: false,
1017 description: None,
1018 sql_column: None,
1019 },
1020 IRField {
1021 name: "friends".to_string(),
1022 field_type: "[User!]".to_string(), nullable: true,
1024 description: None,
1025 sql_column: None,
1026 },
1027 ],
1028 sql_source: None,
1029 description: None,
1030 });
1031
1032 let result = validator.validate(ir);
1033 assert!(result.is_ok());
1034 }
1035
1036 #[test]
1037 fn test_validate_builtin_scalar_types() {
1038 let validator = SchemaValidator::new();
1039 let mut ir = AuthoringIR::new();
1040
1041 ir.types.push(IRType {
1043 name: "TestType".to_string(),
1044 fields: vec![
1045 IRField {
1046 name: "id".to_string(),
1047 field_type: "ID".to_string(),
1048 nullable: true,
1049 description: None,
1050 sql_column: None,
1051 },
1052 IRField {
1053 name: "name".to_string(),
1054 field_type: "String".to_string(),
1055 nullable: true,
1056 description: None,
1057 sql_column: None,
1058 },
1059 IRField {
1060 name: "age".to_string(),
1061 field_type: "Int".to_string(),
1062 nullable: true,
1063 description: None,
1064 sql_column: None,
1065 },
1066 IRField {
1067 name: "rating".to_string(),
1068 field_type: "Float".to_string(),
1069 nullable: true,
1070 description: None,
1071 sql_column: None,
1072 },
1073 IRField {
1074 name: "active".to_string(),
1075 field_type: "Boolean".to_string(),
1076 nullable: true,
1077 description: None,
1078 sql_column: None,
1079 },
1080 IRField {
1081 name: "created".to_string(),
1082 field_type: "DateTime".to_string(),
1083 nullable: true,
1084 description: None,
1085 sql_column: None,
1086 },
1087 IRField {
1088 name: "uid".to_string(),
1089 field_type: "UUID".to_string(),
1090 nullable: true,
1091 description: None,
1092 sql_column: None,
1093 },
1094 ],
1095 sql_source: None,
1096 description: None,
1097 });
1098
1099 let result = validator.validate(ir);
1100 assert!(result.is_ok(), "All builtin scalars should be recognized");
1101 }
1102
1103 #[test]
1104 fn test_validate_rich_scalar_types() {
1105 let validator = SchemaValidator::new();
1106 let mut ir = AuthoringIR::new();
1107
1108 ir.types.push(IRType {
1110 name: "Contact".to_string(),
1111 fields: vec![
1112 IRField {
1113 name: "email".to_string(),
1114 field_type: "Email".to_string(),
1115 nullable: true,
1116 description: None,
1117 sql_column: None,
1118 },
1119 IRField {
1120 name: "phone".to_string(),
1121 field_type: "PhoneNumber".to_string(),
1122 nullable: true,
1123 description: None,
1124 sql_column: None,
1125 },
1126 IRField {
1127 name: "url".to_string(),
1128 field_type: "URL".to_string(),
1129 nullable: true,
1130 description: None,
1131 sql_column: None,
1132 },
1133 IRField {
1134 name: "ip".to_string(),
1135 field_type: "IPAddress".to_string(),
1136 nullable: true,
1137 description: None,
1138 sql_column: None,
1139 },
1140 ],
1141 sql_source: None,
1142 description: None,
1143 });
1144
1145 let result = validator.validate(ir);
1146 assert!(result.is_ok(), "Rich scalars should be recognized");
1147 }
1148}