use serde::{Deserialize, Serialize};
pub use crate::types::{OrderByClause, OrderDirection};
use crate::{
compiler::{
aggregate_types::{AggregateFunction, HavingOperator, TemporalBucket},
fact_table::FactTableMetadata,
},
db::where_clause::WhereClause,
error::{FraiseQLError, Result},
};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AggregationRequest {
pub table_name: String,
pub where_clause: Option<WhereClause>,
pub group_by: Vec<GroupBySelection>,
pub aggregates: Vec<AggregateSelection>,
pub having: Vec<HavingCondition>,
pub order_by: Vec<OrderByClause>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum GroupBySelection {
Dimension {
path: String,
alias: String,
},
TemporalBucket {
column: String,
bucket: TemporalBucket,
alias: String,
},
CalendarDimension {
source_column: String,
calendar_column: String,
json_key: String,
bucket: TemporalBucket,
alias: String,
},
NativeDimension {
column: String,
pg_cast: String,
},
}
impl GroupBySelection {
#[must_use]
pub fn alias(&self) -> &str {
match self {
Self::Dimension { alias, .. }
| Self::TemporalBucket { alias, .. }
| Self::CalendarDimension { alias, .. } => alias,
Self::NativeDimension { column, .. } => column,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum AggregateSelection {
Count {
alias: String,
},
CountDistinct {
field: String,
alias: String,
},
MeasureAggregate {
measure: String,
function: AggregateFunction,
alias: String,
},
BoolAggregate {
field: String,
function: crate::compiler::aggregate_types::BoolAggregateFunction,
alias: String,
},
}
impl AggregateSelection {
#[must_use]
pub fn alias(&self) -> &str {
match self {
Self::Count { alias }
| Self::CountDistinct { alias, .. }
| Self::MeasureAggregate { alias, .. }
| Self::BoolAggregate { alias, .. } => alias,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HavingCondition {
pub aggregate: AggregateSelection,
pub operator: HavingOperator,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AggregationPlan {
pub metadata: FactTableMetadata,
pub request: AggregationRequest,
pub group_by_expressions: Vec<GroupByExpression>,
pub aggregate_expressions: Vec<AggregateExpression>,
pub having_conditions: Vec<ValidatedHavingCondition>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum GroupByExpression {
JsonbPath {
jsonb_column: String,
path: String,
alias: String,
},
TemporalBucket {
column: String,
bucket: TemporalBucket,
alias: String,
},
CalendarPath {
calendar_column: String,
json_key: String,
alias: String,
},
NativeColumn {
column: String,
pg_cast: String,
alias: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum AggregateExpression {
Count {
alias: String,
},
CountDistinct {
column: String,
alias: String,
},
MeasureAggregate {
column: String,
function: AggregateFunction,
alias: String,
#[serde(default)]
native: bool,
},
AdvancedAggregate {
column: String,
function: AggregateFunction,
alias: String,
delimiter: Option<String>,
order_by: Option<Vec<OrderByClause>>,
},
BoolAggregate {
column: String,
function: crate::compiler::aggregate_types::BoolAggregateFunction,
alias: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidatedHavingCondition {
pub aggregate: AggregateExpression,
pub operator: HavingOperator,
pub value: serde_json::Value,
}
impl AggregationPlan {
#[must_use]
pub fn native_aliases(&self) -> std::collections::HashSet<&str> {
self.group_by_expressions
.iter()
.filter_map(|e| {
if let GroupByExpression::NativeColumn { alias, .. } = e {
Some(alias.as_str())
} else {
None
}
})
.collect()
}
}
pub struct AggregationPlanner;
impl AggregationPlanner {
pub fn plan(
request: AggregationRequest,
metadata: FactTableMetadata,
) -> Result<AggregationPlan> {
let group_by_expressions = Self::validate_group_by(&request.group_by, &metadata)?;
let aggregate_expressions = Self::validate_aggregates(&request.aggregates, &metadata)?;
let having_conditions = Self::validate_having(&request.having, &aggregate_expressions)?;
Ok(AggregationPlan {
metadata,
request,
group_by_expressions,
aggregate_expressions,
having_conditions,
})
}
fn validate_group_by(
selections: &[GroupBySelection],
metadata: &FactTableMetadata,
) -> Result<Vec<GroupByExpression>> {
let mut expressions = Vec::new();
for selection in selections {
match selection {
GroupBySelection::Dimension { path, alias } => {
let known_paths = &metadata.dimensions.paths;
if !known_paths.is_empty() && !known_paths.iter().any(|p| p.name == *path) {
return Err(FraiseQLError::Validation {
message: format!(
"Dimension '{}' not found in fact table '{}'",
path, metadata.table_name
),
path: None,
});
}
expressions.push(GroupByExpression::JsonbPath {
jsonb_column: metadata.dimensions.name.clone(),
path: path.clone(),
alias: alias.clone(),
});
},
GroupBySelection::TemporalBucket {
column,
bucket,
alias,
} => {
let filter_exists =
metadata.denormalized_filters.iter().any(|f| f.name == *column);
if !filter_exists {
return Err(FraiseQLError::Validation {
message: format!(
"Column '{}' not found in fact table '{}'",
column, metadata.table_name
),
path: None,
});
}
expressions.push(GroupByExpression::TemporalBucket {
column: column.clone(),
bucket: *bucket,
alias: alias.clone(),
});
},
GroupBySelection::CalendarDimension {
calendar_column,
json_key,
alias,
..
} => {
expressions.push(GroupByExpression::CalendarPath {
calendar_column: calendar_column.clone(),
json_key: json_key.clone(),
alias: alias.clone(),
});
},
GroupBySelection::NativeDimension { column, pg_cast } => {
expressions.push(GroupByExpression::NativeColumn {
alias: column.clone(),
column: column.clone(),
pg_cast: pg_cast.clone(),
});
},
}
}
Ok(expressions)
}
fn validate_aggregates(
selections: &[AggregateSelection],
metadata: &FactTableMetadata,
) -> Result<Vec<AggregateExpression>> {
let mut expressions = Vec::new();
for selection in selections {
match selection {
AggregateSelection::Count { alias } => {
expressions.push(AggregateExpression::Count {
alias: alias.clone(),
});
},
AggregateSelection::CountDistinct { field, alias } => {
let measure_exists = metadata.measures.iter().any(|m| m.name == *field);
if !measure_exists {
return Err(FraiseQLError::Validation {
message: format!(
"Measure '{}' not found in fact table '{}'",
field, metadata.table_name
),
path: None,
});
}
expressions.push(AggregateExpression::CountDistinct {
column: field.clone(),
alias: alias.clone(),
});
},
AggregateSelection::MeasureAggregate {
measure,
function,
alias,
} => {
let measure_exists = metadata.measures.iter().any(|m| m.name == *measure);
let is_dimension = metadata.dimensions.paths.iter().any(|p| p.name == *measure);
let is_filter =
metadata.denormalized_filters.iter().any(|f| f.name == *measure);
let is_native_measure = metadata.native_measures.contains_key(measure.as_str());
if !measure_exists && !is_dimension && !is_filter && !is_native_measure {
return Err(FraiseQLError::Validation {
message: format!(
"Measure or field '{}' not found in fact table '{}'",
measure, metadata.table_name
),
path: None,
});
}
if matches!(
function,
AggregateFunction::ArrayAgg
| AggregateFunction::JsonAgg
| AggregateFunction::JsonbAgg
| AggregateFunction::StringAgg
) {
expressions.push(AggregateExpression::AdvancedAggregate {
column: measure.clone(),
function: *function,
alias: alias.clone(),
delimiter: if *function == AggregateFunction::StringAgg {
Some(", ".to_string())
} else {
None
},
order_by: None,
});
} else {
let (resolved_column, is_native) = if let Some(native_col) =
metadata.native_measures.get(measure.as_str())
{
(native_col.clone(), true)
} else {
(measure.clone(), false)
};
expressions.push(AggregateExpression::MeasureAggregate {
column: resolved_column,
function: *function,
alias: alias.clone(),
native: is_native,
});
}
},
AggregateSelection::BoolAggregate {
field,
function,
alias,
} => {
let field_exists = metadata.dimensions.paths.iter().any(|p| p.name == *field)
|| metadata.denormalized_filters.iter().any(|f| f.name == *field);
if !field_exists {
return Err(FraiseQLError::Validation {
message: format!(
"Boolean field '{}' not found in fact table '{}'",
field, metadata.table_name
),
path: None,
});
}
expressions.push(AggregateExpression::BoolAggregate {
column: field.clone(),
function: *function,
alias: alias.clone(),
});
},
}
}
Ok(expressions)
}
fn validate_having(
conditions: &[HavingCondition],
_aggregate_expressions: &[AggregateExpression],
) -> Result<Vec<ValidatedHavingCondition>> {
let mut validated = Vec::new();
for condition in conditions {
let aggregate_expr = match &condition.aggregate {
AggregateSelection::Count { alias } => AggregateExpression::Count {
alias: alias.clone(),
},
AggregateSelection::CountDistinct { field, alias } => {
AggregateExpression::CountDistinct {
column: field.clone(),
alias: alias.clone(),
}
},
AggregateSelection::MeasureAggregate {
measure,
function,
alias,
} => {
if matches!(
function,
AggregateFunction::ArrayAgg
| AggregateFunction::JsonAgg
| AggregateFunction::JsonbAgg
| AggregateFunction::StringAgg
) {
AggregateExpression::AdvancedAggregate {
column: measure.clone(),
function: *function,
alias: alias.clone(),
delimiter: if *function == AggregateFunction::StringAgg {
Some(", ".to_string())
} else {
None
},
order_by: None,
}
} else {
AggregateExpression::MeasureAggregate {
column: measure.clone(),
function: *function,
alias: alias.clone(),
native: false,
}
}
},
AggregateSelection::BoolAggregate {
field,
function,
alias,
} => AggregateExpression::BoolAggregate {
column: field.clone(),
function: *function,
alias: alias.clone(),
},
};
validated.push(ValidatedHavingCondition {
aggregate: aggregate_expr,
operator: condition.operator,
value: condition.value.clone(),
});
}
Ok(validated)
}
}