use super::CompiledSchema;
use crate::{
compiler::fact_table::FactTableMetadata,
schema::{FieldDefinition, TypeDefinition},
};
pub fn fact_field<'a>(type_def: &'a TypeDefinition, name: &str) -> Option<&'a FieldDefinition> {
let key = crate::utils::to_snake_case(name);
type_def
.fields
.iter()
.find(|f| crate::utils::to_snake_case(f.name.as_str()) == key)
}
fn declared_names(metadata: &FactTableMetadata) -> Vec<&str> {
let mut names: Vec<&str> = metadata
.measures
.iter()
.map(|m| m.name.as_str())
.chain(metadata.denormalized_filters.iter().map(|f| f.name.as_str()))
.chain(metadata.dimensions.paths.iter().map(|p| p.name.as_str()))
.chain(metadata.calendar_dimensions.iter().map(|c| c.source_column.as_str()))
.chain(metadata.native_measures.keys().map(String::as_str))
.chain(metadata.native_dimension_mapping.keys().map(String::as_str))
.collect();
names.sort_unstable();
names.dedup();
names
}
impl CompiledSchema {
#[must_use]
pub fn fact_table_mapping_violations(&self) -> Vec<String> {
use crate::compiler::fact_table::dimension_key;
let mut tables: Vec<&FactTableMetadata> = self.fact_tables.values().collect();
tables.sort_by(|a, b| a.table_name.cmp(&b.table_name));
let mut out = Vec::new();
for table in tables {
let mut mapping: Vec<(&String, &String)> =
table.native_dimension_mapping.iter().collect();
mapping.sort();
for (i, (key, column)) in mapping.iter().enumerate() {
let dimension = dimension_key(key);
if let Some(filter) = table
.denormalized_filters
.iter()
.find(|f| dimension_key(&f.name) == dimension && f.name != **column)
{
out.push(format!(
"fact table `{}`: native_dimension_mapping maps `{key}` to column \
`{column}`, and denormalized_filters declares `{}` as a column of \
its own; declare the dimension once",
table.table_name, filter.name
));
}
for (other, other_column) in &mapping[i + 1..] {
if dimension_key(other) == dimension && other_column != column {
out.push(format!(
"fact table `{}`: native_dimension_mapping maps `{key}` to \
`{column}` and `{other}` to `{other_column}`, which name one \
dimension; declare it once",
table.table_name
));
}
}
}
}
out
}
#[must_use]
pub fn fact_table_type(&self, metadata: &FactTableMetadata) -> Option<&TypeDefinition> {
let name = metadata.type_name.as_deref()?;
self.types.iter().find(|t| t.name == name)
}
#[must_use]
pub fn fact_table_link_violations(&self) -> Vec<String> {
let mut tables: Vec<&FactTableMetadata> = self.fact_tables.values().collect();
tables.sort_by(|a, b| a.table_name.cmp(&b.table_name));
tables
.into_iter()
.filter_map(|metadata| {
let type_name = metadata.type_name.as_deref()?;
let Some(type_def) = self.fact_table_type(metadata) else {
return Some(format!(
"fact table '{}' is read as type '{type_name}', which the schema does \
not declare",
metadata.table_name
));
};
let missing: Vec<&str> = declared_names(metadata)
.into_iter()
.filter(|name| fact_field(type_def, name).is_none())
.collect();
(!missing.is_empty()).then(|| {
format!(
"fact table '{}' is read as type '{type_name}', which has no field for \
{}: a column the type does not declare is one no read gate can reach. \
Declare the fields on '{type_name}', or drop them from the fact table.",
metadata.table_name,
missing.iter().map(|n| format!("'{n}'")).collect::<Vec<_>>().join(", ")
)
})
})
.collect()
}
}