Skip to main content

nodedb_sql/planner/
aggregate_order.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Records the SELECT-list interleaving of GROUP BY keys and aggregate
4//! expressions so the output-schema builder emits aggregate result columns in
5//! the user's SELECT order rather than a hardcoded group-keys-first order.
6
7use sqlparser::ast;
8
9use crate::error::Result;
10use crate::functions::registry::FunctionRegistry;
11use crate::parser::normalize::normalize_ident;
12use crate::planner::aggregate::{
13    extract_aggregates_from_projection, function_args_exprs, normalize_function_name,
14};
15use crate::planner::group_by::{expr_column_name, key_column_name};
16use crate::resolver::ColumnScope;
17use crate::resolver::columns::TableScope;
18use crate::resolver::expr::convert_expr;
19use crate::types::query::AggOutputSlot;
20use crate::types_expr::SqlExpr;
21
22/// Walk `projection` in output order and classify each item as a GROUP BY key
23/// slot or an aggregate slot, producing the interleaving that `output_schema`
24/// replays to emit columns in SELECT-list order.
25///
26/// `group_by` is the canonical key list; [`AggOutputSlot::GroupKey`] indexes
27/// it. Real aggregates are numbered in projection order from 0;
28/// `GROUPING(col)` pseudo-aggregates are numbered after all real aggregates,
29/// mirroring how the planner appends them to the aggregate list. Items that
30/// are neither a bare group key nor an aggregate (a bare non-key expression)
31/// produce no slot — they have no output column in the aggregate result today.
32pub fn compute_output_order(
33    projection: &[ast::SelectItem],
34    group_by: &[SqlExpr],
35    functions: &FunctionRegistry,
36    scope: &TableScope,
37) -> Result<Vec<AggOutputSlot>> {
38    Ok(
39        compute_output_order_by_item(projection, group_by, functions, scope)?
40            .into_iter()
41            .flatten()
42            .collect(),
43    )
44}
45
46/// The output slots each projection item produces, one entry per item in
47/// SELECT-list order. An item that is neither a group key nor an aggregate
48/// (a star, or a bare non-key expression) produces an empty entry.
49///
50/// The flattened entries are exactly [`compute_output_order`]; the per-item
51/// grouping lets a caller interleave other output columns at their
52/// SELECT-list positions.
53pub fn compute_output_order_by_item(
54    projection: &[ast::SelectItem],
55    group_by: &[SqlExpr],
56    functions: &FunctionRegistry,
57    scope: &TableScope,
58) -> Result<Vec<Vec<AggOutputSlot>>> {
59    let real_agg_count = extract_aggregates_from_projection(projection, functions, scope)?.len();
60    let mut by_item = Vec::with_capacity(projection.len());
61    let mut agg_cursor = 0usize;
62    let mut grouping_cursor = 0usize;
63    for item in projection {
64        let mut order = Vec::new();
65        let expr = match item {
66            ast::SelectItem::UnnamedExpr(expr) => expr,
67            ast::SelectItem::ExprWithAlias { expr, .. } => expr,
68            _ => {
69                by_item.push(order);
70                continue;
71            }
72        };
73        // Bare column that names one of the GROUP BY keys.
74        if let Some(name) = expr_column_name(expr)
75            && let Some(index) = group_by
76                .iter()
77                .position(|key| key_column_name(key) == Some(name.as_str()))
78        {
79            order.push(AggOutputSlot::GroupKey(index));
80            by_item.push(order);
81            continue;
82        }
83        // Computed expression that structurally matches a GROUP BY key
84        // (e.g. `UPPER(label)` selected AND grouped). It is neither a bare
85        // column nor an aggregate, so it must be classified before the
86        // grouping / aggregate checks below. `SqlExpr` has no `PartialEq`
87        // (it can carry a subquery plan), so compare the canonical `Debug`
88        // rendering — both the projection expr and the GROUP BY key pass
89        // through the same `convert_expr`, so equal expressions render
90        // identically.
91        if let Ok(converted) = convert_expr(expr, &ColumnScope::Relations(scope)) {
92            let rendered = format!("{converted:?}");
93            if let Some(index) = group_by
94                .iter()
95                .position(|key| format!("{key:?}") == rendered)
96            {
97                order.push(AggOutputSlot::GroupKey(index));
98                by_item.push(order);
99                continue;
100            }
101        }
102        // `GROUPING(col)` pseudo-aggregates: not in the function registry, so
103        // `contains_aggregate` misses them, and the planner appends them after
104        // every real aggregate.
105        let grouping_count = count_grouping_calls(expr);
106        if grouping_count > 0 {
107            for _ in 0..grouping_count {
108                order.push(AggOutputSlot::Aggregate(real_agg_count + grouping_cursor));
109                grouping_cursor += 1;
110            }
111            by_item.push(order);
112            continue;
113        }
114        // Ordinary aggregate expression(s).
115        if crate::aggregate_walk::contains_aggregate(expr, functions) {
116            let alias = match item {
117                ast::SelectItem::ExprWithAlias { alias, .. } => normalize_ident(alias),
118                _ => format!("{expr}").to_lowercase(),
119            };
120            let produced =
121                crate::aggregate_walk::extract_aggregates(expr, &alias, functions, scope)?.len();
122            for _ in 0..produced {
123                order.push(AggOutputSlot::Aggregate(agg_cursor));
124                agg_cursor += 1;
125            }
126        }
127        by_item.push(order);
128    }
129    Ok(by_item)
130}
131
132/// Count the `GROUPING(col)` calls reachable in `expr`, mirroring the planner's
133/// grouping extraction: each argument of a `GROUPING(...)` call yields one
134/// pseudo-aggregate, and binary operators are traversed on both sides.
135fn count_grouping_calls(expr: &ast::Expr) -> usize {
136    match expr {
137        ast::Expr::Function(f) if normalize_function_name(f).eq_ignore_ascii_case("grouping") => {
138            function_args_exprs(f).len()
139        }
140        ast::Expr::BinaryOp { left, right, .. } => {
141            count_grouping_calls(left) + count_grouping_calls(right)
142        }
143        _ => 0,
144    }
145}