1use sqlparser::ast::{Expr, FunctionArguments, SelectItem};
9
10#[derive(Debug, Clone, PartialEq)]
14pub struct AggregateExpr {
15 pub func: AggFunc,
17 pub input: String,
19 pub alias: String,
21}
22
23#[derive(Debug, Clone, PartialEq)]
25pub enum AggFunc {
26 Count,
28 Sum,
30 Avg,
32 Min,
34 Max,
36 CountDistinct,
38}
39
40pub fn extract_aggregates(select_items: &[SelectItem]) -> Vec<AggregateExpr> {
47 select_items
48 .iter()
49 .filter_map(extract_agg_from_item)
50 .collect()
51}
52
53pub fn is_aggregate_expr(expr: &Expr) -> bool {
58 expr_is_aggregate(expr)
59}
60
61pub(crate) fn projection_item_is_aggregate(item: &SelectItem) -> bool {
65 match item {
66 SelectItem::UnnamedExpr(e)
67 | SelectItem::ExprWithAlias { expr: e, .. }
68 | SelectItem::ExprWithAliases { expr: e, .. } => expr_is_aggregate(e),
69 _ => false,
70 }
71}
72
73pub(crate) fn expr_is_aggregate(expr: &Expr) -> bool {
75 match expr {
76 Expr::Function(f) => {
77 let name = f.name.to_string().to_ascii_uppercase();
78 matches!(
79 name.as_str(),
80 "COUNT"
81 | "SUM"
82 | "AVG"
83 | "MIN"
84 | "MAX"
85 | "STDDEV"
86 | "VARIANCE"
87 | "ARRAY_AGG"
88 | "STRING_AGG"
89 )
90 }
91 Expr::BinaryOp { left, right, .. } => expr_is_aggregate(left) || expr_is_aggregate(right),
92 Expr::Nested(inner) => expr_is_aggregate(inner),
93 _ => false,
94 }
95}
96
97fn extract_agg_from_item(item: &SelectItem) -> Option<AggregateExpr> {
100 match item {
101 SelectItem::UnnamedExpr(expr) => {
102 let agg = extract_agg_from_expr(expr)?;
103 Some(AggregateExpr {
105 alias: agg_synthetic_alias(&agg.func, &agg.input),
106 func: agg.func,
107 input: agg.input,
108 })
109 }
110 SelectItem::ExprWithAlias { expr, alias } => {
111 let agg = extract_agg_from_expr(expr)?;
112 Some(AggregateExpr {
113 func: agg.func,
114 input: agg.input,
115 alias: alias.value.clone(),
116 })
117 }
118 SelectItem::ExprWithAliases { expr, aliases } => {
119 let agg = extract_agg_from_expr(expr)?;
120 let alias = aliases
121 .first()
122 .map(|a| a.value.clone())
123 .unwrap_or_else(|| agg_synthetic_alias(&agg.func, &agg.input));
124 Some(AggregateExpr {
125 func: agg.func,
126 input: agg.input,
127 alias,
128 })
129 }
130 _ => None,
131 }
132}
133
134struct PartialAgg {
136 func: AggFunc,
137 input: String,
138}
139
140fn extract_agg_from_expr(expr: &Expr) -> Option<PartialAgg> {
141 if let Expr::Function(f) = expr {
142 let name = f.name.to_string().to_ascii_uppercase();
143 let func = match name.as_str() {
144 "COUNT" => {
145 if is_distinct_args(&f.args) {
147 AggFunc::CountDistinct
148 } else {
149 AggFunc::Count
150 }
151 }
152 "SUM" => AggFunc::Sum,
153 "AVG" => AggFunc::Avg,
154 "MIN" => AggFunc::Min,
155 "MAX" => AggFunc::Max,
156 _ => return None,
157 };
158 let input = extract_first_arg_string(&f.args);
159 Some(PartialAgg { func, input })
160 } else {
161 None
162 }
163}
164
165fn is_distinct_args(args: &FunctionArguments) -> bool {
167 match args {
168 FunctionArguments::List(list) => list.duplicate_treatment.is_some(),
169 _ => false,
170 }
171}
172
173fn extract_first_arg_string(args: &FunctionArguments) -> String {
176 match args {
177 FunctionArguments::List(list) => {
178 if let Some(first) = list.args.first() {
179 first.to_string()
180 } else {
181 "*".to_string()
182 }
183 }
184 FunctionArguments::None => "*".to_string(),
185 FunctionArguments::Subquery(q) => q.to_string(),
186 }
187}
188
189fn agg_synthetic_alias(func: &AggFunc, input: &str) -> String {
191 let fn_part = match func {
192 AggFunc::Count | AggFunc::CountDistinct => "count",
193 AggFunc::Sum => "sum",
194 AggFunc::Avg => "avg",
195 AggFunc::Min => "min",
196 AggFunc::Max => "max",
197 };
198 let col_part = if input == "*" {
199 "star".to_string()
200 } else {
201 input.replace(|c: char| !c.is_alphanumeric(), "_")
202 };
203 format!("{fn_part}_{col_part}")
204}
205
206#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::parse;
212
213 fn select_items(sql: &str) -> Vec<SelectItem> {
214 let stmts = parse(sql).expect("parse");
215 let stmt = stmts.into_iter().next().expect("one stmt");
216 if let sqlparser::ast::Statement::Query(q) = stmt {
217 if let sqlparser::ast::SetExpr::Select(sel) = *q.body {
218 return sel.projection;
219 }
220 }
221 vec![]
222 }
223
224 #[test]
225 fn test_extract_count_star() {
226 let items = select_items("SELECT COUNT(*) FROM t");
227 let aggs = extract_aggregates(&items);
228 assert_eq!(aggs.len(), 1);
229 assert_eq!(aggs[0].func, AggFunc::Count);
230 assert_eq!(aggs[0].input, "*");
231 }
232
233 #[test]
234 fn test_extract_sum() {
235 let items = select_items("SELECT SUM(amount) AS total FROM orders");
236 let aggs = extract_aggregates(&items);
237 assert_eq!(aggs.len(), 1);
238 assert_eq!(aggs[0].func, AggFunc::Sum);
239 assert_eq!(aggs[0].alias, "total");
240 }
241
242 #[test]
243 fn test_extract_count_distinct() {
244 let items = select_items("SELECT COUNT(DISTINCT user_id) FROM events");
245 let aggs = extract_aggregates(&items);
246 assert_eq!(aggs.len(), 1, "expected one aggregate, got {:?}", aggs);
247 assert_eq!(aggs[0].func, AggFunc::CountDistinct);
248 }
249
250 #[test]
251 fn test_is_aggregate_true() {
252 let items = select_items("SELECT COUNT(*) FROM t");
253 let item = items.into_iter().next().expect("item");
254 if let SelectItem::UnnamedExpr(expr) = item {
255 assert!(is_aggregate_expr(&expr));
256 } else {
257 panic!("expected UnnamedExpr");
258 }
259 }
260
261 #[test]
262 fn test_is_aggregate_false() {
263 let items = select_items("SELECT id FROM t");
264 let item = items.into_iter().next().expect("item");
265 if let SelectItem::UnnamedExpr(expr) = item {
266 assert!(!is_aggregate_expr(&expr));
267 } else {
268 panic!("expected UnnamedExpr");
269 }
270 }
271
272 #[test]
273 fn test_having_produces_filter_over_aggregate() {
274 use crate::{parse_one, plan_statement};
275
276 let stmt = parse_one(
277 "SELECT dept, AVG(salary) FROM employees GROUP BY dept HAVING AVG(salary) > 50000",
278 )
279 .expect("parse");
280 let plan = plan_statement(&stmt).expect("plan");
281
282 match &plan {
285 crate::LogicalPlan::Filter { input, predicate } => {
286 assert!(
287 predicate.contains("50000") || !predicate.is_empty(),
288 "HAVING predicate should be non-empty: {predicate}"
289 );
290 assert!(
291 matches!(input.as_ref(), crate::LogicalPlan::Aggregate { .. }),
292 "Filter should wrap Aggregate, got: {input:?}"
293 );
294 }
295 other => panic!("expected Filter over Aggregate, got: {other:?}"),
296 }
297 }
298}