use std::fmt;
use super::{FactTableMetadata, SqlType};
use crate::compiler::aggregate_types::TemporalBucket;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GroupKey {
pub name: String,
pub source: GroupKeySource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum GroupKeySource {
Dimension(String),
Filter(String),
Bucket {
column: String,
bucket: TemporalBucket,
},
Depth(String),
Level(String),
}
impl GroupKeySource {
const fn is_generated(&self) -> bool {
matches!(self, Self::Bucket { .. } | Self::Depth(_) | Self::Level(_))
}
}
impl fmt::Display for GroupKeySource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Dimension(path) => write!(f, "the dimension `{path}`"),
Self::Filter(column) => write!(f, "the denormalized filter `{column}`"),
Self::Bucket { column, bucket } => {
write!(f, "the `{}` bucket of `{column}`", bucket.field_suffix())
},
Self::Depth(column) => write!(f, "the depth of `{column}`"),
Self::Level(column) => write!(f, "the level of `{column}`"),
}
}
}
impl FactTableMetadata {
#[must_use]
pub fn group_keys(&self) -> Vec<GroupKey> {
let mut keys: Vec<GroupKey> = self
.denormalized_filters
.iter()
.map(|f| GroupKey {
name: f.name.clone(),
source: GroupKeySource::Filter(f.name.clone()),
})
.collect();
for path in &self.dimensions.paths {
if !self.denormalized_filters.iter().any(|f| f.name == path.name) {
keys.push(GroupKey {
name: path.name.clone(),
source: GroupKeySource::Dimension(path.name.clone()),
});
}
}
let mut generated = Vec::new();
for filter in &self.denormalized_filters {
match filter.sql_type {
SqlType::Date | SqlType::Timestamp => {
for bucket in TemporalBucket::all() {
generated.push(bucket_key(&filter.name, *bucket));
}
},
SqlType::Ltree => {
generated.push(GroupKey {
name: format!("{}_depth", filter.name),
source: GroupKeySource::Depth(filter.name.clone()),
});
generated.push(GroupKey {
name: format!("{}_level", filter.name),
source: GroupKeySource::Level(filter.name.clone()),
});
},
_ => {},
}
}
for calendar in &self.calendar_dimensions {
for granularity in &calendar.granularities {
for bucket in &granularity.buckets {
generated.push(bucket_key(&calendar.source_column, bucket.bucket_type));
}
}
}
for key in generated {
if !keys.contains(&key) {
keys.push(key);
}
}
keys
}
#[must_use]
pub fn group_key_collisions(&self) -> Vec<String> {
let keys = self.group_keys();
let mut violations = Vec::new();
for (i, key) in keys.iter().enumerate() {
if !key.source.is_generated() {
continue;
}
for other in keys.iter().take(i).filter(|other| other.name == key.name) {
violations.push(format!(
"fact table `{}`: groupBy key `{}` is both {} and {}: rename the column \
and recompile",
self.table_name, key.name, key.source, other.source
));
}
}
violations
}
}
fn bucket_key(column: &str, bucket: TemporalBucket) -> GroupKey {
GroupKey {
name: format!("{column}_{}", bucket.field_suffix()),
source: GroupKeySource::Bucket {
column: column.to_string(),
bucket,
},
}
}