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>, Option<u32>);
21
22pub(super) struct ProjectionMetadata {
23    pub ty: Option<ColumnType>,
24    pub collation: Option<u32>,
25    pub record: Option<crate::schema::RecordMetadata>,
26}
27
28/// Keep only selections that differ from their output type's default while the output schema is being assembled.
29#[derive(Default)]
30pub(super) struct OutputCollations(Vec<(usize, u32)>);
31
32impl OutputCollations {
33    pub(super) fn push(&mut self, position: usize, ty: Option<&ColumnType>, oid: Option<u32>) {
34        if let Some((ty, oid)) = ty.zip(oid) {
35            if crate::schema::composites::type_is_collatable(ty)
36                && crate::catalog::type_metadata::pg_type_collation_oid(ty) != i64::from(oid)
37            {
38                self.0.push((position, oid));
39            }
40        }
41    }
42
43    pub(super) fn apply(self, mut schema: RowSchema) -> RowSchema {
44        for (position, oid) in self.0 {
45            schema = schema
46                .with_collation_override(crate::schema::CollationBinding::Column(position), oid)
47                .expect("selected collation belongs to its typed output");
48        }
49        schema
50    }
51}
52
53/// 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.
54pub fn bind_projection_output_schema(
55    routines: &dyn RoutineResolution,
56    projections: &[ProjectionPlan],
57    expression_schema: &RowSchema,
58    star_schema: &RowSchema,
59    subqueries: &[QueryPlan],
60    params: &[SQLParam],
61    ctes: &BindingContext,
62) -> Result<RowSchema, SQLError> {
63    projection_output_schema(
64        SchemaScope::from_context(ctes)?,
65        routines,
66        projections,
67        expression_schema,
68        star_schema,
69        subqueries,
70        params,
71    )
72}
73
74/// Derive and validate a projection's exact output row type without executing it.
75pub fn analyze_projection_output_schema(
76    routines: &dyn RoutineResolution,
77    projections: &[ProjectionPlan],
78    expression_schema: &RowSchema,
79    star_schema: &RowSchema,
80    subqueries: &[QueryPlan],
81    params: &[SQLParam],
82    ctes: &BindingContext,
83) -> Result<RowSchema, SQLError> {
84    projection_output_schema(
85        SchemaScope::for_analysis(ctes)?,
86        routines,
87        projections,
88        expression_schema,
89        star_schema,
90        subqueries,
91        params,
92    )
93}
94
95fn projection_output_schema(
96    mut scope: SchemaScope,
97    routines: &dyn RoutineResolution,
98    projections: &[ProjectionPlan],
99    expression_schema: &RowSchema,
100    star_schema: &RowSchema,
101    subqueries: &[QueryPlan],
102    params: &[SQLParam],
103) -> Result<RowSchema, SQLError> {
104    let labels = projection_columns(projections);
105    let mut columns = Vec::new();
106    let mut types = Vec::new();
107    let mut records = Vec::new();
108    let mut collations = OutputCollations::default();
109    for (position, projection) in projections.iter().enumerate() {
110        let expansion_schema = match projection.expr {
111            ScalarExpr::QualifiedStar(_) => expression_schema,
112            _ => star_schema,
113        };
114        if let Some(star_columns) = projection_star_columns(&projection.expr, expansion_schema)? {
115            records.extend(
116                super::record_fields::star_metadata(&projection.expr, expansion_schema)
117                    .unwrap_or_default(),
118            );
119            for (column, ty, collation) in star_columns {
120                collations.push(types.len(), ty.as_ref(), collation);
121                columns.push(column);
122                types.push(ty);
123            }
124            continue;
125        }
126        columns.push(labels[position].clone());
127        let metadata = scope.bind_projection_metadata(
128            routines,
129            &projection.expr,
130            expression_schema,
131            subqueries,
132            params,
133        )?;
134        collations.push(types.len(), metadata.ty.as_ref(), metadata.collation);
135        types.push(metadata.ty);
136        records.push(metadata.record);
137    }
138    for projection in projections {
139        scope.collations.validate_expression(&projection.expr)?;
140    }
141    Ok(collations
142        .apply(RowSchema::with_types(columns, types))
143        .with_record_metadata(
144            records
145                .into_iter()
146                .enumerate()
147                .filter_map(|(index, fields)| fields.map(|fields| (index, fields))),
148        ))
149}
150
151/// 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.
152pub fn validate_query_block_expression_types(
153    routines: &dyn RoutineResolution,
154    statement: &QueryBlockPlan,
155    schema: &RowSchema,
156    params: &[SQLParam],
157    ctes: &BindingContext,
158) -> Result<(), SQLError> {
159    let scalar_subquery_types = statement
160        .subqueries
161        .iter()
162        .map(|plan| {
163            bind_query_plan_schema(routines, plan, params, ctes, Some(schema))
164                .map(|output| output.column_type(0).cloned())
165        })
166        .collect::<Result<Vec<_>, _>>()?;
167    let resolver = QueryFunctionTypeResolver {
168        routines,
169        scalar_subquery_types: Some(scalar_subquery_types),
170        defer_routine_namespace_errors: true,
171    };
172    for expression in statement
173        .projections
174        .iter()
175        .map(|projection| &projection.expr)
176        .chain(statement.group_by.iter())
177        .chain(statement.grouping_sets.iter().flatten())
178        .chain(statement.order_by.iter().map(|order| &order.expr))
179        .chain(statement.distinct_on.iter())
180        .chain(statement.r#where.iter())
181        .chain(statement.having.iter())
182        .chain(statement.limit.iter())
183        .chain(statement.offset.iter())
184    {
185        crate::scalar_type_with_resolver(expression, schema, params, &resolver)?;
186    }
187    for expression in statement
188        .group_by
189        .iter()
190        .chain(statement.grouping_sets.iter().flatten())
191        .chain(statement.distinct_on.iter())
192    {
193        if let Some(ty) = crate::scalar_type_with_resolver(expression, schema, params, &resolver)? {
194            crate::type_resolution::ComparisonSupport::Equality
195                .require_in_catalog(&ty, routines)?;
196        }
197    }
198    for order in &statement.order_by {
199        if let Some(ty) = crate::scalar_type_with_resolver(&order.expr, schema, params, &resolver)?
200        {
201            crate::type_resolution::ComparisonSupport::Ordering
202                .require_in_catalog(&ty, routines)?;
203        }
204    }
205    Ok(())
206}
207
208/// 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. `schema` holds the block's columns over `outer`, the scope of the queries that enclose the block.
209pub fn validate_query_block_references(
210    routines: &dyn RoutineResolution,
211    statement: &QueryBlockPlan,
212    schema: &RowSchema,
213    params: &[SQLParam],
214    ctes: &BindingContext,
215    outer: Option<&RowSchema>,
216) -> Result<(), SQLError> {
217    let output = analyze_projection_output_schema(
218        routines,
219        &statement.projections,
220        schema,
221        schema,
222        &statement.subqueries,
223        params,
224        ctes,
225    )?;
226    if statement.distinct && statement.distinct_on.is_empty() {
227        for ty in output.column_types().iter().flatten() {
228            crate::type_resolution::ComparisonSupport::Equality.require_in_catalog(ty, routines)?;
229        }
230    }
231    SchemaScope::for_analysis(ctes)?
232        .validate_query_block_clauses(routines, statement, schema, &output, params, outer)
233}
234
235pub(super) fn projection_star_columns(
236    expression: &ScalarExpr,
237    schema: &RowSchema,
238) -> Result<Option<Vec<ProjectionStarColumn>>, SQLError> {
239    match expression {
240        ScalarExpr::Star => Ok(Some(
241            schema
242                .columns()
243                .iter()
244                .enumerate()
245                .map(|(position, column)| {
246                    (
247                        schema.public_name(position).unwrap_or(column).to_string(),
248                        schema.column_type(position).cloned(),
249                        schema.column_collation(position),
250                    )
251                })
252                .collect(),
253        )),
254        ScalarExpr::QualifiedStar(qualifier) => {
255            let columns = schema
256                .qualified_star_position_layout(qualifier)
257                .into_iter()
258                .map(|(column, logical, _, ty)| {
259                    let collation = logical.map_or_else(
260                        || schema.qualified_collation(qualifier, &column),
261                        |position| schema.column_collation(position),
262                    );
263                    (column, ty, collation)
264                })
265                .collect::<Vec<_>>();
266            if columns.is_empty() {
267                return Err(SQLError::UnknownTable(qualifier.clone()));
268            }
269            Ok(Some(columns))
270        }
271        _ => Ok(None),
272    }
273}
274
275pub(super) fn rename_schema(
276    schema: &RowSchema,
277    aliases: &[String],
278    qualifier: Option<&str>,
279) -> RowSchema {
280    let columns = schema
281        .columns()
282        .iter()
283        .enumerate()
284        .map(|(position, column)| {
285            aliases
286                .get(position)
287                .cloned()
288                .unwrap_or_else(|| schema.public_name(position).unwrap_or(column).to_string())
289        })
290        .collect();
291    let renamed = match qualifier {
292        Some(qualifier) => {
293            RowSchema::with_qualified_types(qualifier, columns, schema.column_types().to_vec())
294        }
295        None => RowSchema::with_types(columns, schema.column_types().to_vec()),
296    };
297    let renamed = renamed
298        .with_column_collations((0..schema.len()).map(|position| schema.column_collation(position)))
299        .with_record_fields_from(schema);
300    let renamed = if schema.columns_are_open(None) {
301        RowSchema::with_open_columns(&renamed, qualifier)
302    } else {
303        renamed
304    };
305    let mut hidden = Vec::new();
306    let mut conflicting = Vec::new();
307    for (identity, ty) in schema.typed_virtual_identities() {
308        let conflicts = match identity.qualifier() {
309            Some(source) => schema.qualified_column_is_ambiguous(source, identity.column()),
310            None => schema.column_is_ambiguous(identity.column()),
311        };
312        let mapped = qualifier.map_or_else(
313            || vec![identity.clone()],
314            |qualifier| {
315                vec![
316                    crate::ColumnIdentity::unqualified(identity.column()),
317                    crate::ColumnIdentity::qualified(qualifier, identity.column()),
318                ]
319            },
320        );
321        for identity in mapped {
322            if conflicts {
323                conflicting.push((identity, ty.cloned()));
324            } else {
325                hidden.push((identity, ty.cloned()));
326            }
327        }
328    }
329    let renamed = RowSchema::with_typed_virtual_identities(&renamed, &hidden);
330    RowSchema::with_typed_conflicting_virtual_identities(&renamed, &conflicting)
331}