Skip to main content

nodedb_sql/planner/
agg_naming.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Canonical output names for aggregate expressions.
4//!
5//! One aggregate has two names: the *canonical* key it is computed and stored
6//! under in a finalized group row (`sum(amount)`), and the *user alias* the
7//! client sees (`SELECT SUM(amount) AS total`). The rename to the user alias
8//! happens last, after HAVING has filtered, so anything that has to address an
9//! aggregate mid-pipeline must use the canonical key. Deriving that key in one
10//! place keeps the planner's HAVING rewrite and the physical spec builder from
11//! drifting into two different names for the same column.
12
13use crate::types::{AggregateExpr, SqlExpr};
14
15/// The accumulator name for an aggregate, accounting for `DISTINCT`.
16pub fn aggregate_function_name(a: &AggregateExpr) -> String {
17    if !a.distinct {
18        return a.function.clone();
19    }
20    match a.function.as_str() {
21        "count" => "count_distinct".into(),
22        "array_agg" => "array_agg_distinct".into(),
23        // SUM(DISTINCT col) / AVG(DISTINCT col) route to a dedicated
24        // accumulator that dedupes input values before summing. The plain
25        // "sum"/"avg" accumulator does not dedupe, so without this remap
26        // `DISTINCT` would be silently ignored.
27        "sum" => "sum_distinct".into(),
28        "avg" => "avg_distinct".into(),
29        // MIN(DISTINCT) and MAX(DISTINCT) yield the same result as their
30        // non-distinct counterparts (the smallest / largest value is the same
31        // whether or not duplicates are deduped), so we accept the DISTINCT
32        // modifier but route to the regular accumulator.
33        _ => a.function.clone(),
34    }
35}
36
37/// The field name an aggregate accumulates over: the bare column when the
38/// argument is one, and `*` for a wildcard or a computed argument (which the
39/// accumulator receives as an expression rather than a field).
40pub fn aggregate_field_name(a: &AggregateExpr) -> String {
41    a.args
42        .first()
43        .map(|arg| match arg {
44            SqlExpr::Column { name, .. } => name.clone(),
45            _ => "*".to_string(),
46        })
47        .unwrap_or_else(|| "*".to_string())
48}
49
50/// The canonical output-column key an aggregate's value is stored under in a
51/// finalized group row.
52///
53/// A computed argument is named by its source text and a `FILTER` clause is
54/// appended, so `SUM(a * 2)`, `SUM(b * 3)` and `COUNT(*) FILTER (WHERE ...)`
55/// each land under their own key.
56pub fn aggregate_output_key(a: &AggregateExpr) -> String {
57    let arg = a
58        .arg_text
59        .clone()
60        .unwrap_or_else(|| aggregate_field_name(a));
61    let base = nodedb_query::agg_key::canonical_agg_key(&aggregate_function_name(a), &arg);
62    match &a.filter_text {
63        None => base,
64        Some(text) => format!("{base} filter (where {text})"),
65    }
66}
67
68/// The internal name of a computed (non-column) GROUP BY key: a stable
69/// `group_{index}` placeholder. The executor emits the evaluated key under
70/// it, and the response shaper reads the value back by it. The SELECT alias
71/// never reaches this name; it is a display name only.
72pub fn computed_group_key_name(index: usize) -> String {
73    format!("group_{index}")
74}
75
76/// The key a finalized group row carries one GROUP BY key under. A column
77/// key keeps its bare column name; a computed key uses
78/// [`computed_group_key_name`].
79pub fn group_key_row_name(key: &SqlExpr, index: usize) -> String {
80    match key {
81        SqlExpr::Column { name, .. } => name.clone(),
82        SqlExpr::Function { .. }
83        | SqlExpr::BinaryOp { .. }
84        | SqlExpr::UnaryOp { .. }
85        | SqlExpr::Cast { .. }
86        | SqlExpr::IsNull { .. }
87        | SqlExpr::Case { .. }
88        | SqlExpr::InList { .. }
89        | SqlExpr::Between { .. }
90        | SqlExpr::Like { .. }
91        | SqlExpr::ArrayLiteral(_)
92        | SqlExpr::Literal(_)
93        | SqlExpr::Subquery(_)
94        | SqlExpr::Wildcard => computed_group_key_name(index),
95    }
96}