1use sqlparser::ast::{self, GroupByExpr};
6
7use crate::engine_rules::{self, AggregateParams};
8use crate::error::Result;
9use crate::functions::registry::{FunctionRegistry, SearchTrigger};
10use crate::parser::normalize::normalize_ident;
11use crate::planner::grouping_sets::expand_group_by;
12use crate::resolver::columns::ResolvedTable;
13use crate::resolver::expr::convert_expr;
14use crate::temporal::TemporalScope;
15use crate::types::*;
16
17pub fn plan_aggregate(
19 select: &ast::Select,
20 table: &ResolvedTable,
21 filters: &[Filter],
22 _scope: &crate::resolver::columns::TableScope,
23 functions: &FunctionRegistry,
24 temporal: &TemporalScope,
25) -> Result<SqlPlan> {
26 let grouping_expansion = expand_group_by(&select.group_by)?;
28
29 let (group_by_exprs, grouping_sets) = if let Some(exp) = grouping_expansion {
30 (exp.canonical_keys, Some(exp.grouping_sets))
31 } else {
32 (convert_group_by(&select.group_by)?, None)
33 };
34
35 let mut aggregates = extract_aggregates_from_projection(&select.projection, functions)?;
36 let having = match &select.having {
37 Some(expr) => super::select::convert_where_to_filters(expr)?,
38 None => Vec::new(),
39 };
40
41 if grouping_sets.is_some() {
44 let grouping_aggs = extract_grouping_calls(&select.projection, &group_by_exprs)?;
45 aggregates.extend(grouping_aggs);
46 }
47
48 let output_order = super::aggregate_order::compute_output_order(
53 &select.projection,
54 &group_by_exprs,
55 functions,
56 )?;
57
58 let (bucket_interval_ms, group_columns) =
60 extract_timeseries_params(&select.group_by, &select.projection, functions)?;
61
62 let rules = engine_rules::resolve_engine_rules(table.info.engine);
63 let mut base_plan = rules.plan_aggregate(AggregateParams {
64 collection: table.name.clone(),
65 alias: table.alias.clone(),
66 filters: filters.to_vec(),
67 group_by: group_by_exprs.clone(),
68 aggregates: aggregates.clone(),
69 having: having.clone(),
70 limit: 10000,
71 bucket_interval_ms,
72 group_columns,
73 has_auto_tier: table.info.has_auto_tier,
74 bitemporal: table.info.bitemporal,
75 temporal: *temporal,
76 })?;
77
78 let group_by_aliases = group_by_output_aliases(&select.projection, &group_by_exprs);
83 if let SqlPlan::Aggregate {
84 group_by_aliases: alias_slot,
85 output_order: order_slot,
86 ..
87 } = &mut base_plan
88 {
89 *alias_slot = group_by_aliases.clone();
90 *order_slot = output_order.clone();
91 }
92
93 if let Some(sets) = grouping_sets {
95 return Ok(attach_grouping_sets(
96 base_plan,
97 group_by_exprs,
98 group_by_aliases,
99 output_order,
100 aggregates,
101 having,
102 sets,
103 ));
104 }
105
106 Ok(base_plan)
107}
108
109pub fn group_by_output_aliases(
116 projection: &[ast::SelectItem],
117 group_by: &[SqlExpr],
118) -> Vec<Option<String>> {
119 group_by
120 .iter()
121 .map(|key| {
122 key_column_name(key).and_then(|name| projection_alias_for_column(projection, name))
123 })
124 .collect()
125}
126
127pub(super) fn key_column_name(key: &SqlExpr) -> Option<&str> {
129 match key {
130 SqlExpr::Column { name, .. } => Some(name.as_str()),
131 _ => None,
132 }
133}
134
135fn projection_alias_for_column(projection: &[ast::SelectItem], col: &str) -> Option<String> {
139 for item in projection {
140 if let ast::SelectItem::ExprWithAlias { expr, alias } = item
141 && expr_column_name(expr).is_some_and(|n| n == col)
142 {
143 return Some(normalize_ident(alias));
144 }
145 }
146 None
147}
148
149pub(super) fn expr_column_name(expr: &ast::Expr) -> Option<String> {
152 match expr {
153 ast::Expr::Identifier(ident) => Some(normalize_ident(ident)),
154 ast::Expr::CompoundIdentifier(parts) => parts.last().map(normalize_ident),
155 _ => None,
156 }
157}
158
159fn attach_grouping_sets(
162 base_plan: SqlPlan,
163 group_by: Vec<SqlExpr>,
164 group_by_aliases: Vec<Option<String>>,
165 output_order: Vec<AggOutputSlot>,
166 aggregates: Vec<AggregateExpr>,
167 having: Vec<Filter>,
168 grouping_sets: Vec<Vec<usize>>,
169) -> SqlPlan {
170 match base_plan {
171 SqlPlan::Aggregate {
172 input,
173 limit,
174 grouping_sets: _,
175 sort_keys,
176 ..
177 } => SqlPlan::Aggregate {
178 input,
179 group_by,
180 group_by_aliases,
181 output_order,
182 aggregates,
183 having,
184 limit,
185 grouping_sets: Some(grouping_sets),
186 sort_keys,
187 },
188 other => {
189 SqlPlan::Aggregate {
192 input: Box::new(other),
193 group_by,
194 group_by_aliases,
195 output_order,
196 aggregates,
197 having,
198 limit: 10000,
199 grouping_sets: Some(grouping_sets),
200 sort_keys: Vec::new(),
201 }
202 }
203 }
204}
205
206fn extract_timeseries_params(
211 raw_group_by: &GroupByExpr,
212 select_items: &[ast::SelectItem],
213 functions: &FunctionRegistry,
214) -> Result<(Option<i64>, Vec<String>)> {
215 let mut bucket_interval_ms: Option<i64> = None;
216 let mut group_columns = Vec::new();
217
218 if let GroupByExpr::Expressions(exprs, _) = raw_group_by {
219 for expr in exprs {
220 let resolved = resolve_group_by_expr(expr, select_items);
221 let check_expr = resolved.unwrap_or(expr);
222
223 if let Some(interval) = try_extract_time_bucket(check_expr, functions)? {
224 bucket_interval_ms = Some(interval);
225 continue;
226 }
227
228 if let ast::Expr::Identifier(ident) = expr {
229 group_columns.push(normalize_ident(ident));
230 }
231 }
232 }
233
234 Ok((bucket_interval_ms, group_columns))
235}
236
237fn try_extract_time_bucket(expr: &ast::Expr, functions: &FunctionRegistry) -> Result<Option<i64>> {
239 if let ast::Expr::Function(func) = expr {
240 let name = func
241 .name
242 .0
243 .iter()
244 .map(|p| match p {
245 ast::ObjectNamePart::Identifier(ident) => normalize_ident(ident),
246 _ => String::new(),
247 })
248 .collect::<Vec<_>>()
249 .join(".");
250 if functions.search_trigger(&name) == SearchTrigger::TimeBucket {
251 return Ok(Some(extract_bucket_interval(func)?));
252 }
253 }
254 Ok(None)
255}
256
257fn resolve_group_by_expr<'a>(
262 expr: &ast::Expr,
263 select_items: &'a [ast::SelectItem],
264) -> Option<&'a ast::Expr> {
265 match expr {
266 ast::Expr::Identifier(ident) => {
267 let alias_name = normalize_ident(ident);
268 select_items.iter().find_map(|item| {
269 if let ast::SelectItem::ExprWithAlias { expr, alias } = item
270 && normalize_ident(alias) == alias_name
271 {
272 Some(expr)
273 } else {
274 None
275 }
276 })
277 }
278 ast::Expr::Value(v) => {
279 if let ast::Value::Number(n, _) = &v.value
280 && let Ok(idx) = n.parse::<usize>()
281 && idx >= 1
282 && idx <= select_items.len()
283 {
284 match &select_items[idx - 1] {
285 ast::SelectItem::UnnamedExpr(e) => Some(e),
286 ast::SelectItem::ExprWithAlias { expr, .. } => Some(expr),
287 _ => None,
288 }
289 } else {
290 None
291 }
292 }
293 _ => None,
294 }
295}
296
297fn extract_bucket_interval(func: &ast::Function) -> Result<i64> {
304 let args = match &func.args {
305 ast::FunctionArguments::List(args) => &args.args,
306 _ => return Ok(0),
307 };
308 for arg in args {
310 if let ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(ast::Expr::Value(v))) = arg {
311 match &v.value {
312 ast::Value::SingleQuotedString(s) => {
313 let ms = parse_interval_to_ms(s);
314 if ms > 0 {
315 return Ok(ms);
316 }
317 }
318 ast::Value::Number(n, _) => {
319 if let Ok(secs) = n.parse::<i64>()
320 && secs > 0
321 {
322 return Ok(secs * 1000);
323 }
324 }
325 _ => {}
326 }
327 }
328 }
329 Ok(0)
330}
331
332fn parse_interval_to_ms(s: &str) -> i64 {
336 nodedb_types::kv_parsing::parse_interval_to_ms(s)
337 .map(|ms| ms as i64)
338 .unwrap_or(0)
339}
340
341pub fn convert_group_by(group_by: &GroupByExpr) -> Result<Vec<SqlExpr>> {
343 match group_by {
344 GroupByExpr::All(_) => Ok(Vec::new()),
345 GroupByExpr::Expressions(exprs, _) => exprs.iter().map(convert_expr).collect(),
346 }
347}
348
349fn extract_grouping_calls(
356 items: &[ast::SelectItem],
357 canonical_keys: &[SqlExpr],
358) -> Result<Vec<AggregateExpr>> {
359 let mut out = Vec::new();
360 for item in items {
361 let (expr, alias): (&ast::Expr, String) = match item {
362 ast::SelectItem::UnnamedExpr(expr) => (expr, format!("{expr}")),
363 ast::SelectItem::ExprWithAlias { expr, alias } => (expr, normalize_ident(alias)),
364 _ => continue,
365 };
366 collect_grouping_from_expr(expr, &alias, canonical_keys, &mut out)?;
367 }
368 Ok(out)
369}
370
371fn collect_grouping_from_expr(
373 expr: &ast::Expr,
374 alias: &str,
375 canonical_keys: &[SqlExpr],
376 out: &mut Vec<AggregateExpr>,
377) -> Result<()> {
378 match expr {
379 ast::Expr::Function(f) => {
380 let name = normalize_function_name(f);
381 if name.eq_ignore_ascii_case("grouping") {
382 let args = function_args_exprs(f);
384 for col_expr in &args {
385 let canonical_idx = crate::planner::grouping_sets::resolve_grouping_col(
386 col_expr,
387 canonical_keys,
388 )?;
389 out.push(AggregateExpr {
391 function: "grouping".into(),
392 args: vec![convert_expr(col_expr)?],
393 alias: alias.to_string(),
394 distinct: false,
395 grouping_col_index: Some(canonical_idx),
396 });
397 }
398 }
399 }
400 ast::Expr::BinaryOp { left, right, .. } => {
402 collect_grouping_from_expr(left, alias, canonical_keys, out)?;
403 collect_grouping_from_expr(right, alias, canonical_keys, out)?;
404 }
405 _ => {}
406 }
407 Ok(())
408}
409
410pub(super) fn function_args_exprs(f: &ast::Function) -> Vec<&ast::Expr> {
412 match &f.args {
413 ast::FunctionArguments::List(list) => list
414 .args
415 .iter()
416 .filter_map(|a| match a {
417 ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(e)) => Some(e),
418 _ => None,
419 })
420 .collect(),
421 _ => Vec::new(),
422 }
423}
424
425pub(super) fn normalize_function_name(f: &ast::Function) -> String {
427 f.name
428 .0
429 .last()
430 .map(|p| match p {
431 ast::ObjectNamePart::Identifier(ident) => normalize_ident(ident),
432 _ => String::new(),
433 })
434 .unwrap_or_default()
435}
436
437pub fn extract_aggregates_from_projection(
439 items: &[ast::SelectItem],
440 functions: &FunctionRegistry,
441) -> Result<Vec<AggregateExpr>> {
442 let mut aggregates = Vec::new();
443 for item in items {
444 let (expr, alias): (&ast::Expr, String) = match item {
445 ast::SelectItem::UnnamedExpr(expr) => (expr, format!("{expr}").to_lowercase()),
454 ast::SelectItem::ExprWithAlias { expr, alias } => (expr, normalize_ident(alias)),
455 _ => continue,
456 };
457 let mut extracted = crate::aggregate_walk::extract_aggregates(expr, &alias, functions)?;
458 aggregates.append(&mut extracted);
459 }
460 Ok(aggregates)
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn parse_intervals() {
469 assert_eq!(parse_interval_to_ms("1h"), 3_600_000);
470 assert_eq!(parse_interval_to_ms("15m"), 900_000);
471 assert_eq!(parse_interval_to_ms("30s"), 30_000);
472 assert_eq!(parse_interval_to_ms("7d"), 604_800_000);
473 assert_eq!(parse_interval_to_ms("1 hour"), 3_600_000);
475 assert_eq!(parse_interval_to_ms("2 hours"), 7_200_000);
476 assert_eq!(parse_interval_to_ms("15 minutes"), 900_000);
477 assert_eq!(parse_interval_to_ms("30 seconds"), 30_000);
478 assert_eq!(parse_interval_to_ms("1 day"), 86_400_000);
479 assert_eq!(parse_interval_to_ms("5 min"), 300_000);
480 }
481
482 fn parse_select(sql: &str) -> ast::Select {
484 use sqlparser::dialect::GenericDialect;
485 use sqlparser::parser::Parser;
486 let stmts = Parser::parse_sql(&GenericDialect {}, sql).unwrap();
487 match stmts.into_iter().next().unwrap() {
488 ast::Statement::Query(q) => match *q.body {
489 ast::SetExpr::Select(s) => *s,
490 _ => panic!("expected SELECT"),
491 },
492 _ => panic!("expected query"),
493 }
494 }
495
496 #[test]
497 fn resolve_group_by_alias_to_time_bucket() {
498 let select = parse_select(
499 "SELECT time_bucket('1 hour', timestamp) AS b, COUNT(*) FROM t GROUP BY b",
500 );
501 let functions = FunctionRegistry::new();
502
503 if let GroupByExpr::Expressions(exprs, _) = &select.group_by {
504 let resolved = resolve_group_by_expr(&exprs[0], &select.projection);
505 assert!(resolved.is_some(), "alias 'b' should resolve");
506 let interval = try_extract_time_bucket(resolved.unwrap(), &functions).unwrap();
507 assert_eq!(interval, Some(3_600_000));
508 } else {
509 panic!("expected GROUP BY expressions");
510 }
511 }
512
513 #[test]
514 fn resolve_group_by_ordinal_to_time_bucket() {
515 let select =
516 parse_select("SELECT time_bucket('5 minutes', timestamp), COUNT(*) FROM t GROUP BY 1");
517 let functions = FunctionRegistry::new();
518
519 if let GroupByExpr::Expressions(exprs, _) = &select.group_by {
520 let resolved = resolve_group_by_expr(&exprs[0], &select.projection);
521 assert!(resolved.is_some(), "ordinal 1 should resolve");
522 let interval = try_extract_time_bucket(resolved.unwrap(), &functions).unwrap();
523 assert_eq!(interval, Some(300_000));
524 } else {
525 panic!("expected GROUP BY expressions");
526 }
527 }
528
529 #[test]
530 fn resolve_group_by_plain_column_not_time_bucket() {
531 let select = parse_select("SELECT qtype, COUNT(*) FROM t GROUP BY qtype");
532 let functions = FunctionRegistry::new();
533
534 if let GroupByExpr::Expressions(exprs, _) = &select.group_by {
535 let resolved = resolve_group_by_expr(&exprs[0], &select.projection);
536 assert!(resolved.is_none());
538 let interval = try_extract_time_bucket(&exprs[0], &functions).unwrap();
539 assert_eq!(interval, None);
540 } else {
541 panic!("expected GROUP BY expressions");
542 }
543 }
544}