Skip to main content

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}