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::{contains_aggregate, has_aggregate, is_aggregate};
13use crate::{RowSchema, SQLError, SQLParam, ScalarExpr};
14
15/// Validate the grouped expressions of a query whose own columns `schema` holds over `outer`, the scope of the queries that enclose it.
16pub fn validate_grouped_expressions(
17    routines: &dyn RoutineResolution,
18    statement: &QueryBlockPlan,
19    schema: &RowSchema,
20    outer: Option<&RowSchema>,
21    params: &[SQLParam],
22) -> Result<(), SQLError> {
23    let aggregates = |name: &str| routines.has_registered_aggregate_function(name);
24    if statement.group_by.is_empty()
25        && statement.grouping_sets.is_empty()
26        && statement.having.is_none()
27        && !has_aggregate(&aggregates, &statement.projections)
28        && !statement
29            .windows
30            .iter()
31            .flat_map(|window| window.spec.expressions())
32            .any(|expression| contains_aggregate(&aggregates, expression))
33    {
34        return Ok(());
35    }
36    let groups = statement
37        .group_by
38        .iter()
39        .chain(statement.grouping_sets.iter().flatten())
40        .map(|expression| {
41            let expression = resolve_grouping_expression(
42                routines,
43                expression,
44                &statement.projections,
45                schema,
46                outer,
47                params,
48            )?;
49            expression_identity(routines, &expression, schema, params)
50        })
51        .collect::<Result<Vec<_>, _>>()?;
52    let projections =
53        crate::semantics::expand_bound_projection_stars(&statement.projections, schema)?;
54    for expression in projections
55        .iter()
56        .map(|projection| (&projection.expr, false))
57        .chain(
58            statement
59                .having
60                .iter()
61                .map(|expression| (expression, false)),
62        )
63        .chain(statement.order_by.iter().map(|order| (&order.expr, true)))
64        .chain(
65            statement
66                .windows
67                .iter()
68                .flat_map(|window| window.spec.expressions())
69                .map(|expression| (expression, false)),
70        )
71        .chain(
72            statement
73                .distinct_on
74                .iter()
75                .map(|expression| (expression, true)),
76        )
77    {
78        let (expression, output_names) = expression;
79        // Ordering and DISTINCT ON may select an already checked output expression by name.
80        if output_names
81            && matches!(expression, ScalarExpr::Column(name) if projections.iter().any(|projection| crate::semantics::projection_label_at(projection) == *name))
82        {
83            continue;
84        }
85        expression.try_visit(&mut |part| {
86            if is_aggregate(&aggregates, part)
87                || groups.contains(&expression_identity(routines, part, schema, params)?)
88            {
89                return Ok(false);
90            }
91            let position = match part {
92                ScalarExpr::Column(column) => schema.unqualified_position(column),
93                ScalarExpr::QualifiedColumn { qualifier, column } => {
94                    schema.qualified_position(qualifier, column)
95                }
96                ScalarExpr::Position(position) => Some(*position),
97                _ => None,
98            };
99            if let Some(identity) = position.and_then(|position| schema.identity(position)) {
100                let name = identity.qualifier().map_or_else(
101                    || identity.column().to_owned(),
102                    |qualifier| format!("{qualifier}.{}", identity.column()),
103                );
104                return Err(SQLError::Routine {
105                    sqlstate: "42803".into(),
106                    message: format!("column \"{name}\" must appear in the GROUP BY clause or be used in an aggregate function"),
107                });
108            }
109            Ok(true)
110        })?;
111    }
112    Ok(())
113}