Skip to main content

uqa_sql/semantics/grouping_sets/
validation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Validate group inputs before constant folding or row production can erase their analyzed identity.
8
9use super::{expression_identity, resolve_grouping_expression};
10use crate::plan::QueryBlockPlan;
11use crate::routines::RoutineResolution;
12use crate::semantics::aggregates::{has_aggregate, is_aggregate};
13use crate::{RowSchema, SQLError, SQLParam, ScalarExpr};
14
15pub fn validate_grouped_expressions(
16    routines: &dyn RoutineResolution,
17    statement: &QueryBlockPlan,
18    schema: &RowSchema,
19    params: &[SQLParam],
20) -> Result<(), SQLError> {
21    let aggregates = |name: &str| routines.has_registered_aggregate_function(name);
22    if statement.group_by.is_empty()
23        && statement.grouping_sets.is_empty()
24        && statement.having.is_none()
25        && !has_aggregate(&aggregates, &statement.projections)
26    {
27        return Ok(());
28    }
29    let groups = statement
30        .group_by
31        .iter()
32        .chain(statement.grouping_sets.iter().flatten())
33        .map(|expression| {
34            let expression = resolve_grouping_expression(
35                routines,
36                expression,
37                &statement.projections,
38                schema,
39                params,
40            )?;
41            expression_identity(routines, &expression, schema, params)
42        })
43        .collect::<Result<Vec<_>, _>>()?;
44    let projections =
45        crate::semantics::expand_bound_projection_stars(&statement.projections, schema)?;
46    for expression in projections
47        .iter()
48        .map(|projection| (&projection.expr, false))
49        .chain(
50            statement
51                .having
52                .iter()
53                .map(|expression| (expression, false)),
54        )
55        .chain(statement.order_by.iter().map(|order| (&order.expr, true)))
56        .chain(
57            statement
58                .distinct_on
59                .iter()
60                .map(|expression| (expression, true)),
61        )
62    {
63        let (expression, output_names) = expression;
64        // Ordering and DISTINCT ON may select an already checked output expression by name.
65        if output_names
66            && matches!(expression, ScalarExpr::Column(name) if projections.iter().any(|projection| crate::semantics::projection_label_at(projection) == *name))
67        {
68            continue;
69        }
70        expression.try_visit(&mut |part| {
71            if is_aggregate(&aggregates, part)
72                || groups.contains(&expression_identity(routines, part, schema, params)?)
73            {
74                return Ok(false);
75            }
76            let position = match part {
77                ScalarExpr::Column(column) => schema.unqualified_position(column),
78                ScalarExpr::QualifiedColumn { qualifier, column } => {
79                    schema.qualified_position(qualifier, column)
80                }
81                ScalarExpr::Position(position) => Some(*position),
82                _ => None,
83            };
84            if let Some(identity) = position.and_then(|position| schema.identity(position)) {
85                let name = identity.qualifier().map_or_else(
86                    || identity.column().to_owned(),
87                    |qualifier| format!("{qualifier}.{}", identity.column()),
88                );
89                return Err(SQLError::Routine {
90                    sqlstate: "42803".into(),
91                    message: format!("column \"{name}\" must appear in the GROUP BY clause or be used in an aggregate function"),
92                });
93            }
94            Ok(true)
95        })?;
96    }
97    Ok(())
98}