1use serde::Deserialize;
2
3#[derive(Debug, Deserialize, Default)]
6pub struct SchemaFile {
7 #[serde(default)]
8 pub sources: Vec<SourceDefinition>,
9
10 #[serde(default)]
11 pub models: Vec<ModelDefinition>,
12
13 #[serde(default)]
14 pub snapshots: Vec<SnapshotDefinition>,
15
16 #[serde(default)]
17 pub exposures: Vec<ExposureDefinition>,
18
19 #[serde(default)]
20 pub semantic_models: Vec<SemanticModelDefinition>,
21
22 #[serde(default)]
23 pub metrics: Vec<MetricDefinition>,
24
25 #[serde(default)]
26 pub saved_queries: Vec<SavedQueryDefinition>,
27}
28
29#[derive(Debug, Deserialize, Clone)]
30pub struct SourceDefinition {
31 pub name: String,
32 #[serde(default)]
33 pub description: Option<String>,
34 #[serde(default)]
35 pub tables: Vec<SourceTable>,
36}
37
38#[derive(Debug, Deserialize, Clone)]
39pub struct SourceTable {
40 pub name: String,
41 #[serde(default)]
42 pub description: Option<String>,
43 #[serde(default)]
44 pub columns: Vec<ColumnDefinition>,
45}
46
47#[derive(Debug, Deserialize, Clone)]
48pub struct ColumnDefinition {
49 pub name: String,
50 #[serde(default)]
51 pub description: Option<String>,
52 #[serde(default, alias = "data_tests")]
53 pub tests: Vec<TestDefinition>,
54}
55
56#[derive(Debug, Deserialize, Clone)]
61#[serde(untagged)]
62pub enum TestDefinition {
63 Simple(String),
64 Complex(serde_json::Value),
65}
66
67impl TestDefinition {
68 pub fn test_name(&self) -> Option<&str> {
74 match self {
75 TestDefinition::Simple(s) => Some(s.as_str()),
76 TestDefinition::Complex(v) => {
77 let obj = v.as_object()?;
78 if let Some(tn) = obj.get("test_name").and_then(|v| v.as_str()) {
80 return Some(tn);
81 }
82 for key in obj.keys() {
86 if !matches!(key.as_str(), "config" | "arguments" | "name") {
87 return Some(key.as_str());
88 }
89 }
90 None
91 }
92 }
93 }
94}
95
96fn version_value_to_str(v: &serde_json::Value) -> String {
103 if let Some(n) = v.as_i64() {
104 return n.to_string();
105 }
106 if let Some(n) = v.as_u64() {
107 return n.to_string();
108 }
109 if let Some(f) = v.as_f64() {
110 return if f.fract() == 0.0 {
115 (f as i64).to_string()
116 } else {
117 f.to_string()
118 };
119 }
120 if let Some(s) = v.as_str() {
121 if let Ok(n) = s.parse::<i64>() {
122 return n.to_string();
123 }
124 return s.to_string();
125 }
126 v.to_string()
127}
128
129#[derive(Debug, Deserialize, Clone)]
131pub struct VersionSpec {
132 pub v: serde_json::Value,
133 #[serde(default)]
135 pub defined_in: Option<String>,
136}
137
138impl VersionSpec {
139 pub fn v_str(&self) -> String {
141 version_value_to_str(&self.v)
142 }
143
144 pub fn sql_stem(&self, model_name: &str) -> String {
147 self.defined_in
148 .clone()
149 .unwrap_or_else(|| format!("{}_v{}", model_name, self.v_str()))
150 }
151}
152
153#[derive(Debug, Deserialize, Clone)]
154pub struct ModelDefinition {
155 pub name: String,
156 #[serde(default)]
157 pub description: Option<String>,
158 #[serde(default)]
159 pub columns: Vec<ColumnDefinition>,
160 #[serde(default)]
161 pub config: Option<ModelConfig>,
162 #[serde(default)]
163 pub tags: Vec<String>,
164 #[serde(default, alias = "data_tests")]
166 pub tests: Vec<TestDefinition>,
167 #[serde(default)]
169 pub versions: Vec<VersionSpec>,
170 #[serde(default)]
172 pub latest_version: Option<serde_json::Value>,
173}
174
175impl ModelDefinition {
176 pub fn resolved_latest_version_str(&self) -> Option<String> {
182 if let Some(lv) = &self.latest_version {
183 return Some(version_value_to_str(lv));
184 }
185 if self.versions.is_empty() {
186 return None;
187 }
188 let strs: Vec<String> = self.versions.iter().map(|v| v.v_str()).collect();
189 let numerics: Vec<i64> = strs.iter().filter_map(|s| s.parse().ok()).collect();
190 if numerics.len() == strs.len() {
191 numerics.into_iter().max().map(|n| n.to_string())
196 } else {
197 strs.into_iter().max()
198 }
199 }
200}
201
202#[derive(Debug, Deserialize, Clone, Default)]
203pub struct ModelConfig {
204 #[serde(default)]
205 pub materialized: Option<String>,
206 #[serde(default)]
207 pub tags: Vec<String>,
208}
209
210#[derive(Debug, Deserialize, Clone)]
213pub struct SnapshotDefinition {
214 pub name: String,
215 #[serde(default)]
216 pub description: Option<String>,
217 #[serde(default)]
219 pub relation: Option<String>,
220}
221
222#[derive(Debug, Deserialize, Clone)]
223pub struct ExposureDefinition {
224 pub name: String,
225 #[serde(default)]
226 pub description: Option<String>,
227 #[serde(default)]
228 pub label: Option<String>,
229 #[serde(rename = "type", default)]
230 pub exposure_type: Option<String>,
231 #[serde(default)]
232 pub url: Option<String>,
233 #[serde(default)]
234 pub maturity: Option<String>,
235 #[serde(default)]
236 pub depends_on: Vec<String>,
237 #[serde(default)]
238 pub owner: Option<ExposureOwner>,
239}
240
241#[derive(Debug, Deserialize, Clone)]
242pub struct ExposureOwner {
243 pub name: Option<String>,
244 pub email: Option<String>,
245}
246
247#[derive(Debug, Deserialize, Clone)]
249pub struct SemanticModelDefinition {
250 pub name: String,
251 #[serde(default)]
252 pub description: Option<String>,
253 #[serde(default)]
254 pub label: Option<String>,
255 #[serde(default)]
257 pub model: Option<String>,
258 #[serde(default)]
260 pub measures: Vec<MeasureDefinition>,
261}
262
263#[derive(Debug, Deserialize, Clone)]
264pub struct MeasureDefinition {
265 pub name: String,
266}
267
268#[derive(Debug, Deserialize, Clone)]
270pub struct MetricDefinition {
271 pub name: String,
272 #[serde(default)]
273 pub description: Option<String>,
274 #[serde(default)]
275 pub label: Option<String>,
276 #[serde(default)]
279 pub type_params: Option<serde_json::Value>,
280 #[serde(default)]
282 pub base_metric: Option<serde_json::Value>,
283 #[serde(default)]
285 pub conversion_metric: Option<serde_json::Value>,
286 #[serde(default)]
288 pub input_metric: Option<serde_json::Value>,
289}
290
291impl MetricDefinition {
292 fn name_ref(value: &serde_json::Value) -> Option<&str> {
293 value
294 .as_str()
295 .or_else(|| value.get("name").and_then(|n| n.as_str()))
296 }
297
298 pub fn measure_refs(&self) -> Vec<&str> {
304 let Some(p) = &self.type_params else {
305 return vec![];
306 };
307 let mut refs = vec![];
308 for field in &["measure", "base_measure", "conversion_measure"] {
309 if let Some(v) = p.get(field)
310 && let Some(name) = Self::name_ref(v)
311 {
312 refs.push(name);
313 }
314 }
315 refs
316 }
317
318 pub fn metric_refs(&self) -> Vec<&str> {
320 let mut refs = vec![];
321 if let Some(p) = &self.type_params {
323 for field in &["numerator", "denominator"] {
324 if let Some(v) = p.get(field)
325 && let Some(name) = Self::name_ref(v)
326 {
327 refs.push(name);
328 }
329 }
330 for field in &["input_metrics", "metrics"] {
332 if let Some(arr) = p.get(field).and_then(|v| v.as_array()) {
333 for item in arr {
334 if let Some(name) = Self::name_ref(item) {
335 refs.push(name);
336 }
337 }
338 }
339 }
340 }
341 for v in [&self.base_metric, &self.conversion_metric]
343 .into_iter()
344 .flatten()
345 {
346 if let Some(name) = Self::name_ref(v) {
347 refs.push(name);
348 }
349 }
350 if let Some(v) = &self.input_metric
352 && let Some(name) = Self::name_ref(v)
353 {
354 refs.push(name);
355 }
356 refs
357 }
358}
359
360#[derive(Debug, Deserialize, Clone)]
362pub struct SavedQueryDefinition {
363 pub name: String,
364 #[serde(default)]
365 pub description: Option<String>,
366 #[serde(default)]
367 pub label: Option<String>,
368 #[serde(default)]
369 pub query_params: Option<SavedQueryQueryParams>,
370}
371
372#[derive(Debug, Deserialize, Clone)]
373pub struct SavedQueryQueryParams {
374 #[serde(default)]
375 pub metrics: Vec<String>,
376}
377
378pub fn parse_schema_file(
380 content: &str,
381 path: Option<&std::path::Path>,
382) -> anyhow::Result<SchemaFile> {
383 let location = path
384 .map(|p| p.display().to_string())
385 .unwrap_or_else(|| "<input>".to_string());
386 super::yaml_from_str(content, &location)
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn test_parse_sources() {
395 let yaml = r#"
396sources:
397 - name: raw
398 description: Raw data from the warehouse
399 tables:
400 - name: orders
401 description: Raw orders table
402 - name: customers
403"#;
404 let schema = parse_schema_file(yaml, None).unwrap();
405 assert_eq!(schema.sources.len(), 1);
406 assert_eq!(schema.sources[0].name, "raw");
407 assert_eq!(schema.sources[0].tables.len(), 2);
408 assert_eq!(schema.sources[0].tables[0].name, "orders");
409 }
410
411 #[test]
412 fn test_parse_models_with_data_tests() {
413 let yaml = r#"
414models:
415 - name: stg_orders
416 description: Staged orders
417 columns:
418 - name: order_id
419 data_tests:
420 - not_null
421 - unique
422"#;
423 let schema = parse_schema_file(yaml, None).unwrap();
424 assert_eq!(schema.models.len(), 1);
425 assert_eq!(schema.models[0].name, "stg_orders");
426 assert_eq!(schema.models[0].columns.len(), 1);
427 assert_eq!(schema.models[0].columns[0].tests.len(), 2);
428 }
429
430 #[test]
431 fn test_parse_models_with_legacy_tests_key() {
432 let yaml = r#"
433models:
434 - name: stg_orders
435 columns:
436 - name: order_id
437 tests:
438 - not_null
439 - unique
440"#;
441 let schema = parse_schema_file(yaml, None).unwrap();
442 assert_eq!(schema.models[0].columns[0].tests.len(), 2);
443 }
444
445 #[test]
446 fn test_parse_data_tests_all_formats() {
447 let yaml = r#"
448models:
449 - name: orders
450 columns:
451 - name: order_id
452 data_tests:
453 - not_null
454 - unique:
455 config:
456 where: "order_id > 21"
457 - name: status
458 data_tests:
459 - accepted_values:
460 arguments:
461 values:
462 - placed
463 - shipped
464 - completed
465 - returned
466 config:
467 severity: warn
468 - name: customer_id
469 data_tests:
470 - relationships:
471 arguments:
472 to: ref('customers')
473 field: id
474 - name: custom_test_name
475 test_name: accepted_values
476 arguments:
477 values:
478 - 1
479 - 2
480 - 3
481 config:
482 where: "order_date = current_date"
483"#;
484 let schema = parse_schema_file(yaml, None).unwrap();
485 let model = &schema.models[0];
486 assert_eq!(model.columns.len(), 3);
487
488 assert_eq!(model.columns[0].tests.len(), 2);
490 assert!(
491 matches!(model.columns[0].tests[0], TestDefinition::Simple(ref s) if s == "not_null")
492 );
493 assert!(matches!(
494 model.columns[0].tests[1],
495 TestDefinition::Complex(_)
496 ));
497
498 assert_eq!(model.columns[1].tests.len(), 1);
500 assert!(matches!(
501 model.columns[1].tests[0],
502 TestDefinition::Complex(_)
503 ));
504
505 assert_eq!(model.columns[2].tests.len(), 2);
507 assert!(matches!(
508 model.columns[2].tests[0],
509 TestDefinition::Complex(_)
510 ));
511 assert!(matches!(
512 model.columns[2].tests[1],
513 TestDefinition::Complex(_)
514 ));
515 }
516
517 #[test]
518 fn test_parse_exposures() {
519 let yaml = r#"
520exposures:
521 - name: weekly_report
522 description: Weekly business report
523 type: dashboard
524 depends_on:
525 - ref('orders')
526 - ref('customers')
527 owner:
528 name: Data Team
529 email: data@example.com
530"#;
531 let schema = parse_schema_file(yaml, None).unwrap();
532 assert_eq!(schema.exposures.len(), 1);
533 assert_eq!(schema.exposures[0].name, "weekly_report");
534 assert_eq!(schema.exposures[0].depends_on.len(), 2);
535 }
536
537 #[test]
538 fn test_parse_duplicate_mapping_keys() {
539 let yaml = r#"
542sources:
543 - name: raw
544 tables:
545 - name: orders
546sources:
547 - name: other
548 tables:
549 - name: users
550"#;
551 let schema = parse_schema_file(yaml, None).unwrap();
552 assert_eq!(schema.sources.len(), 1);
554 assert_eq!(schema.sources[0].name, "other");
555 }
556
557 #[test]
558 fn test_empty_file() {
559 let yaml = "";
560 let schema = parse_schema_file(yaml, None).unwrap();
561 assert!(schema.sources.is_empty());
562 assert!(schema.models.is_empty());
563 assert!(schema.snapshots.is_empty());
564 assert!(schema.exposures.is_empty());
565 }
566
567 #[test]
568 fn test_parse_yaml_only_snapshots() {
569 let yaml = r#"
570snapshots:
571 - name: snap_orders
572 description: Orders snapshot
573 relation: ref('stg_orders')
574 - name: snap_customers
575 relation: ref('stg_customers', version=2)
576 - name: snap_no_relation
577 description: Snapshot without upstream relation
578"#;
579 let schema = parse_schema_file(yaml, None).unwrap();
580 assert_eq!(schema.snapshots.len(), 3);
581 assert_eq!(schema.snapshots[0].name, "snap_orders");
582 assert_eq!(
583 schema.snapshots[0].description.as_deref(),
584 Some("Orders snapshot")
585 );
586 assert_eq!(
587 schema.snapshots[0].relation.as_deref(),
588 Some("ref('stg_orders')")
589 );
590 assert_eq!(schema.snapshots[1].name, "snap_customers");
591 assert_eq!(
592 schema.snapshots[1].relation.as_deref(),
593 Some("ref('stg_customers', version=2)")
594 );
595 assert!(schema.snapshots[2].relation.is_none());
596 }
597
598 #[test]
599 fn test_parse_versioned_model() {
600 let yaml = r#"
601models:
602 - name: my_model
603 description: A versioned model
604 latest_version: 2
605 versions:
606 - v: 1
607 - v: 2
608 defined_in: my_model_custom
609"#;
610 let schema = parse_schema_file(yaml, None).unwrap();
611 assert_eq!(schema.models.len(), 1);
612 let m = &schema.models[0];
613 assert_eq!(m.name, "my_model");
614 assert_eq!(m.versions.len(), 2);
615 assert_eq!(m.versions[0].v_str(), "1");
616 assert_eq!(m.versions[0].sql_stem("my_model"), "my_model_v1");
617 assert_eq!(m.versions[1].v_str(), "2");
618 assert_eq!(m.versions[1].sql_stem("my_model"), "my_model_custom");
619 assert_eq!(m.resolved_latest_version_str().as_deref(), Some("2"));
620 }
621
622 #[test]
623 fn test_versioned_model_infers_latest_from_max_v() {
624 let yaml = r#"
625models:
626 - name: orders
627 versions:
628 - v: 1
629 - v: 3
630 - v: 2
631"#;
632 let schema = parse_schema_file(yaml, None).unwrap();
633 let m = &schema.models[0];
634 assert_eq!(m.resolved_latest_version_str().as_deref(), Some("3"));
635 }
636
637 #[test]
638 fn test_versioned_model_infers_latest_from_quoted_v() {
639 let yaml = r#"
640models:
641 - name: orders
642 versions:
643 - v: "1"
644 - v: "3"
645 - v: "2"
646"#;
647 let schema = parse_schema_file(yaml, None).unwrap();
648 let m = &schema.models[0];
649 assert_eq!(m.resolved_latest_version_str().as_deref(), Some("3"));
650 }
651
652 #[test]
653 fn test_v_str_normalizes_quoted_numeric() {
654 let quoted = VersionSpec {
657 v: serde_json::Value::String("2".to_string()),
658 defined_in: None,
659 };
660 assert_eq!(quoted.v_str(), "2");
661
662 let quoted_large = VersionSpec {
664 v: serde_json::Value::String("10".to_string()),
665 defined_in: None,
666 };
667 assert_eq!(quoted_large.v_str(), "10");
668
669 let non_numeric = VersionSpec {
671 v: serde_json::Value::String("alpha".to_string()),
672 defined_in: None,
673 };
674 assert_eq!(non_numeric.v_str(), "alpha");
675
676 let large_int = VersionSpec {
679 v: serde_json::Value::String("9007199254740993".to_string()),
680 defined_in: None,
681 };
682 assert_eq!(large_int.v_str(), "9007199254740993");
683
684 let u64_num = VersionSpec {
686 v: serde_json::Value::Number(serde_json::Number::from(i64::MAX as u64 + 1)),
687 defined_in: None,
688 };
689 assert_eq!(u64_num.v_str(), (i64::MAX as u64 + 1).to_string());
690 }
691
692 #[test]
693 fn test_unversioned_model_has_empty_versions() {
694 let yaml = r#"
695models:
696 - name: plain_model
697 description: Not versioned
698"#;
699 let schema = parse_schema_file(yaml, None).unwrap();
700 let m = &schema.models[0];
701 assert!(m.versions.is_empty());
702 assert!(m.latest_version.is_none());
703 assert!(m.resolved_latest_version_str().is_none());
704 }
705
706 #[test]
707 fn test_test_name_extraction() {
708 let simple = TestDefinition::Simple("not_null".to_string());
710 assert_eq!(simple.test_name(), Some("not_null"));
711
712 let complex_single = TestDefinition::Complex(serde_json::json!({
714 "unique": {"config": {"where": "id > 0"}}
715 }));
716 assert_eq!(complex_single.test_name(), Some("unique"));
717
718 let complex_named = TestDefinition::Complex(serde_json::json!({
720 "name": "custom_test_name",
721 "test_name": "accepted_values",
722 "arguments": {"values": [1, 2]}
723 }));
724 assert_eq!(complex_named.test_name(), Some("accepted_values"));
725
726 let relationships = TestDefinition::Complex(serde_json::json!({
728 "relationships": {"arguments": {"to": "ref('customers')", "field": "id"}}
729 }));
730 assert_eq!(relationships.test_name(), Some("relationships"));
731
732 let name_only = TestDefinition::Complex(serde_json::json!({"name": "something"}));
734 assert_eq!(name_only.test_name(), None);
735 }
736
737 #[test]
738 fn test_parse_semantic_models() {
739 let yaml = r#"
740semantic_models:
741 - name: orders
742 description: Order semantic model
743 model: ref('orders')
744 measures:
745 - name: order_total
746 - name: order_count
747 dimensions:
748 - name: ordered_at
749 type: time
750"#;
751 let schema = parse_schema_file(yaml, None).unwrap();
752 assert_eq!(schema.semantic_models.len(), 1);
753 let sm = &schema.semantic_models[0];
754 assert_eq!(sm.name, "orders");
755 assert_eq!(sm.description.as_deref(), Some("Order semantic model"));
756 assert_eq!(sm.model.as_deref(), Some("ref('orders')"));
757 assert_eq!(sm.measures.len(), 2);
758 assert_eq!(sm.measures[0].name, "order_total");
759 assert_eq!(sm.measures[1].name, "order_count");
760 }
761
762 #[test]
763 fn test_parse_metrics_simple() {
764 let yaml = r#"
765metrics:
766 - name: order_total
767 label: Order Total
768 description: Sum of orders
769 type: simple
770 type_params:
771 measure: order_total
772"#;
773 let schema = parse_schema_file(yaml, None).unwrap();
774 assert_eq!(schema.metrics.len(), 1);
775 let m = &schema.metrics[0];
776 assert_eq!(m.name, "order_total");
777 assert_eq!(m.label.as_deref(), Some("Order Total"));
778 assert_eq!(m.measure_refs(), vec!["order_total"]);
779 assert!(m.metric_refs().is_empty());
780 }
781
782 #[test]
783 fn test_parse_metrics_simple_with_object_measure() {
784 let yaml = r#"
785metrics:
786 - name: order_total
787 type: simple
788 type_params:
789 measure:
790 name: order_total
791 fill_nulls_with: 0
792"#;
793 let schema = parse_schema_file(yaml, None).unwrap();
794 let m = &schema.metrics[0];
795 assert_eq!(m.measure_refs(), vec!["order_total"]);
796 assert!(m.metric_refs().is_empty());
797 }
798
799 #[test]
800 fn test_parse_metrics_ratio() {
801 let yaml = r#"
802metrics:
803 - name: revenue_per_order
804 type: ratio
805 type_params:
806 numerator: revenue
807 denominator: orders
808"#;
809 let schema = parse_schema_file(yaml, None).unwrap();
810 let m = &schema.metrics[0];
811 assert!(m.measure_refs().is_empty());
812 let refs = m.metric_refs();
813 assert!(refs.contains(&"revenue"));
814 assert!(refs.contains(&"orders"));
815 }
816
817 #[test]
818 fn test_parse_metrics_derived_with_input_metrics_and_metrics() {
819 let yaml = r#"
820metrics:
821 - name: pct_change
822 type: derived
823 type_params:
824 input_metrics:
825 - name: revenue
826 - orders
827 metrics:
828 - name: margin
829 - customer_count
830"#;
831 let schema = parse_schema_file(yaml, None).unwrap();
832 let m = &schema.metrics[0];
833 assert!(m.measure_refs().is_empty());
834 let refs = m.metric_refs();
835 assert!(refs.contains(&"revenue"));
836 assert!(refs.contains(&"orders"));
837 assert!(refs.contains(&"margin"));
838 assert!(refs.contains(&"customer_count"));
839 }
840
841 #[test]
842 fn test_parse_metrics_conversion_measure() {
843 let yaml = r#"
844metrics:
845 - name: visitors_who_bought
846 type: conversion
847 type_params:
848 base_measure:
849 name: visitors
850 conversion_measure:
851 name: buyers
852 entity: user
853"#;
854 let schema = parse_schema_file(yaml, None).unwrap();
855 let m = &schema.metrics[0];
856 let refs = m.measure_refs();
857 assert!(
858 refs.contains(&"visitors"),
859 "base_measure should be included"
860 );
861 assert!(
862 refs.contains(&"buyers"),
863 "conversion_measure should be included"
864 );
865 assert!(m.metric_refs().is_empty());
866 }
867
868 #[test]
869 fn test_parse_metrics_conversion_metric() {
870 let yaml = r#"
871metrics:
872 - name: visit_to_purchase
873 type: conversion
874 base_metric: visits
875 conversion_metric:
876 name: purchases
877 filter: "{{ Dimension('user__country') }} = 'US'"
878 entity: user
879 window: 7 days
880"#;
881 let schema = parse_schema_file(yaml, None).unwrap();
882 let m = &schema.metrics[0];
883 assert!(m.measure_refs().is_empty());
884 let refs = m.metric_refs();
885 assert!(
886 refs.contains(&"visits"),
887 "base_metric (string) should be in metric_refs"
888 );
889 assert!(
890 refs.contains(&"purchases"),
891 "conversion_metric (object) should be in metric_refs"
892 );
893 }
894
895 #[test]
896 fn test_parse_metrics_cumulative_input_metric() {
897 let yaml = r#"
898metrics:
899 - name: cumulative_revenue
900 type: cumulative
901 input_metric: revenue
902 window: 1 month
903"#;
904 let schema = parse_schema_file(yaml, None).unwrap();
905 let m = &schema.metrics[0];
906 assert!(m.measure_refs().is_empty());
907 let refs = m.metric_refs();
908 assert!(
909 refs.contains(&"revenue"),
910 "input_metric should be in metric_refs"
911 );
912 }
913
914 #[test]
915 fn test_parse_saved_queries() {
916 let yaml = r#"
917saved_queries:
918 - name: order_metrics
919 description: Key order metrics
920 query_params:
921 metrics:
922 - orders
923 - order_total
924 - food_orders
925"#;
926 let schema = parse_schema_file(yaml, None).unwrap();
927 assert_eq!(schema.saved_queries.len(), 1);
928 let sq = &schema.saved_queries[0];
929 assert_eq!(sq.name, "order_metrics");
930 assert_eq!(sq.description.as_deref(), Some("Key order metrics"));
931 let metrics = sq.query_params.as_ref().unwrap().metrics.as_slice();
932 assert_eq!(metrics, &["orders", "order_total", "food_orders"]);
933 }
934
935 #[test]
936 fn test_parse_full_semantic_layer_yaml() {
937 let yaml = r#"
939models:
940 - name: orders
941 description: Orders table
942
943semantic_models:
944 - name: orders
945 model: ref('orders')
946 measures:
947 - name: order_count
948 - name: order_total
949
950metrics:
951 - name: orders
952 type: simple
953 type_params:
954 measure: order_count
955 - name: order_total
956 type: simple
957 type_params:
958 measure: order_total
959
960saved_queries:
961 - name: order_kpis
962 query_params:
963 metrics:
964 - orders
965 - order_total
966"#;
967 let schema = parse_schema_file(yaml, None).unwrap();
968 assert_eq!(schema.models.len(), 1);
969 assert_eq!(schema.semantic_models.len(), 1);
970 assert_eq!(schema.metrics.len(), 2);
971 assert_eq!(schema.saved_queries.len(), 1);
972 assert_eq!(
973 schema.saved_queries[0]
974 .query_params
975 .as_ref()
976 .unwrap()
977 .metrics
978 .len(),
979 2
980 );
981 }
982}