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    let mut records = Vec::new();
77    for (position, projection) in projections.iter().enumerate() {
78        let expansion_schema = match projection.expr {
79            ScalarExpr::QualifiedStar(_) => expression_schema,
80            _ => star_schema,
81        };
82        if let Some(star_columns) = projection_star_columns(&projection.expr, expansion_schema)? {
83            records.extend(
84                super::record_fields::star_fields(&projection.expr, expansion_schema)
85                    .unwrap_or_default(),
86            );
87            for (column, ty) in star_columns {
88                columns.push(column);
89                types.push(ty);
90            }
91            continue;
92        }
93        columns.push(labels[position].clone());
94        types.push(scope.bind_expression_type(
95            routines,
96            &projection.expr,
97            expression_schema,
98            subqueries,
99            params,
100        )?);
101        records.push(
102            if types
103                .last()
104                .is_some_and(|ty| matches!(ty, Some(ColumnType::Record | ColumnType::Composite(_))))
105            {
106                scope.bind_record_fields(
107                    routines,
108                    &projection.expr,
109                    expression_schema,
110                    subqueries,
111                    params,
112                )?
113            } else {
114                None
115            },
116        );
117    }
118    Ok(RowSchema::with_types(columns, types).with_record_fields(
119        records
120            .into_iter()
121            .enumerate()
122            .filter_map(|(index, fields)| fields.map(|fields| (index, fields))),
123    ))
124}
125
126/// 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.
127pub fn validate_query_block_expression_types(
128    routines: &dyn RoutineResolution,
129    statement: &QueryBlockPlan,
130    schema: &RowSchema,
131    params: &[SQLParam],
132    ctes: &BindingContext,
133) -> Result<(), SQLError> {
134    let scalar_subquery_types = statement
135        .subqueries
136        .iter()
137        .map(|plan| {
138            bind_query_plan_schema(routines, plan, params, ctes, Some(schema))
139                .map(|output| output.column_type(0).cloned())
140        })
141        .collect::<Result<Vec<_>, _>>()?;
142    let resolver = QueryFunctionTypeResolver {
143        routines,
144        scalar_subquery_types: Some(scalar_subquery_types),
145        defer_routine_namespace_errors: true,
146    };
147    for expression in statement
148        .projections
149        .iter()
150        .map(|projection| &projection.expr)
151        .chain(statement.group_by.iter())
152        .chain(statement.grouping_sets.iter().flatten())
153        .chain(statement.order_by.iter().map(|order| &order.expr))
154        .chain(statement.distinct_on.iter())
155        .chain(statement.r#where.iter())
156        .chain(statement.having.iter())
157        .chain(statement.limit.iter())
158        .chain(statement.offset.iter())
159    {
160        crate::scalar_type_with_resolver(expression, schema, params, &resolver)?;
161    }
162    for expression in statement
163        .group_by
164        .iter()
165        .chain(statement.grouping_sets.iter().flatten())
166        .chain(statement.distinct_on.iter())
167    {
168        if let Some(ty) = crate::scalar_type_with_resolver(expression, schema, params, &resolver)? {
169            crate::require_equality_operator(&ty)?;
170        }
171    }
172    for order in &statement.order_by {
173        if let Some(ty) = crate::scalar_type_with_resolver(&order.expr, schema, params, &resolver)?
174        {
175            crate::require_ordering_operator(&ty)?;
176        }
177    }
178    Ok(())
179}
180
181/// 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.
182pub fn validate_query_block_references(
183    routines: &dyn RoutineResolution,
184    statement: &QueryBlockPlan,
185    schema: &RowSchema,
186    params: &[SQLParam],
187    ctes: &BindingContext,
188    outer: Option<&RowSchema>,
189) -> Result<(), SQLError> {
190    let output = analyze_projection_output_schema(
191        routines,
192        &statement.projections,
193        schema,
194        schema,
195        &statement.subqueries,
196        params,
197        ctes,
198    )?;
199    if statement.distinct && statement.distinct_on.is_empty() {
200        for ty in output.column_types().iter().flatten() {
201            crate::require_equality_operator(ty)?;
202        }
203    }
204    SchemaScope::for_analysis(ctes)?
205        .validate_query_block_clauses(routines, statement, schema, &output, params, outer)
206}
207
208pub(super) fn projection_star_columns(
209    expression: &ScalarExpr,
210    schema: &RowSchema,
211) -> Result<Option<Vec<ProjectionStarColumn>>, SQLError> {
212    match expression {
213        ScalarExpr::Star => Ok(Some(
214            schema
215                .columns()
216                .iter()
217                .enumerate()
218                .map(|(position, column)| {
219                    (
220                        schema.public_name(position).unwrap_or(column).to_string(),
221                        schema.column_type(position).cloned(),
222                    )
223                })
224                .collect(),
225        )),
226        ScalarExpr::QualifiedStar(qualifier) => {
227            let columns = schema
228                .qualified_star_layout(qualifier)
229                .into_iter()
230                .map(|(column, _, ty)| (column, ty))
231                .collect::<Vec<_>>();
232            if columns.is_empty() {
233                return Err(SQLError::UnknownTable(qualifier.clone()));
234            }
235            Ok(Some(columns))
236        }
237        _ => Ok(None),
238    }
239}
240
241pub(super) fn rename_schema(
242    schema: &RowSchema,
243    aliases: &[String],
244    qualifier: Option<&str>,
245) -> RowSchema {
246    let columns = schema
247        .columns()
248        .iter()
249        .enumerate()
250        .map(|(position, column)| {
251            aliases
252                .get(position)
253                .cloned()
254                .unwrap_or_else(|| schema.public_name(position).unwrap_or(column).to_string())
255        })
256        .collect();
257    let renamed = match qualifier {
258        Some(qualifier) => {
259            RowSchema::with_qualified_types(qualifier, columns, schema.column_types().to_vec())
260        }
261        None => RowSchema::with_types(columns, schema.column_types().to_vec()),
262    };
263    let renamed = renamed.with_record_fields_from(schema);
264    let renamed = if schema.columns_are_open(None) {
265        RowSchema::with_open_columns(&renamed, qualifier)
266    } else {
267        renamed
268    };
269    let mut hidden = Vec::new();
270    let mut conflicting = Vec::new();
271    for (identity, ty) in schema.typed_virtual_identities() {
272        let conflicts = match identity.qualifier() {
273            Some(source) => schema.qualified_column_is_ambiguous(source, identity.column()),
274            None => schema.column_is_ambiguous(identity.column()),
275        };
276        let mapped = qualifier.map_or_else(
277            || vec![identity.clone()],
278            |qualifier| {
279                vec![
280                    crate::ColumnIdentity::unqualified(identity.column()),
281                    crate::ColumnIdentity::qualified(qualifier, identity.column()),
282                ]
283            },
284        );
285        for identity in mapped {
286            if conflicts {
287                conflicting.push((identity, ty.cloned()));
288            } else {
289                hidden.push((identity, ty.cloned()));
290            }
291        }
292    }
293    let renamed = RowSchema::with_typed_virtual_identities(&renamed, &hidden);
294    RowSchema::with_typed_conflicting_virtual_identities(&renamed, &conflicting)
295}