Skip to main content

uqa_sql/semantics/grouping_sets/
names.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! GROUP BY input-column precedence and output-name fallback after source binding.
8
9use std::borrow::Cow;
10
11use crate::plan::{ProjectionPlan, QueryBlockPlan};
12use crate::routines::RoutineResolution;
13use crate::{RowSchema, SQLError, SQLParam, ScalarExpr};
14
15/// An unqualified input column wins even when its lookup is ambiguous. Output names are considered only for a whole bare name, never inside an expression. `schema` holds the query's own columns over `outer`, the scope of the queries that enclose it, whose columns do not take a name before an output column.
16pub fn resolve_grouping_expression<'a>(
17    routines: &dyn RoutineResolution,
18    expression: &'a ScalarExpr,
19    projections: &'a [ProjectionPlan],
20    schema: &RowSchema,
21    outer: Option<&RowSchema>,
22    params: &[SQLParam],
23) -> Result<Cow<'a, ScalarExpr>, SQLError> {
24    let resolved = resolve_grouping_expression_reference(
25        routines,
26        expression,
27        projections,
28        schema,
29        outer,
30        params,
31    )?;
32    Ok(if std::ptr::eq(resolved, expression) {
33        Cow::Borrowed(expression)
34    } else {
35        Cow::Owned(resolved.clone())
36    })
37}
38
39/// Borrow the original grouped expression or its projection, preserving input-literal identity during prepared analysis.
40pub(crate) fn resolve_grouping_expression_reference<'a>(
41    routines: &dyn RoutineResolution,
42    expression: &'a ScalarExpr,
43    projections: &'a [ProjectionPlan],
44    schema: &RowSchema,
45    outer: Option<&RowSchema>,
46    params: &[SQLParam],
47) -> Result<&'a ScalarExpr, SQLError> {
48    let mut resolved = expression;
49    if let ScalarExpr::Column(name) = expression {
50        if !is_input_column(schema, outer, name) {
51            let mut matches = projections
52                .iter()
53                .filter(|projection| crate::semantics::projection_label_at(projection) == *name);
54            if let Some(first) = matches.next() {
55                resolved = &first.expr;
56                let mut identity = None;
57                for candidate in matches {
58                    let first_identity = match &identity {
59                        Some(identity) => identity,
60                        None => identity.insert(super::expression_identity(
61                            routines, resolved, schema, params,
62                        )?),
63                    };
64                    if *first_identity
65                        != super::expression_identity(routines, &candidate.expr, schema, params)?
66                    {
67                        return Err(SQLError::Routine {
68                            sqlstate: "42702".into(),
69                            message: format!("GROUP BY \"{name}\" is ambiguous"),
70                        });
71                    }
72                }
73            }
74        }
75    }
76    if crate::semantics::aggregates::contains_aggregate(
77        &|name: &str| routines.has_registered_aggregate_function(name),
78        resolved,
79    ) {
80        return Err(SQLError::Routine {
81            sqlstate: "42803".into(),
82            message: "aggregate functions are not allowed in GROUP BY".into(),
83        });
84    }
85    if resolved.contains_window() {
86        return Err(SQLError::Routine {
87            sqlstate: "42P20".into(),
88            message: "window functions are not allowed in GROUP BY".into(),
89        });
90    }
91    Ok(resolved)
92}
93
94/// Whether `name` is a column of the query's own sources, ambiguous or not, in `schema`, which holds those columns over `outer`: `findTargetlistEntrySQL92` looks for a GROUP BY name among the columns of the query's own level only.
95fn is_input_column(schema: &RowSchema, outer: Option<&RowSchema>, name: &str) -> bool {
96    if !schema.has_unqualified_column(name) {
97        return false;
98    }
99    let Some(outer) = outer else {
100        return true;
101    };
102    let own_width = schema
103        .physical_width()
104        .saturating_sub(outer.physical_width());
105    schema.column_slot(name).is_none_or(|slot| slot < own_width)
106}
107
108/// Bind grouping expressions before storing a query or executing its aggregation. Returns whether an output name was replaced.
109pub fn bind_grouping_names(
110    routines: &dyn RoutineResolution,
111    statement: &mut QueryBlockPlan,
112    schema: &RowSchema,
113    outer: Option<&RowSchema>,
114    params: &[SQLParam],
115) -> Result<bool, SQLError> {
116    let mut changed = false;
117    for expression in statement
118        .group_by
119        .iter_mut()
120        .chain(statement.grouping_sets.iter_mut().flatten())
121    {
122        if let Cow::Owned(resolved) = resolve_grouping_expression(
123            routines,
124            expression,
125            &statement.projections,
126            schema,
127            outer,
128            params,
129        )? {
130            *expression = resolved;
131            changed = true;
132        }
133    }
134    Ok(changed)
135}