Skip to main content

uqa_sql/binding/
projection.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Projection row-type binding and reference validation.
8
9use crate::ast::ColumnType;
10use crate::plan::{ProjectionPlan, QueryBlockPlan, QueryPlan};
11use crate::RowSchema;
12use crate::{SQLError, SQLParam};
13
14use super::{
15    bind_query_plan_schema, projection_columns, BindingContext, QueryFunctionTypeResolver,
16    ScalarExpr, SchemaScope,
17};
18use crate::routines::RoutineResolution;
19
20type ProjectionStarColumn = (String, Option<ColumnType>);
21
22/// Bind a projection against an already-declared input schema. `star_schema` identifies the relation expanded by bare `*`; `expression_schema` may also contain joined sources and hidden lookup aliases used by scalar expressions.
23pub fn bind_projection_output_schema(
24    routines: &dyn RoutineResolution,
25    projections: &[ProjectionPlan],
26    expression_schema: &RowSchema,
27    star_schema: &RowSchema,
28    subqueries: &[QueryPlan],
29    params: &[SQLParam],
30    ctes: &BindingContext,
31) -> Result<RowSchema, SQLError> {
32    projection_output_schema(
33        SchemaScope::from_context(ctes)?,
34        routines,
35        projections,
36        expression_schema,
37        star_schema,
38        subqueries,
39        params,
40    )
41}
42
43/// Derive and validate a projection's exact output row type without executing it.
44pub fn analyze_projection_output_schema(
45    routines: &dyn RoutineResolution,
46    projections: &[ProjectionPlan],
47    expression_schema: &RowSchema,
48    star_schema: &RowSchema,
49    subqueries: &[QueryPlan],
50    params: &[SQLParam],
51    ctes: &BindingContext,
52) -> Result<RowSchema, SQLError> {
53    projection_output_schema(
54        SchemaScope::for_analysis(ctes)?,
55        routines,
56        projections,
57        expression_schema,
58        star_schema,
59        subqueries,
60        params,
61    )
62}
63
64fn projection_output_schema(
65    mut scope: SchemaScope,
66    routines: &dyn RoutineResolution,
67    projections: &[ProjectionPlan],
68    expression_schema: &RowSchema,
69    star_schema: &RowSchema,
70    subqueries: &[QueryPlan],
71    params: &[SQLParam],
72) -> Result<RowSchema, SQLError> {
73    let labels = projection_columns(projections);
74    let mut columns = Vec::new();
75    let mut types = Vec::new();
76    for (position, projection) in projections.iter().enumerate() {
77        let expansion_schema = match projection.expr {
78            ScalarExpr::QualifiedStar(_) => expression_schema,
79            _ => star_schema,
80        };
81        if let Some(star_columns) = projection_star_columns(&projection.expr, expansion_schema)? {
82            for (column, ty) in star_columns {
83                columns.push(column);
84                types.push(ty);
85            }
86            continue;
87        }
88        columns.push(labels[position].clone());
89        types.push(scope.bind_expression_type(
90            routines,
91            &projection.expr,
92            expression_schema,
93            subqueries,
94            params,
95            Some(expression_schema),
96        )?);
97    }
98    Ok(RowSchema::with_types(columns, types))
99}
100
101/// Validate every scalar expression in a query block while the physical input still carries declared SQL types. This must precede polymorphic rewrites such as `pg_typeof`, because an invalid common type is an error, not an `unknown` result.
102pub fn validate_query_block_expression_types(
103    routines: &dyn RoutineResolution,
104    statement: &QueryBlockPlan,
105    schema: &RowSchema,
106    params: &[SQLParam],
107    ctes: &BindingContext,
108) -> Result<(), SQLError> {
109    let scalar_subquery_types = statement
110        .subqueries
111        .iter()
112        .map(|plan| {
113            bind_query_plan_schema(routines, plan, params, ctes, Some(schema))
114                .map(|output| output.column_type(0).cloned())
115        })
116        .collect::<Result<Vec<_>, _>>()?;
117    let resolver = QueryFunctionTypeResolver {
118        routines,
119        scalar_subquery_types: Some(scalar_subquery_types),
120        defer_routine_namespace_errors: true,
121    };
122    for expression in statement
123        .projections
124        .iter()
125        .map(|projection| &projection.expr)
126        .chain(statement.group_by.iter())
127        .chain(statement.grouping_sets.iter().flatten())
128        .chain(statement.order_by.iter().map(|order| &order.expr))
129        .chain(statement.distinct_on.iter())
130        .chain(statement.r#where.iter())
131        .chain(statement.having.iter())
132        .chain(statement.limit.iter())
133        .chain(statement.offset.iter())
134    {
135        crate::scalar_type_with_resolver(expression, schema, params, &resolver)?;
136    }
137    for expression in statement
138        .group_by
139        .iter()
140        .chain(statement.grouping_sets.iter().flatten())
141        .chain(statement.distinct_on.iter())
142    {
143        if let Some(ty) = crate::scalar_type_with_resolver(expression, schema, params, &resolver)? {
144            crate::require_equality_operator(&ty)?;
145        }
146    }
147    for order in &statement.order_by {
148        if let Some(ty) = crate::scalar_type_with_resolver(&order.expr, schema, params, &resolver)?
149        {
150            crate::require_ordering_operator(&ty)?;
151        }
152    }
153    Ok(())
154}
155
156/// Validate every query-block reference only after the caller has the authoritative source schema. This preserves registered table-function row shapes and checks recursive argument references before definitive routine namespace lookup.
157pub fn validate_query_block_references(
158    routines: &dyn RoutineResolution,
159    statement: &QueryBlockPlan,
160    schema: &RowSchema,
161    params: &[SQLParam],
162    ctes: &BindingContext,
163) -> Result<(), SQLError> {
164    let output = analyze_projection_output_schema(
165        routines,
166        &statement.projections,
167        schema,
168        schema,
169        &statement.subqueries,
170        params,
171        ctes,
172    )?;
173    if statement.distinct && statement.distinct_on.is_empty() {
174        for ty in output.column_types().iter().flatten() {
175            crate::require_equality_operator(ty)?;
176        }
177    }
178    SchemaScope::for_analysis(ctes)?
179        .validate_query_block_clauses(routines, statement, schema, &output, params)
180}
181
182pub(super) fn projection_star_columns(
183    expression: &ScalarExpr,
184    schema: &RowSchema,
185) -> Result<Option<Vec<ProjectionStarColumn>>, SQLError> {
186    match expression {
187        ScalarExpr::Star => Ok(Some(
188            schema
189                .columns()
190                .iter()
191                .enumerate()
192                .map(|(position, column)| {
193                    (
194                        schema.public_name(position).unwrap_or(column).to_string(),
195                        schema.column_type(position).cloned(),
196                    )
197                })
198                .collect(),
199        )),
200        ScalarExpr::QualifiedStar(qualifier) => {
201            let columns = schema
202                .qualified_star_layout(qualifier)
203                .into_iter()
204                .map(|(column, _, ty)| (column, ty))
205                .collect::<Vec<_>>();
206            if columns.is_empty() {
207                return Err(SQLError::UnknownTable(qualifier.clone()));
208            }
209            Ok(Some(columns))
210        }
211        _ => Ok(None),
212    }
213}
214
215pub(super) fn rename_schema(
216    schema: &RowSchema,
217    aliases: &[String],
218    qualifier: Option<&str>,
219) -> RowSchema {
220    let columns = schema
221        .columns()
222        .iter()
223        .enumerate()
224        .map(|(position, column)| {
225            aliases
226                .get(position)
227                .cloned()
228                .unwrap_or_else(|| schema.public_name(position).unwrap_or(column).to_string())
229        })
230        .collect();
231    let renamed = match qualifier {
232        Some(qualifier) => {
233            RowSchema::with_qualified_types(qualifier, columns, schema.column_types().to_vec())
234        }
235        None => RowSchema::with_types(columns, schema.column_types().to_vec()),
236    };
237    let renamed = if schema.columns_are_open(None) {
238        RowSchema::with_open_columns(&renamed, qualifier)
239    } else {
240        renamed
241    };
242    let mut hidden = Vec::new();
243    let mut conflicting = Vec::new();
244    for (identity, ty) in schema.typed_virtual_identities() {
245        let conflicts = match identity.qualifier() {
246            Some(source) => schema.qualified_column_is_ambiguous(source, identity.column()),
247            None => schema.column_is_ambiguous(identity.column()),
248        };
249        let mapped = qualifier.map_or_else(
250            || vec![identity.clone()],
251            |qualifier| {
252                vec![
253                    crate::ColumnIdentity::unqualified(identity.column()),
254                    crate::ColumnIdentity::qualified(qualifier, identity.column()),
255                ]
256            },
257        );
258        for identity in mapped {
259            if conflicts {
260                conflicting.push((identity, ty.cloned()));
261            } else {
262                hidden.push((identity, ty.cloned()));
263            }
264        }
265    }
266    let renamed = RowSchema::with_typed_virtual_identities(&renamed, &hidden);
267    RowSchema::with_typed_conflicting_virtual_identities(&renamed, &conflicting)
268}