1use core::ops::ControlFlow;
27
28use sqlparser::ast::{self, Expr, Visit, Visitor};
29
30use crate::error::{Result, SqlError};
31use crate::functions::registry::FunctionRegistry;
32use crate::parser::normalize::normalize_ident;
33use crate::resolver::ColumnScope;
34use crate::resolver::columns::TableScope;
35use crate::resolver::expr::convert_expr;
36use crate::types::{AggregateExpr, SqlExpr, SqlValue};
37
38pub fn contains_aggregate(expr: &Expr, functions: &FunctionRegistry) -> bool {
41 let mut detector = AggregateDetector {
42 functions,
43 found: false,
44 };
45 let _ = expr.visit(&mut detector);
46 detector.found
47}
48
49pub fn extract_aggregates(
57 expr: &Expr,
58 alias: &str,
59 functions: &FunctionRegistry,
60 scope: &TableScope,
61) -> Result<Vec<AggregateExpr>> {
62 let mut extractor = AggregateExtractor {
63 functions,
64 alias,
65 scope,
66 inside_aggregate: 0,
67 out: Vec::new(),
68 error: None,
69 };
70 let _ = expr.visit(&mut extractor);
71 if let Some(e) = extractor.error {
72 return Err(e);
73 }
74 Ok(extractor.out)
75}
76
77struct AggregateDetector<'a> {
80 functions: &'a FunctionRegistry,
81 found: bool,
82}
83
84impl Visitor for AggregateDetector<'_> {
85 type Break = ();
86
87 fn pre_visit_expr(&mut self, expr: &Expr) -> ControlFlow<()> {
88 if let Expr::Function(f) = expr
89 && self.functions.is_aggregate(&function_name(f))
90 && f.over.is_none()
94 {
95 self.found = true;
96 return ControlFlow::Break(());
97 }
98 ControlFlow::Continue(())
99 }
100}
101
102struct AggregateExtractor<'a> {
105 functions: &'a FunctionRegistry,
106 alias: &'a str,
107 scope: &'a TableScope,
108 inside_aggregate: u32,
112 out: Vec<AggregateExpr>,
113 error: Option<SqlError>,
118}
119
120impl Visitor for AggregateExtractor<'_> {
121 type Break = ();
122
123 fn pre_visit_expr(&mut self, expr: &Expr) -> ControlFlow<()> {
124 if self.error.is_some() {
125 return ControlFlow::Break(());
126 }
127 if let Expr::Function(f) = expr
128 && self.functions.is_aggregate(&function_name(f))
129 && f.over.is_none()
132 {
133 if self.inside_aggregate > 0 {
134 self.error = Some(SqlError::Unsupported {
135 detail: format!(
136 "nested aggregate functions are not allowed: {}(...{}...)",
137 function_name(f),
138 function_name(f),
139 ),
140 });
141 return ControlFlow::Break(());
142 }
143 let (args, distinct) = match function_args_and_distinct(f, self.scope) {
144 Ok(parsed) => parsed,
145 Err(e) => {
146 self.error = Some(e);
147 return ControlFlow::Break(());
148 }
149 };
150 let filter = match aggregate_filter(f, self.scope) {
151 Ok(filter) => filter,
152 Err(e) => {
153 self.error = Some(e);
154 return ControlFlow::Break(());
155 }
156 };
157 let mut arg_text = computed_arg_text(f, &args);
158 let (args, filter_text) = match filter {
159 Some((predicate, text)) => {
160 if arg_text.is_none() {
163 arg_text = Some(match args.first() {
164 Some(SqlExpr::Column { name, .. }) => name.clone(),
165 _ => "*".to_string(),
166 });
167 }
168 (filtered_args(args, predicate), Some(text))
169 }
170 None => (args, None),
171 };
172 self.out.push(AggregateExpr {
173 function: function_name(f),
174 args,
175 alias: self.alias.into(),
176 distinct,
177 grouping_col_index: None,
178 filter_text,
179 arg_text,
180 });
181 self.inside_aggregate += 1;
182 }
183 ControlFlow::Continue(())
184 }
185
186 fn post_visit_expr(&mut self, expr: &Expr) -> ControlFlow<()> {
187 if let Expr::Function(f) = expr
188 && self.functions.is_aggregate(&function_name(f))
189 && f.over.is_none()
190 && self.inside_aggregate > 0
191 {
192 self.inside_aggregate -= 1;
193 }
194 ControlFlow::Continue(())
195 }
196}
197
198fn function_name(f: &ast::Function) -> String {
208 if f.name.0.len() > 1 {
209 let qualified: String = f
212 .name
213 .0
214 .iter()
215 .map(|p| match p {
216 ast::ObjectNamePart::Identifier(ident) => ident.value.clone(),
217 _ => String::new(),
218 })
219 .collect::<Vec<_>>()
220 .join(".");
221 return format!("__schema_qualified__{qualified}");
222 }
223 f.name
224 .0
225 .iter()
226 .map(|p| match p {
227 ast::ObjectNamePart::Identifier(ident) => normalize_ident(ident),
228 _ => String::new(),
229 })
230 .collect::<Vec<_>>()
231 .join(".")
232}
233
234fn function_args_and_distinct(
240 f: &ast::Function,
241 scope: &TableScope,
242) -> Result<(Vec<SqlExpr>, bool)> {
243 let ast::FunctionArguments::List(args) = &f.args else {
244 return Ok((Vec::new(), false));
245 };
246 let mut parsed = Vec::with_capacity(args.args.len());
247 for a in &args.args {
248 match a {
249 ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(e)) => {
250 parsed.push(convert_expr(e, &ColumnScope::Relations(scope))?);
251 }
252 ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Wildcard) => {
254 parsed.push(SqlExpr::Wildcard);
255 }
256 _ => {}
257 }
258 }
259 let distinct = matches!(
260 args.duplicate_treatment,
261 Some(ast::DuplicateTreatment::Distinct)
262 );
263 Ok((parsed, distinct))
264}
265
266fn aggregate_filter(f: &ast::Function, scope: &TableScope) -> Result<Option<(SqlExpr, String)>> {
273 let Some(predicate) = f.filter.as_deref() else {
274 return Ok(None);
275 };
276 Ok(Some((
277 convert_expr(predicate, &ColumnScope::Relations(scope))?,
278 format!("{predicate}").to_lowercase(),
279 )))
280}
281
282fn filtered_args(args: Vec<SqlExpr>, predicate: SqlExpr) -> Vec<SqlExpr> {
289 let args = if args.is_empty() {
290 vec![SqlExpr::Wildcard]
291 } else {
292 args
293 };
294 args.into_iter()
295 .map(|arg| {
296 let value = match arg {
297 SqlExpr::Wildcard => SqlExpr::Literal(SqlValue::Int(1)),
298 other => other,
299 };
300 SqlExpr::Case {
301 operand: None,
302 when_then: vec![(predicate.clone(), value)],
303 else_expr: None,
304 }
305 })
306 .collect()
307}
308
309fn computed_arg_text(f: &ast::Function, args: &[SqlExpr]) -> Option<String> {
312 match args.first() {
313 None | Some(SqlExpr::Column { .. }) | Some(SqlExpr::Wildcard) => return None,
314 Some(_) => {}
315 }
316 let ast::FunctionArguments::List(list) = &f.args else {
317 return None;
318 };
319 list.args.iter().find_map(|a| match a {
320 ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(e)) => {
321 Some(format!("{e}").to_lowercase())
322 }
323 _ => None,
324 })
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::parser::statement::parse_sql;
331 use crate::resolver::columns::ResolvedTable;
332 use crate::types::{CollectionInfo, EngineType};
333
334 fn first_select_projection(sql: &str) -> Vec<ast::SelectItem> {
335 let stmts = parse_sql(sql).unwrap();
336 match stmts.into_iter().next().unwrap() {
337 ast::Statement::Query(q) => match *q.body {
338 ast::SetExpr::Select(s) => s.projection,
339 _ => panic!(),
340 },
341 _ => panic!(),
342 }
343 }
344
345 fn first_expr(sql: &str) -> ast::Expr {
346 match first_select_projection(sql).into_iter().next().unwrap() {
347 ast::SelectItem::UnnamedExpr(e) | ast::SelectItem::ExprWithAlias { expr: e, .. } => e,
348 _ => panic!(),
349 }
350 }
351
352 fn functions() -> FunctionRegistry {
353 FunctionRegistry::new()
354 }
355
356 fn open_scope() -> TableScope {
358 let info = CollectionInfo {
359 name: "t".into(),
360 engine: EngineType::DocumentSchemaless,
361 columns: Vec::new(),
362 primary_key: None,
363 has_auto_tier: false,
364 indexes: Vec::new(),
365 bitemporal: false,
366 primary: nodedb_types::PrimaryEngine::Document,
367 vector_primary: None,
368 partition_strategy: nodedb_types::PartitionStrategy::CollectionHomed,
369 open_schema: CollectionInfo::open_schema_for(EngineType::DocumentSchemaless),
370 };
371 TableScope::single(ResolvedTable {
372 name: info.name.clone(),
373 alias: None,
374 info,
375 })
376 .expect("single-relation scope")
377 }
378
379 #[test]
382 fn detect_plain_aggregate() {
383 assert!(contains_aggregate(
384 &first_expr("SELECT SUM(x) FROM t"),
385 &functions()
386 ));
387 }
388
389 #[test]
390 fn detect_aggregate_inside_case() {
391 assert!(contains_aggregate(
392 &first_expr("SELECT CASE WHEN x > 0 THEN SUM(y) ELSE 0 END FROM t"),
393 &functions(),
394 ));
395 }
396
397 #[test]
398 fn detect_aggregate_inside_cast() {
399 assert!(contains_aggregate(
400 &first_expr("SELECT CAST(SUM(x) AS TEXT) FROM t"),
401 &functions(),
402 ));
403 }
404
405 #[test]
406 fn detect_aggregate_inside_unary_op() {
407 assert!(contains_aggregate(
408 &first_expr("SELECT -SUM(x) FROM t"),
409 &functions(),
410 ));
411 }
412
413 #[test]
414 fn detect_aggregate_inside_coalesce() {
415 assert!(contains_aggregate(
416 &first_expr("SELECT COALESCE(SUM(x), 0) FROM t"),
417 &functions(),
418 ));
419 }
420
421 #[test]
422 fn detect_aggregate_inside_between() {
423 assert!(contains_aggregate(
424 &first_expr("SELECT SUM(x) BETWEEN 1 AND 10 FROM t"),
425 &functions(),
426 ));
427 }
428
429 #[test]
430 fn detect_aggregate_inside_in_list() {
431 assert!(contains_aggregate(
432 &first_expr("SELECT SUM(x) IN (1, 2, 3) FROM t"),
433 &functions(),
434 ));
435 }
436
437 #[test]
438 fn no_aggregate_in_plain_select() {
439 assert!(!contains_aggregate(
440 &first_expr("SELECT x FROM t"),
441 &functions()
442 ));
443 assert!(!contains_aggregate(
444 &first_expr("SELECT x + 1 FROM t"),
445 &functions()
446 ));
447 assert!(!contains_aggregate(
448 &first_expr("SELECT upper(name) FROM t"),
449 &functions(),
450 ));
451 }
452
453 #[test]
456 fn extract_plain_aggregate() {
457 let aggs = extract_aggregates(
458 &first_expr("SELECT SUM(x) FROM t"),
459 "total",
460 &functions(),
461 &open_scope(),
462 )
463 .unwrap();
464 assert_eq!(aggs.len(), 1);
465 assert_eq!(aggs[0].function, "sum");
466 assert_eq!(aggs[0].alias, "total");
467 }
468
469 #[test]
470 fn extract_aggregate_inside_cast() {
471 let aggs = extract_aggregates(
472 &first_expr("SELECT CAST(SUM(x) AS TEXT) AS n FROM t"),
473 "n",
474 &functions(),
475 &open_scope(),
476 )
477 .unwrap();
478 assert_eq!(aggs.len(), 1);
479 assert_eq!(aggs[0].function, "sum");
480 }
481
482 #[test]
483 fn extract_aggregate_inside_case() {
484 let aggs = extract_aggregates(
485 &first_expr("SELECT CASE WHEN x > 0 THEN SUM(y) ELSE 0 END FROM t"),
486 "r",
487 &functions(),
488 &open_scope(),
489 )
490 .unwrap();
491 assert_eq!(aggs.len(), 1);
492 assert_eq!(aggs[0].function, "sum");
493 }
494
495 #[test]
496 fn extract_aggregate_inside_coalesce() {
497 let aggs = extract_aggregates(
498 &first_expr("SELECT COALESCE(SUM(x), 0) FROM t"),
499 "r",
500 &functions(),
501 &open_scope(),
502 )
503 .unwrap();
504 assert_eq!(aggs.len(), 1);
505 assert_eq!(aggs[0].function, "sum");
506 }
507
508 #[test]
509 fn extract_two_aggregates_under_one_alias() {
510 let aggs = extract_aggregates(
511 &first_expr("SELECT SUM(x) + COUNT(y) AS total FROM t"),
512 "total",
513 &functions(),
514 &open_scope(),
515 )
516 .unwrap();
517 assert_eq!(aggs.len(), 2);
518 let names: Vec<&str> = aggs.iter().map(|a| a.function.as_str()).collect();
519 assert!(names.contains(&"sum"));
520 assert!(names.contains(&"count"));
521 }
522
523 #[test]
524 fn nested_aggregate_directly_inside_aggregate_rejected() {
525 let err = extract_aggregates(
526 &first_expr("SELECT SUM(AVG(x)) FROM t"),
527 "r",
528 &functions(),
529 &open_scope(),
530 )
531 .unwrap_err();
532 let msg = format!("{err:?}");
533 assert!(
534 msg.to_lowercase().contains("nested aggregate"),
535 "error must identify the nested-aggregate class: {msg}"
536 );
537 }
538
539 #[test]
544 fn nested_aggregate_through_cast_rejected() {
545 let err = extract_aggregates(
546 &first_expr("SELECT SUM(CAST(AVG(x) AS BIGINT)) FROM t"),
547 "r",
548 &functions(),
549 &open_scope(),
550 )
551 .unwrap_err();
552 assert!(
553 format!("{err:?}")
554 .to_lowercase()
555 .contains("nested aggregate"),
556 "got: {err:?}"
557 );
558 }
559
560 #[test]
563 fn sibling_aggregates_not_treated_as_nested() {
564 let aggs = extract_aggregates(
565 &first_expr("SELECT CAST(SUM(x) AS TEXT) || CAST(COUNT(y) AS TEXT) FROM t"),
566 "r",
567 &functions(),
568 &open_scope(),
569 )
570 .unwrap();
571 assert_eq!(aggs.len(), 2);
572 }
573
574 #[test]
575 fn extract_distinct_preserved() {
576 let aggs = extract_aggregates(
577 &first_expr("SELECT COUNT(DISTINCT x) FROM t"),
578 "c",
579 &functions(),
580 &open_scope(),
581 )
582 .unwrap();
583 assert_eq!(aggs.len(), 1);
584 assert!(aggs[0].distinct);
585 }
586}