use sqlparser::ast::{self, GroupByExpr};
use crate::error::Result;
use crate::parser::normalize::normalize_ident;
use crate::resolver::ColumnScope;
use crate::resolver::columns::TableScope;
use crate::resolver::expr::convert_expr;
use crate::types::{ColumnInfo, SqlExpr};
pub fn convert_group_by(group_by: &GroupByExpr, scope: &TableScope) -> Result<Vec<SqlExpr>> {
convert_group_by_with_projection(group_by, &[], &[], scope)
}
pub fn convert_group_by_with_projection(
group_by: &GroupByExpr,
projection: &[ast::SelectItem],
table_columns: &[ColumnInfo],
scope: &TableScope,
) -> Result<Vec<SqlExpr>> {
let key_scope = scope.with_output_names(
crate::planner::select::select_output_aliases(projection)
.into_iter()
.filter(|alias| !is_self_alias(projection, alias)),
);
let key_scope = ColumnScope::Relations(&key_scope);
match group_by {
GroupByExpr::All(_) => Ok(Vec::new()),
GroupByExpr::Expressions(exprs, _) => exprs
.iter()
.map(|e| {
reject_ordinal_out_of_range(e, projection)?;
match resolve_output_alias(e, projection, table_columns) {
Some(aliased) => convert_expr(aliased, &key_scope),
None => convert_expr(e, &key_scope),
}
})
.collect(),
}
}
fn is_self_alias(projection: &[ast::SelectItem], alias: &str) -> bool {
projection.iter().any(|item| match item {
ast::SelectItem::ExprWithAlias { expr, alias: name } => {
normalize_ident(name) == alias && expr_column_name(expr).is_some_and(|c| c == alias)
}
ast::SelectItem::UnnamedExpr(_)
| ast::SelectItem::ExprWithAliases { .. }
| ast::SelectItem::QualifiedWildcard(..)
| ast::SelectItem::Wildcard(_) => false,
})
}
fn reject_ordinal_out_of_range(expr: &ast::Expr, projection: &[ast::SelectItem]) -> Result<()> {
if projection.is_empty() {
return Ok(());
}
let ast::Expr::Value(v) = expr else {
return Ok(());
};
let ast::Value::Number(n, _) = &v.value else {
return Ok(());
};
let in_range = n
.parse::<usize>()
.is_ok_and(|position| position >= 1 && position <= projection.len());
if in_range {
return Ok(());
}
Err(crate::error::SqlError::Unsupported {
detail: format!("GROUP BY position {n} is not in select list"),
})
}
fn resolve_output_alias<'a>(
expr: &ast::Expr,
projection: &'a [ast::SelectItem],
table_columns: &[ColumnInfo],
) -> Option<&'a ast::Expr> {
if let ast::Expr::Value(v) = expr
&& let ast::Value::Number(n, _) = &v.value
&& let Ok(position) = n.parse::<usize>()
&& position >= 1
{
return match projection.get(position - 1) {
Some(
ast::SelectItem::UnnamedExpr(e) | ast::SelectItem::ExprWithAlias { expr: e, .. },
) => Some(e),
Some(_) | None => None,
};
}
let ast::Expr::Identifier(ident) = expr else {
return None;
};
let needle = normalize_ident(ident);
if table_columns.iter().any(|c| c.name == needle) {
return None;
}
projection.iter().find_map(|item| match item {
ast::SelectItem::ExprWithAlias { expr, alias }
if normalize_ident(alias) == needle
&& !matches!(expr, ast::Expr::Identifier(i) if normalize_ident(i) == needle) =>
{
Some(expr)
}
_ => None,
})
}
pub fn group_by_output_aliases(
projection: &[ast::SelectItem],
group_by: &[SqlExpr],
) -> Vec<Option<String>> {
group_by
.iter()
.map(|key| {
key_column_name(key).and_then(|name| projection_alias_for_column(projection, name))
})
.collect()
}
pub fn key_column_name(key: &SqlExpr) -> Option<&str> {
match key {
SqlExpr::Column { name, .. } => Some(name.as_str()),
_ => None,
}
}
fn projection_alias_for_column(projection: &[ast::SelectItem], col: &str) -> Option<String> {
for item in projection {
if let ast::SelectItem::ExprWithAlias { expr, alias } = item
&& expr_column_name(expr).is_some_and(|n| n == col)
{
return Some(normalize_ident(alias));
}
}
None
}
pub fn expr_column_name(expr: &ast::Expr) -> Option<String> {
match expr {
ast::Expr::Identifier(ident) => Some(normalize_ident(ident)),
ast::Expr::CompoundIdentifier(parts) => parts.last().map(normalize_ident),
_ => None,
}
}