1use std::collections::HashMap;
24
25use crate::identity::fold_name;
26use crate::model::TabularDatabase;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub struct TableHandle(
34 pub usize,
36);
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub struct ColumnHandle {
42 pub table: usize,
44 pub column: usize,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub struct MeasureHandle {
52 pub table: usize,
54 pub measure: usize,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub struct HierarchyHandle {
62 pub table: usize,
64 pub hierarchy: usize,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
71pub struct ExpressionHandle(
72 pub usize,
74);
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79pub struct FunctionHandle(
80 pub usize,
82);
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum Resolved {
87 Column(ColumnHandle),
89 Measure(MeasureHandle),
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100pub struct UnqualifiedMatches {
101 pub measure: Option<MeasureHandle>,
104 pub column: Option<ColumnHandle>,
106}
107
108impl UnqualifiedMatches {
109 #[must_use]
115 pub fn primary(&self) -> Option<Resolved> {
116 match (self.measure, self.column) {
117 (Some(measure), _) => Some(Resolved::Measure(measure)),
118 (None, Some(column)) => Some(Resolved::Column(column)),
119 (None, None) => None,
120 }
121 }
122
123 #[must_use]
126 pub fn is_empty(&self) -> bool {
127 self.measure.is_none() && self.column.is_none()
128 }
129}
130
131#[derive(Debug, Clone, Default)]
137struct TableEntry {
138 table: usize,
140 columns: HashMap<String, ColumnHandle>,
142 hierarchies: HashMap<String, HierarchyHandle>,
144}
145
146#[derive(Debug, Clone)]
156pub struct ModelIndex {
157 tables: HashMap<String, TableEntry>,
159 measures: HashMap<String, MeasureHandle>,
162 expressions: HashMap<String, ExpressionHandle>,
164 functions: HashMap<String, FunctionHandle>,
167}
168
169impl ModelIndex {
170 #[must_use]
175 pub fn build(db: &TabularDatabase) -> Self {
176 let mut tables: HashMap<String, TableEntry> = HashMap::new();
177 let mut measures: HashMap<String, MeasureHandle> = HashMap::new();
178 let mut expressions: HashMap<String, ExpressionHandle> = HashMap::new();
179 let mut functions: HashMap<String, FunctionHandle> = HashMap::new();
180
181 for (table_idx, table) in db.tables.iter().enumerate() {
182 let entry = tables
184 .entry(fold_name(&table.name))
185 .or_insert_with(|| TableEntry {
186 table: table_idx,
187 ..TableEntry::default()
188 });
189
190 for (column_idx, column) in table.columns.iter().enumerate() {
191 entry
192 .columns
193 .entry(fold_name(&column.name))
194 .or_insert(ColumnHandle {
195 table: table_idx,
196 column: column_idx,
197 });
198 }
199
200 for (hierarchy_idx, hierarchy) in table.hierarchies.iter().enumerate() {
201 entry
202 .hierarchies
203 .entry(fold_name(&hierarchy.name))
204 .or_insert(HierarchyHandle {
205 table: table_idx,
206 hierarchy: hierarchy_idx,
207 });
208 }
209
210 for (measure_idx, measure) in table.measures.iter().enumerate() {
211 measures
212 .entry(fold_name(&measure.name))
213 .or_insert(MeasureHandle {
214 table: table_idx,
215 measure: measure_idx,
216 });
217 }
218 }
219
220 for (expression_idx, expression) in db.expressions.iter().enumerate() {
221 expressions
222 .entry(fold_name(&expression.name))
223 .or_insert(ExpressionHandle(expression_idx));
224 }
225
226 for (function_idx, function) in db.functions.iter().enumerate() {
227 functions
228 .entry(fold_name(&function.name))
229 .or_insert(FunctionHandle(function_idx));
230 }
231
232 Self {
233 tables,
234 measures,
235 expressions,
236 functions,
237 }
238 }
239
240 #[must_use]
242 pub fn resolve_table(&self, name: &str) -> Option<TableHandle> {
243 self.tables
244 .get(&fold_name(name))
245 .map(|entry| TableHandle(entry.table))
246 }
247
248 #[must_use]
283 pub fn resolve_qualified(&self, table: &str, name: &str) -> Option<Resolved> {
284 let folded_name = fold_name(name);
285 let column = self
286 .tables
287 .get(&fold_name(table))
288 .and_then(|entry| entry.columns.get(&folded_name));
289 if let Some(column) = column {
290 return Some(Resolved::Column(*column));
291 }
292 self.measures
293 .get(&folded_name)
294 .copied()
295 .map(Resolved::Measure)
296 }
297
298 #[must_use]
338 pub fn resolve_unqualified(&self, name: &str, home_table: Option<&str>) -> UnqualifiedMatches {
339 let folded_name = fold_name(name);
340 UnqualifiedMatches {
341 measure: self.measures.get(&folded_name).copied(),
342 column: home_table.and_then(|table| {
343 self.tables
344 .get(&fold_name(table))
345 .and_then(|entry| entry.columns.get(&folded_name))
346 .copied()
347 }),
348 }
349 }
350
351 #[must_use]
355 pub fn resolve_hierarchy(&self, table: &str, name: &str) -> Option<HierarchyHandle> {
356 self.tables
357 .get(&fold_name(table))?
358 .hierarchies
359 .get(&fold_name(name))
360 .copied()
361 }
362
363 #[must_use]
366 pub fn resolve_expression(&self, name: &str) -> Option<ExpressionHandle> {
367 self.expressions.get(&fold_name(name)).copied()
368 }
369
370 #[must_use]
373 pub fn resolve_function(&self, name: &str) -> Option<FunctionHandle> {
374 self.functions.get(&fold_name(name)).copied()
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use crate::model::{Column, Function, Hierarchy, Measure, SharedExpression, Table};
382 use rstest::rstest;
383
384 fn column(name: &str) -> Column {
385 Column {
386 name: name.to_string(),
387 ..Default::default()
388 }
389 }
390
391 fn measure(name: &str) -> Measure {
392 Measure {
393 name: name.to_string(),
394 expression: "0".to_string(),
395 ..Default::default()
396 }
397 }
398
399 fn model() -> TabularDatabase {
416 TabularDatabase {
417 name: Some("Contoso".to_string()),
418 tables: vec![
419 Table {
420 name: "Sales".to_string(),
421 columns: vec![column("Amount"), column("Beløb")],
422 measures: vec![measure("Total Sales"), measure("Antal")],
423 ..Default::default()
424 },
425 Table {
426 name: "Dato".to_string(),
427 columns: vec![column("Måned"), column("Antal")],
428 measures: vec![measure("Omsætning")],
429 hierarchies: vec![Hierarchy {
430 name: "Kalender".to_string(),
431 ..Default::default()
432 }],
433 ..Default::default()
434 },
435 Table {
436 name: "sales".to_string(),
437 columns: vec![column("Amount")],
438 ..Default::default()
439 },
440 ],
441 expressions: vec![
442 SharedExpression {
443 name: "Server".to_string(),
444 expression: "\"contoso.database.windows.net\"".to_string(),
445 },
446 SharedExpression {
447 name: "Database".to_string(),
448 expression: "\"AdventureWorks\"".to_string(),
449 },
450 ],
451 functions: vec![Function {
452 name: "Sales.NetPrice".to_string(),
453 expression: "(price: SCALAR) => price * 0.75".to_string(),
454 is_hidden: false,
455 }],
456 ..Default::default()
457 }
458 }
459
460 fn index() -> ModelIndex {
461 ModelIndex::build(&model())
462 }
463
464 fn column_handle(table: usize, column: usize) -> Resolved {
465 Resolved::Column(ColumnHandle { table, column })
466 }
467
468 fn measure_handle(table: usize, measure: usize) -> Resolved {
469 Resolved::Measure(MeasureHandle { table, measure })
470 }
471
472 mod resolve_table {
473 use super::*;
474
475 #[rstest]
476 #[case::upper("SALES", 0)]
477 #[case::as_written("Sales", 0)]
478 #[case::danish_lower("dato", 1)]
479 #[case::danish_upper("DATO", 1)]
480 fn finds_a_table_ignoring_case(#[case] name: &str, #[case] expected: usize) {
481 assert_eq!(index().resolve_table(name), Some(TableHandle(expected)));
482 }
483
484 #[rstest]
485 #[case::misspelled("Salez")]
486 #[case::empty("")]
487 fn returns_none_for_an_unknown_name(#[case] name: &str) {
488 assert_eq!(index().resolve_table(name), None);
489 }
490 }
491
492 mod resolve_qualified {
493 use super::*;
494
495 #[rstest]
496 #[case::lower_table_upper_column("sales", "AMOUNT", 0, 0)]
497 #[case::upper_table_danish_column("DATO", "måned", 1, 0)]
498 #[case::danish_column_upper("Dato", "MÅNED", 1, 0)]
499 fn finds_a_column_on_the_named_table(
500 #[case] table: &str,
501 #[case] name: &str,
502 #[case] expected_table: usize,
503 #[case] expected_column: usize,
504 ) {
505 assert_eq!(
506 index().resolve_qualified(table, name),
507 Some(column_handle(expected_table, expected_column))
508 );
509 }
510
511 #[rstest]
515 #[case::on_its_home_table("SALES", "total sales", 0, 0)]
516 #[case::danish_on_its_home_table("Dato", "OMSÆTNING", 1, 0)]
517 #[case::under_a_wrong_table_prefix("Dato", "Total Sales", 0, 0)]
518 #[case::under_a_nonexistent_table_prefix("Ukendt Tabel", "omsætning", 1, 0)]
519 fn finds_a_measure(
520 #[case] table: &str,
521 #[case] name: &str,
522 #[case] expected_table: usize,
523 #[case] expected_measure: usize,
524 ) {
525 assert_eq!(
526 index().resolve_qualified(table, name),
527 Some(measure_handle(expected_table, expected_measure))
528 );
529 }
530
531 #[rstest]
532 #[case::column_of_a_different_table("Dato", "Amount")]
535 #[case::unknown_name("Sales", "Nope")]
536 #[case::unknown_table_and_name("Ukendt Tabel", "Måned")]
537 fn returns_none_when_nothing_matches(#[case] table: &str, #[case] name: &str) {
538 assert_eq!(index().resolve_qualified(table, name), None);
539 }
540
541 #[test]
544 fn binds_to_the_named_tables_column_when_both_exist() {
545 assert_eq!(
546 index().resolve_qualified("Dato", "Antal"),
547 Some(column_handle(1, 1))
548 );
549 }
550
551 #[test]
552 fn binds_to_the_measure_when_the_named_table_has_no_such_column() {
553 assert_eq!(
554 index().resolve_qualified("Sales", "antal"),
555 Some(measure_handle(0, 1))
556 );
557 }
558 }
559
560 mod resolve_unqualified {
561 use super::*;
562
563 #[test]
566 fn finds_a_global_measure_without_a_home_table() {
567 assert_eq!(
568 index().resolve_unqualified("OMSÆTNING", None).measure,
569 Some(MeasureHandle {
570 table: 1,
571 measure: 0
572 })
573 );
574 }
575
576 #[test]
577 fn offers_no_column_candidate_without_a_home_table() {
578 assert_eq!(index().resolve_unqualified("OMSÆTNING", None).column, None);
579 }
580
581 #[test]
584 fn returns_the_measure_candidate_for_an_ambiguous_name() {
585 assert_eq!(
586 index().resolve_unqualified("antal", Some("Dato")).measure,
587 Some(MeasureHandle {
588 table: 0,
589 measure: 1
590 })
591 );
592 }
593
594 #[test]
595 fn returns_the_column_candidate_for_an_ambiguous_name() {
596 assert_eq!(
597 index().resolve_unqualified("antal", Some("Dato")).column,
598 Some(ColumnHandle {
599 table: 1,
600 column: 1
601 })
602 );
603 }
604
605 #[test]
606 fn drops_the_column_candidate_when_there_is_no_home_table() {
607 assert_eq!(index().resolve_unqualified("antal", None).column, None);
608 }
609
610 #[test]
611 fn finds_a_column_of_the_home_table() {
612 assert_eq!(
613 index().resolve_unqualified("BELØB", Some("sales")).column,
614 Some(ColumnHandle {
615 table: 0,
616 column: 1
617 })
618 );
619 }
620
621 #[test]
622 fn offers_no_measure_candidate_for_a_column_only_name() {
623 assert_eq!(
624 index().resolve_unqualified("BELØB", Some("sales")).measure,
625 None
626 );
627 }
628
629 #[test]
632 fn matches_nothing_against_the_wrong_home_table() {
633 let found = index().resolve_unqualified("Beløb", Some("Dato"));
634
635 assert!(found.is_empty(), "expected no candidates, got {found:?}");
636 }
637
638 #[rstest]
639 #[case::known_home_table(Some("Sales"))]
640 #[case::unknown_home_table(Some("Ukendt Tabel"))]
641 #[case::no_home_table(None)]
642 fn is_empty_for_an_unknown_name(#[case] home_table: Option<&str>) {
643 let found = index().resolve_unqualified("Ukendt", home_table);
644
645 assert!(found.is_empty(), "expected no candidates, got {found:?}");
646 }
647
648 #[test]
649 fn primary_prefers_the_measure_when_a_name_is_ambiguous() {
650 assert_eq!(
651 index().resolve_unqualified("antal", Some("Dato")).primary(),
652 Some(measure_handle(0, 1))
653 );
654 }
655
656 #[test]
657 fn primary_returns_the_column_when_there_is_no_measure() {
658 assert_eq!(
659 index()
660 .resolve_unqualified("BELØB", Some("sales"))
661 .primary(),
662 Some(column_handle(0, 1))
663 );
664 }
665
666 #[test]
667 fn primary_is_none_for_an_unknown_name() {
668 assert_eq!(
669 index()
670 .resolve_unqualified("Ukendt", Some("Sales"))
671 .primary(),
672 None
673 );
674 }
675 }
676
677 mod resolve_hierarchy {
678 use super::*;
679
680 #[rstest]
681 #[case::lower_table_upper_name("dato", "KALENDER")]
682 #[case::upper_table_as_written("DATO", "Kalender")]
683 fn finds_a_hierarchy_ignoring_case(#[case] table: &str, #[case] name: &str) {
684 assert_eq!(
685 index().resolve_hierarchy(table, name),
686 Some(HierarchyHandle {
687 table: 1,
688 hierarchy: 0
689 })
690 );
691 }
692
693 #[rstest]
694 #[case::another_table("Sales", "Kalender")]
697 #[case::a_column_name("Dato", "Måned")]
698 #[case::unknown_table("Ukendt Tabel", "Kalender")]
699 fn returns_none(#[case] table: &str, #[case] name: &str) {
700 assert_eq!(index().resolve_hierarchy(table, name), None);
701 }
702 }
703
704 mod resolve_expression {
705 use super::*;
706
707 #[rstest]
708 #[case::upper("SERVER", 0)]
709 #[case::as_written("Server", 0)]
710 #[case::lower("database", 1)]
711 fn finds_an_expression_ignoring_case(#[case] name: &str, #[case] expected: usize) {
712 assert_eq!(
713 index().resolve_expression(name),
714 Some(ExpressionHandle(expected))
715 );
716 }
717
718 #[test]
719 fn returns_none_for_an_unknown_name() {
720 assert_eq!(index().resolve_expression("Ukendt"), None);
721 }
722 }
723
724 mod resolve_function {
725 use super::*;
726
727 #[rstest]
728 #[case::as_written("Sales.NetPrice")]
729 #[case::upper("SALES.NETPRICE")]
730 #[case::lower("sales.netprice")]
731 fn finds_a_function_ignoring_case(#[case] name: &str) {
732 assert_eq!(index().resolve_function(name), Some(FunctionHandle(0)));
733 }
734
735 #[rstest]
736 #[case::a_shared_expression_name("Server")]
738 #[case::a_measure_name("Total Sales")]
739 #[case::unknown("Ukendt")]
740 fn returns_none(#[case] name: &str) {
741 assert_eq!(index().resolve_function(name), None);
742 }
743 }
744
745 mod build_with_duplicate_names {
748 use super::*;
749
750 #[rstest]
751 #[case::as_written("Sales")]
752 #[case::as_the_duplicate_is_spelled("sales")]
753 fn resolves_the_table_to_the_first_occurrence(#[case] name: &str) {
754 assert_eq!(index().resolve_table(name), Some(TableHandle(0)));
755 }
756
757 #[test]
758 fn resolves_a_shared_column_name_to_the_first_tables_column() {
759 assert_eq!(
760 index().resolve_qualified("Sales", "Amount"),
761 Some(column_handle(0, 0))
762 );
763 }
764
765 #[test]
766 fn resolves_a_shared_column_name_unqualified_to_the_first_tables_column() {
767 assert_eq!(
768 index().resolve_unqualified("Amount", Some("SALES")).column,
769 Some(ColumnHandle {
770 table: 0,
771 column: 0
772 })
773 );
774 }
775 }
776
777 mod accessors {
778 use super::*;
779
780 #[test]
781 fn a_resolved_table_handle_reaches_its_table() {
782 let db = model();
783 let handle = ModelIndex::build(&db).resolve_table("SALES").unwrap();
784
785 assert_eq!(db.table(handle).unwrap().name, "Sales");
786 }
787
788 #[test]
789 fn a_resolved_column_handle_reaches_its_column() {
790 let db = model();
791 let Some(Resolved::Column(handle)) =
792 ModelIndex::build(&db).resolve_qualified("sales", "BELØB")
793 else {
794 panic!("expected 'sales'[BELØB] to resolve to a column");
795 };
796
797 assert_eq!(db.column(handle).unwrap().name, "Beløb");
798 }
799
800 #[test]
801 fn a_resolved_measure_handle_reaches_its_measure() {
802 let db = model();
803 let handle = ModelIndex::build(&db)
804 .resolve_unqualified("omsætning", None)
805 .measure
806 .unwrap();
807
808 assert_eq!(db.measure(handle).unwrap().name, "Omsætning");
809 }
810
811 #[test]
812 fn a_resolved_hierarchy_handle_reaches_its_hierarchy() {
813 let db = model();
814 let handle = ModelIndex::build(&db)
815 .resolve_hierarchy("DATO", "kalender")
816 .unwrap();
817
818 assert_eq!(db.hierarchy(handle).unwrap().name, "Kalender");
819 }
820
821 #[test]
822 fn a_resolved_expression_handle_reaches_its_expression() {
823 let db = model();
824 let handle = ModelIndex::build(&db)
825 .resolve_expression("DATABASE")
826 .unwrap();
827
828 assert_eq!(db.shared_expression(handle).unwrap().name, "Database");
829 }
830
831 #[test]
832 fn a_stale_table_handle_is_none() {
833 assert!(model().table(TableHandle(9)).is_none());
834 }
835
836 #[test]
837 fn a_column_handle_with_an_out_of_range_table_is_none() {
838 let handle = ColumnHandle {
839 table: 9,
840 column: 9,
841 };
842
843 assert!(model().column(handle).is_none());
844 }
845
846 #[test]
847 fn a_column_handle_with_an_out_of_range_column_is_none() {
848 let handle = ColumnHandle {
849 table: 0,
850 column: 9,
851 };
852
853 assert!(model().column(handle).is_none());
854 }
855
856 #[test]
857 fn a_measure_handle_with_an_out_of_range_table_is_none() {
858 let handle = MeasureHandle {
859 table: 9,
860 measure: 0,
861 };
862
863 assert!(model().measure(handle).is_none());
864 }
865
866 #[test]
867 fn a_measure_handle_into_a_table_without_measures_is_none() {
868 let handle = MeasureHandle {
869 table: 2,
870 measure: 0,
871 };
872
873 assert!(model().measure(handle).is_none());
874 }
875
876 #[test]
877 fn a_hierarchy_handle_into_a_table_without_hierarchies_is_none() {
878 let handle = HierarchyHandle {
879 table: 0,
880 hierarchy: 0,
881 };
882
883 assert!(model().hierarchy(handle).is_none());
884 }
885
886 #[test]
887 fn a_stale_expression_handle_is_none() {
888 assert!(model().shared_expression(ExpressionHandle(9)).is_none());
889 }
890
891 #[test]
892 fn a_resolved_function_handle_reaches_its_function() {
893 let db = model();
894 let handle = ModelIndex::build(&db)
895 .resolve_function("SALES.NETPRICE")
896 .unwrap();
897
898 assert_eq!(db.function(handle).unwrap().name, "Sales.NetPrice");
899 }
900
901 #[test]
902 fn a_stale_function_handle_is_none() {
903 assert!(model().function(FunctionHandle(9)).is_none());
904 }
905
906 #[test]
907 fn an_object_id_for_a_stale_column_handle_is_none() {
908 assert!(model().object_id(column_handle(9, 9)).is_none());
909 }
910
911 #[test]
912 fn an_object_id_for_a_stale_measure_handle_is_none() {
913 assert!(model().object_id(measure_handle(9, 9)).is_none());
914 }
915 }
916
917 mod object_id {
920 use super::*;
921
922 #[test]
923 fn carries_the_models_casing_for_a_measure() {
924 let db = model();
925 let resolved = ModelIndex::build(&db)
926 .resolve_qualified("SALES", "TOTAL SALES")
927 .unwrap();
928
929 assert_eq!(
930 db.object_id(resolved).unwrap().to_string(),
931 "'Sales'[Total Sales]"
932 );
933 }
934
935 #[test]
936 fn carries_the_models_casing_for_a_danish_column() {
937 let db = model();
938 let resolved = ModelIndex::build(&db)
939 .resolve_qualified("DATO", "MÅNED")
940 .unwrap();
941
942 assert_eq!(db.object_id(resolved).unwrap().to_string(), "'Dato'[Måned]");
943 }
944
945 #[test]
946 fn distinguishes_a_column_from_a_measure_of_the_same_name() {
947 let db = model();
948
949 assert_ne!(
950 db.object_id(column_handle(1, 1)).unwrap(),
951 db.object_id(measure_handle(0, 1)).unwrap()
952 );
953 }
954
955 #[rstest]
956 #[case::the_dato_column(column_handle(1, 1), "'Dato'[Antal]")]
957 #[case::the_sales_measure(measure_handle(0, 1), "'Sales'[Antal]")]
958 fn renders_each_antal_under_its_own_table(
959 #[case] resolved: Resolved,
960 #[case] expected: &str,
961 ) {
962 assert_eq!(model().object_id(resolved).unwrap().to_string(), expected);
963 }
964 }
965}