use sqlparser::ast;
use crate::engine_rules::{self, AggregateParams, TimeBucketKey};
use crate::error::Result;
use crate::functions::registry::{FunctionRegistry, SearchTrigger};
use crate::parser::normalize::normalize_ident;
use crate::planner::aggregate_clauses::plan_aggregate_clauses;
use crate::resolver::ColumnScope;
use crate::resolver::columns::{ResolvedTable, TableScope};
use crate::resolver::expr::convert_expr;
use crate::temporal::TemporalScope;
use crate::types::*;
pub struct AggregateQuery<'a> {
pub select: &'a ast::Select,
pub order_by: Option<&'a ast::OrderBy>,
pub table: &'a ResolvedTable,
pub filters: &'a [Filter],
pub scope: &'a TableScope,
pub temporal: &'a TemporalScope,
pub input_joins: bool,
}
pub const AGGREGATE_NO_LIMIT: usize = usize::MAX;
pub fn plan_aggregate(query: AggregateQuery<'_>, functions: &FunctionRegistry) -> Result<SqlPlan> {
let AggregateQuery {
select,
order_by,
table,
filters,
scope,
temporal,
input_joins,
} = query;
let clauses = plan_aggregate_clauses(select, order_by, &table.info.columns, scope, functions)?;
let time_bucket = time_bucket_key(&clauses.group_by, functions);
let rules = engine_rules::resolve_engine_rules(table.info.engine);
let mut plan = rules.plan_aggregate(AggregateParams {
collection: table.name.clone(),
alias: table.alias.clone(),
filters: filters.to_vec(),
group_by: clauses.group_by,
aggregates: clauses.aggregates,
having: clauses.having,
limit: AGGREGATE_NO_LIMIT,
time_bucket,
grouping_sets: clauses.grouping_sets,
columns: table.info.columns.clone(),
has_auto_tier: table.info.has_auto_tier,
bitemporal: table.info.bitemporal,
temporal: *temporal,
hidden_aggregates: clauses.hidden_aggregates,
input_joins,
})?;
if let SqlPlan::Aggregate {
group_by_aliases,
output_order,
..
} = &mut plan
{
*group_by_aliases = clauses.group_by_aliases;
*output_order = clauses.output_order;
}
Ok(plan)
}
fn time_bucket_key(group_by: &[SqlExpr], functions: &FunctionRegistry) -> Option<TimeBucketKey> {
group_by
.iter()
.enumerate()
.find_map(|(group_index, key)| match key {
SqlExpr::Function { name, args, .. }
if functions.search_trigger(name) == SearchTrigger::TimeBucket =>
{
Some(TimeBucketKey {
group_index,
interval_ms: bucket_interval_ms(args),
})
}
_ => None,
})
}
fn bucket_interval_ms(args: &[SqlExpr]) -> i64 {
args.iter()
.find_map(|arg| match arg {
SqlExpr::Literal(SqlValue::String(s)) => {
Some(parse_interval_to_ms(s)).filter(|ms| *ms > 0)
}
SqlExpr::Literal(SqlValue::Int(secs)) if *secs > 0 => secs.checked_mul(1000),
_ => None,
})
.unwrap_or(0)
}
fn parse_interval_to_ms(s: &str) -> i64 {
nodedb_types::kv_parsing::parse_interval_to_ms(s)
.map(|ms| ms as i64)
.unwrap_or(0)
}
pub(super) fn extract_grouping_calls(
items: &[ast::SelectItem],
canonical_keys: &[SqlExpr],
scope: &TableScope,
) -> Result<Vec<AggregateExpr>> {
let mut out = Vec::new();
for item in items {
let (expr, alias): (&ast::Expr, String) = match item {
ast::SelectItem::UnnamedExpr(expr) => (expr, format!("{expr}")),
ast::SelectItem::ExprWithAlias { expr, alias } => (expr, normalize_ident(alias)),
_ => continue,
};
collect_grouping_from_expr(expr, &alias, canonical_keys, &mut out, scope)?;
}
Ok(out)
}
fn collect_grouping_from_expr(
expr: &ast::Expr,
alias: &str,
canonical_keys: &[SqlExpr],
out: &mut Vec<AggregateExpr>,
scope: &TableScope,
) -> Result<()> {
match expr {
ast::Expr::Function(f) => {
let name = normalize_function_name(f);
if name.eq_ignore_ascii_case("grouping") {
let args = function_args_exprs(f);
for col_expr in &args {
let canonical_idx = crate::planner::grouping_sets::resolve_grouping_col(
col_expr,
canonical_keys,
)?;
out.push(AggregateExpr {
function: "grouping".into(),
args: vec![convert_expr(col_expr, &ColumnScope::Relations(scope))?],
alias: alias.to_string(),
distinct: false,
grouping_col_index: Some(canonical_idx),
filter_text: None,
arg_text: None,
});
}
}
}
ast::Expr::BinaryOp { left, right, .. } => {
collect_grouping_from_expr(left, alias, canonical_keys, out, scope)?;
collect_grouping_from_expr(right, alias, canonical_keys, out, scope)?;
}
_ => {}
}
Ok(())
}
pub(super) fn function_args_exprs(f: &ast::Function) -> Vec<&ast::Expr> {
match &f.args {
ast::FunctionArguments::List(list) => list
.args
.iter()
.filter_map(|a| match a {
ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(e)) => Some(e),
_ => None,
})
.collect(),
_ => Vec::new(),
}
}
pub(super) fn normalize_function_name(f: &ast::Function) -> String {
f.name
.0
.last()
.map(|p| match p {
ast::ObjectNamePart::Identifier(ident) => normalize_ident(ident),
_ => String::new(),
})
.unwrap_or_default()
}
pub fn extract_aggregates_from_projection(
items: &[ast::SelectItem],
functions: &FunctionRegistry,
scope: &TableScope,
) -> Result<Vec<AggregateExpr>> {
let mut aggregates = Vec::new();
for item in items {
let (expr, alias): (&ast::Expr, String) = match item {
ast::SelectItem::UnnamedExpr(expr) => (expr, format!("{expr}").to_lowercase()),
ast::SelectItem::ExprWithAlias { expr, alias } => (expr, normalize_ident(alias)),
_ => continue,
};
let mut extracted =
crate::aggregate_walk::extract_aggregates(expr, &alias, functions, scope)?;
aggregates.append(&mut extracted);
}
Ok(aggregates)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_intervals() {
assert_eq!(parse_interval_to_ms("1h"), 3_600_000);
assert_eq!(parse_interval_to_ms("15m"), 900_000);
assert_eq!(parse_interval_to_ms("30s"), 30_000);
assert_eq!(parse_interval_to_ms("7d"), 604_800_000);
assert_eq!(parse_interval_to_ms("1 hour"), 3_600_000);
assert_eq!(parse_interval_to_ms("2 hours"), 7_200_000);
assert_eq!(parse_interval_to_ms("15 minutes"), 900_000);
assert_eq!(parse_interval_to_ms("30 seconds"), 30_000);
assert_eq!(parse_interval_to_ms("1 day"), 86_400_000);
assert_eq!(parse_interval_to_ms("5 min"), 300_000);
}
fn column(name: &str) -> SqlExpr {
SqlExpr::Column {
table: None,
name: name.into(),
}
}
fn time_bucket(interval: SqlValue) -> SqlExpr {
SqlExpr::Function {
name: "time_bucket".into(),
args: vec![SqlExpr::Literal(interval), column("ts")],
distinct: false,
}
}
#[test]
fn time_bucket_key_finds_the_bucket_among_plain_keys() {
let functions = FunctionRegistry::new();
let keys = vec![
column("host"),
time_bucket(SqlValue::String("1 hour".into())),
];
assert_eq!(
time_bucket_key(&keys, &functions),
Some(TimeBucketKey {
group_index: 1,
interval_ms: 3_600_000,
})
);
}
#[test]
fn time_bucket_key_reads_integer_seconds() {
let functions = FunctionRegistry::new();
let keys = vec![time_bucket(SqlValue::Int(300))];
assert_eq!(
time_bucket_key(&keys, &functions).map(|k| k.interval_ms),
Some(300_000)
);
}
#[test]
fn time_bucket_key_is_absent_for_plain_columns() {
let functions = FunctionRegistry::new();
assert_eq!(time_bucket_key(&[column("qtype")], &functions), None);
}
#[test]
fn unreadable_interval_is_zero() {
let functions = FunctionRegistry::new();
let keys = vec![time_bucket(SqlValue::String("soon".into()))];
assert_eq!(
time_bucket_key(&keys, &functions).map(|k| k.interval_ms),
Some(0)
);
}
}