use sqlparser::ast;
use crate::aggregate_walk::extract_aggregates;
use crate::error::{Result, SqlError};
use crate::functions::registry::{FunctionCategory, FunctionRegistry};
use crate::parser::normalize::normalize_ident;
use crate::planner::agg_naming::aggregate_output_key;
use crate::resolver::columns::TableScope;
use crate::types::AggregateExpr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindName {
Canonical,
Output,
}
pub fn bind_aggregate_calls(
expr: &ast::Expr,
projection: &[ast::SelectItem],
aggregates: &mut Vec<AggregateExpr>,
functions: &FunctionRegistry,
name: BindName,
scope: &TableScope,
) -> Result<ast::Expr> {
let resolved = resolve_select_aliases(expr, projection);
bind(&resolved, aggregates, functions, name, scope)
}
fn resolve_select_aliases(expr: &ast::Expr, projection: &[ast::SelectItem]) -> ast::Expr {
match expr {
ast::Expr::Identifier(ident) => {
let needle = normalize_ident(ident);
projection
.iter()
.find_map(|item| match item {
ast::SelectItem::ExprWithAlias { expr, alias }
if normalize_ident(alias) == needle =>
{
Some(expr.clone())
}
_ => None,
})
.unwrap_or_else(|| expr.clone())
}
ast::Expr::BinaryOp { left, op, right } => ast::Expr::BinaryOp {
left: Box::new(resolve_select_aliases(left, projection)),
op: op.clone(),
right: Box::new(resolve_select_aliases(right, projection)),
},
ast::Expr::UnaryOp { op, expr } => ast::Expr::UnaryOp {
op: *op,
expr: Box::new(resolve_select_aliases(expr, projection)),
},
ast::Expr::Nested(inner) => {
ast::Expr::Nested(Box::new(resolve_select_aliases(inner, projection)))
}
other => other.clone(),
}
}
fn bind(
expr: &ast::Expr,
aggregates: &mut Vec<AggregateExpr>,
functions: &FunctionRegistry,
name: BindName,
scope: &TableScope,
) -> Result<ast::Expr> {
match expr {
ast::Expr::Function(func) if is_aggregate_call(func, functions) => {
let column = register_aggregate(expr, aggregates, functions, name, scope)?;
Ok(ast::Expr::Identifier(ast::Ident::new(column)))
}
ast::Expr::BinaryOp { left, op, right } => Ok(ast::Expr::BinaryOp {
left: Box::new(bind(left, aggregates, functions, name, scope)?),
op: op.clone(),
right: Box::new(bind(right, aggregates, functions, name, scope)?),
}),
ast::Expr::UnaryOp { op, expr } => Ok(ast::Expr::UnaryOp {
op: *op,
expr: Box::new(bind(expr, aggregates, functions, name, scope)?),
}),
ast::Expr::Nested(inner) => Ok(ast::Expr::Nested(Box::new(bind(
inner, aggregates, functions, name, scope,
)?))),
ast::Expr::IsNull(inner) => Ok(ast::Expr::IsNull(Box::new(bind(
inner, aggregates, functions, name, scope,
)?))),
ast::Expr::IsNotNull(inner) => Ok(ast::Expr::IsNotNull(Box::new(bind(
inner, aggregates, functions, name, scope,
)?))),
ast::Expr::Between {
expr,
negated,
low,
high,
} => Ok(ast::Expr::Between {
expr: Box::new(bind(expr, aggregates, functions, name, scope)?),
negated: *negated,
low: Box::new(bind(low, aggregates, functions, name, scope)?),
high: Box::new(bind(high, aggregates, functions, name, scope)?),
}),
other => Ok(other.clone()),
}
}
pub(super) fn is_aggregate_call(func: &ast::Function, functions: &FunctionRegistry) -> bool {
let name = func
.name
.0
.iter()
.map(|part| match part {
ast::ObjectNamePart::Identifier(ident) => normalize_ident(ident),
_ => String::new(),
})
.collect::<Vec<_>>()
.join(".");
matches!(
functions.lookup(&name).map(|m| m.category),
Some(FunctionCategory::Aggregate)
)
}
fn register_aggregate(
expr: &ast::Expr,
aggregates: &mut Vec<AggregateExpr>,
functions: &FunctionRegistry,
name: BindName,
scope: &TableScope,
) -> Result<String> {
let mut extracted = extract_aggregates(expr, "", functions, scope)?;
let Some(mut agg) = extracted.pop() else {
return Err(SqlError::Unsupported {
detail: format!("aggregate `{expr}` could not be extracted"),
});
};
if !extracted.is_empty() {
return Err(SqlError::Unsupported {
detail: format!("nested aggregates are not supported: `{expr}`"),
});
}
let key = aggregate_output_key(&agg);
if let Some(existing) = aggregates
.iter()
.find(|existing| aggregate_output_key(existing) == key)
{
return Ok(match name {
BindName::Canonical => key,
BindName::Output => existing.alias.clone(),
});
}
agg.alias = key.clone();
aggregates.push(agg);
Ok(key)
}