use super::{
FactTableMetadata, FraiseQLError, OrderByClause, PartitionByColumn, Result, SelectColumn,
WindowExecutionPlan, WindowFunction, WindowFunctionRequest, WindowFunctionSpec,
WindowFunctionType, WindowOrderBy, WindowRequest, WindowSelectColumn,
};
pub struct WindowPlanner;
impl WindowPlanner {
pub fn plan(
request: WindowRequest,
metadata: &FactTableMetadata,
) -> Result<WindowExecutionPlan> {
let select = Self::convert_select_columns(&request.select, metadata)?;
let windows = Self::convert_window_functions(&request.windows, metadata)?;
let order_by = Self::convert_order_by(&request.order_by, metadata)?;
Ok(WindowExecutionPlan {
table: request.table_name,
select,
windows,
where_clause: request.where_clause,
order_by,
limit: request.limit,
offset: request.offset,
})
}
fn convert_select_columns(
columns: &[WindowSelectColumn],
metadata: &FactTableMetadata,
) -> Result<Vec<SelectColumn>> {
columns
.iter()
.map(|col| Self::convert_single_select_column(col, metadata))
.collect()
}
fn convert_single_select_column(
column: &WindowSelectColumn,
metadata: &FactTableMetadata,
) -> Result<SelectColumn> {
match column {
WindowSelectColumn::Measure { name, alias } => {
if !metadata.measures.iter().any(|m| m.name == *name) {
return Err(FraiseQLError::Validation {
message: format!(
"Measure '{}' not found in fact table '{}'",
name, metadata.table_name
),
path: None,
});
}
Ok(SelectColumn {
expression: name.clone(),
alias: alias.clone(),
})
},
WindowSelectColumn::Dimension { path, alias } => {
let expression = format!("{}->>'{}'", metadata.dimensions.name, path);
Ok(SelectColumn {
expression,
alias: alias.clone(),
})
},
WindowSelectColumn::Filter { name, alias } => {
if !metadata.denormalized_filters.iter().any(|f| f.name == *name) {
return Err(FraiseQLError::Validation {
message: format!(
"Filter column '{}' not found in fact table '{}'",
name, metadata.table_name
),
path: None,
});
}
Ok(SelectColumn {
expression: name.clone(),
alias: alias.clone(),
})
},
}
}
fn convert_window_functions(
windows: &[WindowFunctionRequest],
metadata: &FactTableMetadata,
) -> Result<Vec<WindowFunction>> {
windows
.iter()
.map(|w| Self::convert_single_window_function(w, metadata))
.collect()
}
fn convert_single_window_function(
request: &WindowFunctionRequest,
metadata: &FactTableMetadata,
) -> Result<WindowFunction> {
let function = Self::convert_function_spec(&request.function, metadata)?;
let partition_by = request
.partition_by
.iter()
.map(|p| Self::convert_partition_by(p, metadata))
.collect::<Result<Vec<_>>>()?;
let order_by = request
.order_by
.iter()
.map(|o| Self::convert_window_order_by(o, metadata))
.collect::<Result<Vec<_>>>()?;
Ok(WindowFunction {
function,
alias: request.alias.clone(),
partition_by,
order_by,
frame: request.frame.clone(),
})
}
fn convert_function_spec(
spec: &WindowFunctionSpec,
metadata: &FactTableMetadata,
) -> Result<WindowFunctionType> {
match spec {
WindowFunctionSpec::RowNumber => Ok(WindowFunctionType::RowNumber),
WindowFunctionSpec::Rank => Ok(WindowFunctionType::Rank),
WindowFunctionSpec::DenseRank => Ok(WindowFunctionType::DenseRank),
WindowFunctionSpec::Ntile { n } => Ok(WindowFunctionType::Ntile { n: *n }),
WindowFunctionSpec::PercentRank => Ok(WindowFunctionType::PercentRank),
WindowFunctionSpec::CumeDist => Ok(WindowFunctionType::CumeDist),
WindowFunctionSpec::Lag {
field,
offset,
default,
} => {
let sql_field = Self::resolve_field_to_sql(field, metadata)?;
Ok(WindowFunctionType::Lag {
field: sql_field,
offset: *offset,
default: default.clone(),
})
},
WindowFunctionSpec::Lead {
field,
offset,
default,
} => {
let sql_field = Self::resolve_field_to_sql(field, metadata)?;
Ok(WindowFunctionType::Lead {
field: sql_field,
offset: *offset,
default: default.clone(),
})
},
WindowFunctionSpec::FirstValue { field } => {
let sql_field = Self::resolve_field_to_sql(field, metadata)?;
Ok(WindowFunctionType::FirstValue { field: sql_field })
},
WindowFunctionSpec::LastValue { field } => {
let sql_field = Self::resolve_field_to_sql(field, metadata)?;
Ok(WindowFunctionType::LastValue { field: sql_field })
},
WindowFunctionSpec::NthValue { field, n } => {
let sql_field = Self::resolve_field_to_sql(field, metadata)?;
Ok(WindowFunctionType::NthValue {
field: sql_field,
n: *n,
})
},
WindowFunctionSpec::RunningSum { measure } => {
Self::validate_measure(measure, metadata)?;
Ok(WindowFunctionType::Sum {
field: measure.clone(),
})
},
WindowFunctionSpec::RunningAvg { measure } => {
Self::validate_measure(measure, metadata)?;
Ok(WindowFunctionType::Avg {
field: measure.clone(),
})
},
WindowFunctionSpec::RunningCount => Ok(WindowFunctionType::Count { field: None }),
WindowFunctionSpec::RunningCountField { field } => {
let sql_field = Self::resolve_field_to_sql(field, metadata)?;
Ok(WindowFunctionType::Count {
field: Some(sql_field),
})
},
WindowFunctionSpec::RunningMin { measure } => {
Self::validate_measure(measure, metadata)?;
Ok(WindowFunctionType::Min {
field: measure.clone(),
})
},
WindowFunctionSpec::RunningMax { measure } => {
Self::validate_measure(measure, metadata)?;
Ok(WindowFunctionType::Max {
field: measure.clone(),
})
},
WindowFunctionSpec::RunningStddev { measure } => {
Self::validate_measure(measure, metadata)?;
Ok(WindowFunctionType::Stddev {
field: measure.clone(),
})
},
WindowFunctionSpec::RunningVariance { measure } => {
Self::validate_measure(measure, metadata)?;
Ok(WindowFunctionType::Variance {
field: measure.clone(),
})
},
}
}
fn convert_partition_by(
partition: &PartitionByColumn,
metadata: &FactTableMetadata,
) -> Result<String> {
match partition {
PartitionByColumn::Dimension { path } => {
Ok(format!("{}->>'{}'", metadata.dimensions.name, path))
},
PartitionByColumn::Filter { name } => {
if !metadata.denormalized_filters.iter().any(|f| f.name == *name) {
return Err(FraiseQLError::Validation {
message: format!(
"Filter column '{}' not found in fact table '{}'",
name, metadata.table_name
),
path: None,
});
}
Ok(name.clone())
},
PartitionByColumn::Measure { name } => {
Self::validate_measure(name, metadata)?;
Ok(name.clone())
},
}
}
fn convert_window_order_by(
order: &WindowOrderBy,
metadata: &FactTableMetadata,
) -> Result<OrderByClause> {
let field = Self::resolve_field_to_sql(&order.field, metadata)?;
Ok(OrderByClause::new(field, order.direction))
}
fn convert_order_by(
orders: &[WindowOrderBy],
metadata: &FactTableMetadata,
) -> Result<Vec<OrderByClause>> {
orders.iter().map(|o| Self::convert_window_order_by(o, metadata)).collect()
}
fn resolve_field_to_sql(field: &str, metadata: &FactTableMetadata) -> Result<String> {
if metadata.measures.iter().any(|m| m.name == field) {
return Ok(field.to_string());
}
if metadata.denormalized_filters.iter().any(|f| f.name == field) {
return Ok(field.to_string());
}
Self::validate_field_identifier(field)?;
Ok(format!("{}->>'{}'", metadata.dimensions.name, field))
}
fn validate_field_identifier(field: &str) -> Result<()> {
let mut chars = field.chars();
let first_ok = chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
let rest_ok = chars.all(|c| c.is_ascii_alphanumeric() || c == '_');
if first_ok && rest_ok {
Ok(())
} else {
Err(crate::error::FraiseQLError::Validation {
message: format!(
"window field '{field}' contains invalid characters; \
only [_A-Za-z][_0-9A-Za-z]* is allowed"
),
path: None,
})
}
}
fn validate_measure(measure: &str, metadata: &FactTableMetadata) -> Result<()> {
if !metadata.measures.iter().any(|m| m.name == *measure) {
return Err(FraiseQLError::Validation {
message: format!(
"Measure '{}' not found in fact table '{}'",
measure, metadata.table_name
),
path: None,
});
}
Ok(())
}
}