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.
16pub fn resolve_grouping_expression<'a>(
17    routines: &dyn RoutineResolution,
18    expression: &'a ScalarExpr,
19    projections: &'a [ProjectionPlan],
20    schema: &RowSchema,
21    params: &[SQLParam],
22) -> Result<Cow<'a, ScalarExpr>, SQLError> {
23    let mut resolved = expression;
24    if let ScalarExpr::Column(name) = expression {
25        if !schema.has_unqualified_column(name) {
26            let mut matches = projections
27                .iter()
28                .filter(|projection| crate::semantics::projection_label_at(projection) == *name);
29            if let Some(first) = matches.next() {
30                resolved = &first.expr;
31                let mut identity = None;
32                for candidate in matches {
33                    let first_identity = match &identity {
34                        Some(identity) => identity,
35                        None => identity.insert(super::expression_identity(
36                            routines, resolved, schema, params,
37                        )?),
38                    };
39                    if *first_identity
40                        != super::expression_identity(routines, &candidate.expr, schema, params)?
41                    {
42                        return Err(SQLError::Routine {
43                            sqlstate: "42702".into(),
44                            message: format!("GROUP BY \"{name}\" is ambiguous"),
45                        });
46                    }
47                }
48            }
49        }
50    }
51    if crate::semantics::aggregates::contains_aggregate(
52        &|name: &str| routines.has_registered_aggregate_function(name),
53        resolved,
54    ) {
55        return Err(SQLError::Routine {
56            sqlstate: "42803".into(),
57            message: "aggregate functions are not allowed in GROUP BY".into(),
58        });
59    }
60    if resolved.contains_window() {
61        return Err(SQLError::Routine {
62            sqlstate: "42P20".into(),
63            message: "window functions are not allowed in GROUP BY".into(),
64        });
65    }
66    Ok(if std::ptr::eq(resolved, expression) {
67        Cow::Borrowed(expression)
68    } else {
69        Cow::Owned(resolved.clone())
70    })
71}
72
73/// Bind grouping expressions before storing a query or executing its aggregation. Returns whether an output name was replaced.
74pub fn bind_grouping_names(
75    routines: &dyn RoutineResolution,
76    statement: &mut QueryBlockPlan,
77    schema: &RowSchema,
78    params: &[SQLParam],
79) -> Result<bool, SQLError> {
80    let mut changed = false;
81    for expression in statement
82        .group_by
83        .iter_mut()
84        .chain(statement.grouping_sets.iter_mut().flatten())
85    {
86        if let Cow::Owned(resolved) = resolve_grouping_expression(
87            routines,
88            expression,
89            &statement.projections,
90            schema,
91            params,
92        )? {
93            *expression = resolved;
94            changed = true;
95        }
96    }
97    Ok(changed)
98}