nodedb_sql/planner/having.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! HAVING planning: bind the predicate to computed aggregate columns.
4//!
5//! A HAVING predicate is evaluated against finalized group rows, so every
6//! aggregate it mentions must (a) actually be computed and (b) be addressed by
7//! the column name it lands under.
8//!
9//! Left untranslated, `HAVING SUM(amount) > 0` reaches the executor as a
10//! literal `sum(...)` *call* over a group row that has no such column and no
11//! scalar `sum` to apply — every group fails the predicate and the query
12//! returns nothing, for data that plainly satisfies it. And an aggregate named
13//! only in HAVING was never added to the aggregate list, so it was never
14//! computed at all.
15//!
16//! HAVING filters *before* aggregates are renamed to their user aliases, so it
17//! binds to the canonical key.
18
19use sqlparser::ast;
20
21use crate::aggregate_walk::contains_aggregate;
22use crate::error::{Result, SqlError};
23use crate::functions::registry::FunctionRegistry;
24use crate::planner::agg_bind::{BindName, bind_aggregate_calls};
25use crate::planner::agg_naming::aggregate_output_key;
26use crate::planner::select::select_output_aliases;
27use crate::resolver::columns::{ColumnNaming, TableScope};
28use crate::types::{AggregateExpr, Filter};
29
30/// Convert a HAVING clause into filters over finalized group rows.
31///
32/// `aggregates` is extended in place with any aggregate that appears only in
33/// HAVING, so it is computed alongside the projected ones.
34pub fn plan_having(
35 having: &ast::Expr,
36 projection: &[ast::SelectItem],
37 aggregates: &mut Vec<AggregateExpr>,
38 functions: &FunctionRegistry,
39 scope: &TableScope,
40) -> Result<Vec<Filter>> {
41 let rewritten = bind_aggregate_calls(
42 having,
43 projection,
44 aggregates,
45 functions,
46 BindName::Canonical,
47 scope,
48 )?;
49
50 // Any aggregate call still standing is one this rewrite did not reach.
51 // Failing loudly beats handing the executor a predicate that silently
52 // matches no group.
53 if contains_aggregate(&rewritten, functions) {
54 return Err(SqlError::Unsupported {
55 detail: format!(
56 "HAVING predicate shape is not supported: aggregate call in `{having}` could not \
57 be bound to a computed group column"
58 ),
59 });
60 }
61
62 // The rewritten predicate addresses computed group columns: an
63 // aggregate's canonical key, its output alias, or a SELECT-list alias.
64 // Those are the aggregate's output rows. Over a join they key a group
65 // column by its bare name, never by the merged row's `qualifier.column`.
66 let output_naming = match scope.column_naming() {
67 ColumnNaming::JoinRow | ColumnNaming::OutputRow => ColumnNaming::OutputRow,
68 ColumnNaming::AsWritten => ColumnNaming::AsWritten,
69 };
70 let having_scope = scope
71 .with_output_names(
72 aggregates
73 .iter()
74 .map(aggregate_output_key)
75 .chain(aggregates.iter().map(|a| a.alias.clone()))
76 .chain(select_output_aliases(projection)),
77 )
78 .with_column_naming(output_naming);
79 crate::planner::select::convert_where_to_filters(&rewritten, &having_scope)
80}