fraiseql_core/schema/compiled/
fact_table_links.rs1use super::CompiledSchema;
11use crate::{
12 compiler::fact_table::FactTableMetadata,
13 schema::{FieldDefinition, TypeDefinition},
14};
15
16pub fn fact_field<'a>(type_def: &'a TypeDefinition, name: &str) -> Option<&'a FieldDefinition> {
20 let key = crate::utils::to_snake_case(name);
21 type_def
22 .fields
23 .iter()
24 .find(|f| crate::utils::to_snake_case(f.name.as_str()) == key)
25}
26
27fn declared_names(metadata: &FactTableMetadata) -> Vec<&str> {
29 let mut names: Vec<&str> = metadata
30 .measures
31 .iter()
32 .map(|m| m.name.as_str())
33 .chain(metadata.denormalized_filters.iter().map(|f| f.name.as_str()))
34 .chain(metadata.dimensions.paths.iter().map(|p| p.name.as_str()))
35 .chain(metadata.calendar_dimensions.iter().map(|c| c.source_column.as_str()))
36 .chain(metadata.native_measures.keys().map(String::as_str))
37 .chain(metadata.native_dimension_mapping.keys().map(String::as_str))
38 .collect();
39 names.sort_unstable();
40 names.dedup();
41 names
42}
43
44impl CompiledSchema {
45 #[must_use]
51 pub fn fact_table_mapping_violations(&self) -> Vec<String> {
52 use crate::compiler::fact_table::dimension_key;
53
54 let mut tables: Vec<&FactTableMetadata> = self.fact_tables.values().collect();
55 tables.sort_by(|a, b| a.table_name.cmp(&b.table_name));
56 let mut out = Vec::new();
57 for table in tables {
58 let mut mapping: Vec<(&String, &String)> =
59 table.native_dimension_mapping.iter().collect();
60 mapping.sort();
61 for (i, (key, column)) in mapping.iter().enumerate() {
62 let dimension = dimension_key(key);
63 if let Some(filter) = table
64 .denormalized_filters
65 .iter()
66 .find(|f| dimension_key(&f.name) == dimension && f.name != **column)
67 {
68 out.push(format!(
69 "fact table `{}`: native_dimension_mapping maps `{key}` to column \
70 `{column}`, and denormalized_filters declares `{}` as a column of \
71 its own; declare the dimension once",
72 table.table_name, filter.name
73 ));
74 }
75 for (other, other_column) in &mapping[i + 1..] {
76 if dimension_key(other) == dimension && other_column != column {
77 out.push(format!(
78 "fact table `{}`: native_dimension_mapping maps `{key}` to \
79 `{column}` and `{other}` to `{other_column}`, which name one \
80 dimension; declare it once",
81 table.table_name
82 ));
83 }
84 }
85 }
86 }
87 out
88 }
89
90 #[must_use]
92 pub fn fact_table_type(&self, metadata: &FactTableMetadata) -> Option<&TypeDefinition> {
93 let name = metadata.type_name.as_deref()?;
94 self.types.iter().find(|t| t.name == name)
95 }
96
97 #[must_use]
101 pub fn fact_table_link_violations(&self) -> Vec<String> {
102 let mut tables: Vec<&FactTableMetadata> = self.fact_tables.values().collect();
103 tables.sort_by(|a, b| a.table_name.cmp(&b.table_name));
104 tables
105 .into_iter()
106 .filter_map(|metadata| {
107 let type_name = metadata.type_name.as_deref()?;
108 let Some(type_def) = self.fact_table_type(metadata) else {
109 return Some(format!(
110 "fact table '{}' is read as type '{type_name}', which the schema does \
111 not declare",
112 metadata.table_name
113 ));
114 };
115 let missing: Vec<&str> = declared_names(metadata)
116 .into_iter()
117 .filter(|name| fact_field(type_def, name).is_none())
118 .collect();
119 (!missing.is_empty()).then(|| {
120 format!(
121 "fact table '{}' is read as type '{type_name}', which has no field for \
122 {}: a column the type does not declare is one no read gate can reach. \
123 Declare the fields on '{type_name}', or drop them from the fact table.",
124 metadata.table_name,
125 missing.iter().map(|n| format!("'{n}'")).collect::<Vec<_>>().join(", ")
126 )
127 })
128 })
129 .collect()
130 }
131}