Skip to main content

uqa_sql/binding/
mod.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Static relational row-type binding.
8//!
9//! This module derives source and query output schemas from plans, catalog
10//! declarations, and already-bound CTE schemas. It never executes a query or
11//! samples a result row, so empty, spilled, and correlated relations retain
12//! the same declared `PostgreSQL` type identities as non-empty relations.
13
14mod analysis;
15pub mod catalog_sources;
16mod command_scopes;
17mod commands;
18pub mod composite_dependencies;
19mod composite_inputs;
20pub mod composite_rename;
21mod cte_controls;
22mod ctes;
23mod dependencies;
24mod merge_scopes;
25mod ordered_calls;
26mod preparation;
27mod projection;
28mod record_fields;
29mod routine_binding;
30mod routine_parameters;
31mod scope;
32mod source_schemas;
33mod sources;
34pub mod statements;
35mod type_resolution;
36mod variable_sites;
37
38pub use commands::analyze_prepared_command_schema;
39pub use preparation::{analyze_column_type_transform, infer_prepared_parameter_types};
40pub(crate) use preparation::{
41    analyze_default_inputs, analyze_routine_body_argument, analyze_routine_body_inputs,
42    prepare_routine_body_inputs,
43};
44pub(crate) use preparation::{
45    analyze_domain_check, analyze_stored_expression_inputs, read_prepared_inputs,
46};
47
48#[cfg(test)]
49mod tests;
50
51pub use cte_controls::{analyze_recursive_control_step, extend_cte_generated_schema};
52pub use projection::{
53    analyze_projection_output_schema, bind_projection_output_schema,
54    validate_query_block_expression_types, validate_query_block_references,
55};
56pub use routine_binding::{
57    bind_expression_plan_routines_for_storage, bind_query_plan_routines_for_storage,
58    bind_syntax_query_plan_routines,
59};
60pub use routine_parameters::{bind_routine_parameter_references, RoutineParameterScope};
61pub use scope::{
62    analyze_expression_plan_type, analyze_query_plan_schema,
63    analyze_query_plan_schema_with_catalog, bind_expression_plan_type, bind_query_plan_schema,
64};
65pub use scope::{overlay_outer_schema, values_types_in_scope};
66pub use sources::{
67    analyze_source_plan_schema, bind_source_plan_schema, bind_source_plan_schema_for_execution,
68    with_query_table_pseudo_columns,
69};
70pub use variable_sites::{resolve_variable_sites, VariableSiteResolution};
71
72use catalog_sources::operator_join_relation_schemas;
73use cte_controls::extend_recursive_cte_binding_schema;
74pub use cte_controls::hide_recursive_generated_schema;
75use projection::{projection_star_columns, rename_schema};
76use sources::{alias_table_schema, table_function_member_source, JoinSchemaBinding};
77use type_resolution::{set_operation_output_schema, QueryFunctionTypeResolver};
78
79use crate::plan::{QueryBlockPlan, QueryPlan, RelationalPlan, SourcePlan};
80use crate::semantics::{cte_references_own_name, expr_contains_subquery, projection_columns};
81use crate::{SQLError, SQLParam, ScalarExpr};
82use catalog_sources::user_function_output_columns;
83pub use context::BindingContext;
84use uqa_core::Value;
85
86pub mod context;
87mod rule_inputs;
88use crate::ast::ColumnType;
89use crate::catalog::analysis::CatalogReadView;
90use crate::catalog::resolution::RelationNameResolution;
91use crate::routines::RoutineResolution;
92use crate::semantics::{
93    apply_table_function_aliases, table_function_column_types, table_function_empty_schema,
94    validate_table_function_alias_count, validate_table_function_column_definition,
95    TableFunctionTypeRequest,
96};
97use crate::RowSchema;
98use std::collections::{BTreeMap, BTreeSet};
99
100#[derive(Clone, Copy)]
101enum ScalarBindingMode {
102    References,
103    Stored,
104    CompositeInputs,
105}
106
107struct SchemaScope {
108    catalog: CatalogReadView,
109    resolution: RelationNameResolution,
110    ctes: BTreeMap<String, RowSchema>,
111    deferred_ctes: BTreeMap<String, crate::plan::CtePlan>,
112    non_returning_ctes: BTreeSet<String>,
113    visiting_views: BTreeSet<String>,
114    validate_references: bool,
115    stored_expression_outer: Option<RowSchema>,
116    stored_whole_rows: BTreeMap<String, ScalarExpr>,
117    /// The parameters of the SQL routine whose body is bound, as the outermost scope: a reference that resolves into them, because no column of any query level takes its name, becomes the positional parameter it names.
118    routine_parameters: Option<RoutineParameterScope>,
119    /// Select reference resolution, complete stored binding, or prepared composite identity retention.
120    scalar_binding: ScalarBindingMode,
121    /// The names of a `PL/pgSQL` statement that the function's variables take, checked against what the statement can see.
122    variable_sites: Option<variable_sites::VariableSites>,
123    /// Keep `*` projections and `GROUP BY` output-name references as written, so a bound copy of stored syntax still corresponds to that syntax node for node.
124    preserve_syntax_shape: bool,
125    prepared_dependencies: Option<crate::prepared::dependencies::PreparedAnalysisDependencies>,
126    composite_dependencies: Option<Vec<crate::catalog::dependencies::ObjectAddress>>,
127}
128
129fn non_returning_cte_error(name: &str) -> SQLError {
130    SQLError::Unsupported(format!(
131        "WITH query \"{name}\" does not have a RETURNING clause"
132    ))
133}
134
135impl SchemaScope {
136    fn from_context(ctes: &BindingContext) -> Result<Self, SQLError> {
137        Ok(Self {
138            catalog: ctes.catalog.clone(),
139            resolution: ctes.resolution.clone(),
140            ctes: ctes.ctes.clone(),
141            deferred_ctes: ctes.deferred_ctes.clone(),
142            non_returning_ctes: ctes.non_returning_ctes.clone(),
143            visiting_views: BTreeSet::new(),
144            validate_references: false,
145            stored_expression_outer: None,
146            stored_whole_rows: BTreeMap::new(),
147            routine_parameters: None,
148            scalar_binding: ScalarBindingMode::Stored,
149            variable_sites: None,
150            preserve_syntax_shape: false,
151            prepared_dependencies: None,
152            composite_dependencies: None,
153        })
154    }
155
156    fn for_analysis(ctes: &BindingContext) -> Result<Self, SQLError> {
157        let mut scope = Self::from_context(ctes)?;
158        scope.validate_references = true;
159        Ok(scope)
160    }
161
162    fn for_catalog_analysis(catalog: CatalogReadView, resolution: RelationNameResolution) -> Self {
163        Self {
164            catalog,
165            resolution,
166            ctes: BTreeMap::new(),
167            deferred_ctes: BTreeMap::new(),
168            non_returning_ctes: BTreeSet::new(),
169            visiting_views: BTreeSet::new(),
170            validate_references: true,
171            stored_expression_outer: None,
172            stored_whole_rows: BTreeMap::new(),
173            routine_parameters: None,
174            scalar_binding: ScalarBindingMode::Stored,
175            variable_sites: None,
176            preserve_syntax_shape: false,
177            prepared_dependencies: None,
178            composite_dependencies: None,
179        }
180    }
181
182    fn bind_query(
183        &mut self,
184        routines: &dyn RoutineResolution,
185        plan: &QueryPlan,
186        params: &[SQLParam],
187        outer: Option<&RowSchema>,
188    ) -> Result<RowSchema, SQLError> {
189        self.bind_query_mode(routines, plan, params, outer, false)
190    }
191
192    fn bind_set_operand(
193        &mut self,
194        routines: &dyn RoutineResolution,
195        plan: &QueryPlan,
196        params: &[SQLParam],
197        outer: Option<&RowSchema>,
198    ) -> Result<RowSchema, SQLError> {
199        self.bind_query_mode(routines, plan, params, outer, true)
200    }
201
202    fn bind_query_mode(
203        &mut self,
204        routines: &dyn RoutineResolution,
205        plan: &QueryPlan,
206        params: &[SQLParam],
207        outer: Option<&RowSchema>,
208        preserve_top_level_unknown: bool,
209    ) -> Result<RowSchema, SQLError> {
210        let lookup_mode = if plan.relations_bound {
211            crate::catalog::resolution::RelationLookupMode::Bound
212        } else {
213            crate::catalog::resolution::RelationLookupMode::Dynamic
214        };
215        let previous = self.resolution.set_lookup_mode(lookup_mode);
216        let result =
217            self.bind_query_mode_inner(routines, plan, params, outer, preserve_top_level_unknown);
218        self.resolution.set_lookup_mode(previous);
219        result
220    }
221
222    fn bind_query_mode_inner(
223        &mut self,
224        routines: &dyn RoutineResolution,
225        plan: &QueryPlan,
226        params: &[SQLParam],
227        outer: Option<&RowSchema>,
228        preserve_top_level_unknown: bool,
229    ) -> Result<RowSchema, SQLError> {
230        let previous = self.bind_cte_schemas(routines, &plan.ctes, params, outer)?;
231
232        let result = self.bind_root(
233            routines,
234            &plan.root,
235            params,
236            outer,
237            preserve_top_level_unknown,
238        );
239        self.restore_cte_schemas(previous);
240        result
241    }
242
243    fn bind_recursive_seed(
244        &mut self,
245        routines: &dyn RoutineResolution,
246        plan: &QueryPlan,
247        params: &[SQLParam],
248        outer: Option<&RowSchema>,
249    ) -> Result<RowSchema, SQLError> {
250        match &plan.root {
251            RelationalPlan::SetOp { left, .. } => self.bind_query(routines, left, params, outer),
252            _ => self.bind_root(routines, &plan.root, params, outer, false),
253        }
254    }
255
256    fn bind_root(
257        &mut self,
258        routines: &dyn RoutineResolution,
259        root: &RelationalPlan,
260        params: &[SQLParam],
261        outer: Option<&RowSchema>,
262        preserve_top_level_unknown: bool,
263    ) -> Result<RowSchema, SQLError> {
264        match root {
265            RelationalPlan::QueryBlock(block) => {
266                self.bind_query_block(routines, block, params, outer, preserve_top_level_unknown)
267            }
268            RelationalPlan::SetOp {
269                kind,
270                all,
271                left,
272                right,
273                order_by,
274                limit,
275                offset,
276                subqueries,
277                ..
278            } => {
279                let left = self.bind_set_operand(routines, left, params, outer)?;
280                let right = self.bind_set_operand(routines, right, params, outer)?;
281                if left.len() != right.len() {
282                    return Err(SQLError::TypeMismatch(format!(
283                        "set operation has {} columns on the left and {} on the right",
284                        left.len(),
285                        right.len()
286                    )));
287                }
288                let output = set_operation_output_schema(&left, &right, *kind, *all)?;
289                if self.validate_references {
290                    self.validate_set_operation_clauses(
291                        routines,
292                        analysis::SetOperationClauses {
293                            order_by,
294                            limit: limit.as_deref(),
295                            offset: offset.as_deref(),
296                            subqueries,
297                            output: &output,
298                            outer,
299                        },
300                        params,
301                    )?;
302                }
303                Ok(output)
304            }
305            RelationalPlan::Values { rows, subqueries } => {
306                let columns = rows.first().map_or_else(Vec::new, |row| {
307                    (1..=row.len())
308                        .map(|index| format!("column{index}"))
309                        .collect()
310                });
311                let types = self.bind_values_types(routines, rows, subqueries, outer, params)?;
312                Ok(RowSchema::with_types(columns, types))
313            }
314        }
315    }
316
317    fn bind_query_block(
318        &mut self,
319        routines: &dyn RoutineResolution,
320        block: &QueryBlockPlan,
321        params: &[SQLParam],
322        outer: Option<&RowSchema>,
323        preserve_top_level_unknown: bool,
324    ) -> Result<RowSchema, SQLError> {
325        let source = block.from.as_ref().map_or_else(
326            || Ok(RowSchema::default()),
327            |source| self.bind_source(routines, source, &block.subqueries, params, outer),
328        )?;
329        let source = if self.validate_references {
330            analysis::with_query_source_columns(&source, block)
331        } else {
332            source
333        };
334        let expression_schema = overlay_outer_schema(&source, outer);
335        let labels = projection_columns(&block.projections);
336        let mut columns = Vec::new();
337        let mut types = Vec::new();
338        let mut records = Vec::new();
339        for (position, projection) in block.projections.iter().enumerate() {
340            if let Some(star_columns) = projection_star_columns(&projection.expr, &source)? {
341                records.extend(
342                    record_fields::star_fields(&projection.expr, &source).unwrap_or_default(),
343                );
344                for (column, ty) in star_columns {
345                    columns.push(column);
346                    types.push(ty);
347                }
348                continue;
349            }
350            columns.push(labels[position].clone());
351            types.push(
352                if preserve_top_level_unknown
353                    && matches!(
354                        &projection.expr,
355                        ScalarExpr::Literal(Value::Str(_) | Value::Null)
356                    )
357                {
358                    None
359                } else if matches!(&projection.expr, ScalarExpr::Literal(Value::Null)) {
360                    Some(ColumnType::Text)
361                } else {
362                    self.bind_expression_type(
363                        routines,
364                        &projection.expr,
365                        &expression_schema,
366                        &block.subqueries,
367                        params,
368                    )?
369                },
370            );
371            records.push(
372                if types.last().is_some_and(|ty| {
373                    matches!(ty, Some(ColumnType::Record | ColumnType::Composite(_)))
374                }) {
375                    self.bind_record_fields(
376                        routines,
377                        &projection.expr,
378                        &expression_schema,
379                        &block.subqueries,
380                        params,
381                    )?
382                } else {
383                    None
384                },
385            );
386        }
387        let output = RowSchema::with_types(columns, types).with_record_fields(
388            records
389                .into_iter()
390                .enumerate()
391                .filter_map(|(index, fields)| fields.map(|fields| (index, fields))),
392        );
393        let output = analysis::with_projected_open_columns(&output, &block.projections, &source);
394        if self.validate_references {
395            self.validate_query_block_clauses(
396                routines,
397                block,
398                &expression_schema,
399                &output,
400                params,
401                outer,
402            )?;
403        }
404        Ok(output)
405    }
406
407    fn bind_expression_type(
408        &mut self,
409        routines: &dyn RoutineResolution,
410        expression: &ScalarExpr,
411        schema: &RowSchema,
412        subqueries: &[QueryPlan],
413        params: &[SQLParam],
414    ) -> Result<Option<ColumnType>, SQLError> {
415        if let ScalarExpr::ScalarSubquery(index) = expression {
416            let plan = subqueries.get(*index).ok_or_else(|| {
417                SQLError::Internal(format!("scalar subquery slot {index} is out of bounds"))
418            })?;
419            // Result typing and validation share the complete lexical row scope.
420            let subquery_outer = Some(schema);
421            let output = self.bind_query(routines, plan, params, subquery_outer)?;
422            return Ok(output.column_type(0).cloned());
423        }
424        let schema = self.with_stored_outer_internal_aliases(schema);
425        let schema = &schema;
426        let resolver =
427            self.query_function_type_resolver(routines, expression, schema, subqueries, params)?;
428        if self.validate_references {
429            Self::validate_expression_references_with_resolver(
430                routines, expression, schema, None, params, &resolver,
431            )?;
432        }
433        crate::type_resolution::validate_catalog_literals(expression, schema, params, &resolver)?;
434        crate::scalar_type_with_resolver(expression, schema, params, &resolver)
435    }
436
437    #[expect(
438        clippy::too_many_lines,
439        reason = "preserves SELECT schema and row identity"
440    )]
441    fn bind_source_inner(
442        &mut self,
443        routines: &dyn RoutineResolution,
444        source: &SourcePlan,
445        subqueries: &[QueryPlan],
446        params: &[SQLParam],
447        outer: Option<&RowSchema>,
448    ) -> Result<RowSchema, SQLError> {
449        match source {
450            SourcePlan::Table {
451                name,
452                qualifier,
453                alias,
454                column_aliases,
455                bound_columns,
456                ..
457            } => {
458                let qualifier = alias.as_deref().unwrap_or(qualifier);
459                let cte_name = crate::semantics::cte_reference_name(name);
460                if let Some(schema) = cte_name.as_ref().and_then(|name| self.ctes.get(name)) {
461                    if let Some(name) = cte_name
462                        .as_ref()
463                        .filter(|name| self.non_returning_ctes.contains(*name))
464                    {
465                        return Err(non_returning_cte_error(name));
466                    }
467                    return alias_table_schema(schema, qualifier, column_aliases);
468                }
469                if let Some(plan) = cte_name
470                    .as_ref()
471                    .and_then(|name| self.deferred_ctes.get(name))
472                {
473                    if !plan.body.returns_rows() {
474                        return Err(non_returning_cte_error(&plan.name));
475                    }
476                }
477                if let Some(plan) = cte_name.and_then(|name| self.deferred_ctes.remove(&name)) {
478                    let result = self
479                        .bind_cte_body(routines, &plan.body, params, outer)
480                        .and_then(|schema| {
481                            let schema = rename_schema(&schema, &plan.columns, Some(qualifier));
482                            alias_table_schema(&schema, qualifier, column_aliases)
483                        });
484                    self.deferred_ctes.insert(plan.name.clone(), plan);
485                    return result;
486                }
487                if self.catalog.sequence_exists(&self.resolution, name)? {
488                    self.record_relation_dependency(name)?;
489                    let schema = RowSchema::with_qualified_types(
490                        qualifier,
491                        vec!["last_value".into(), "log_cnt".into(), "is_called".into()],
492                        vec![
493                            Some(ColumnType::BigInteger),
494                            Some(ColumnType::BigInteger),
495                            Some(ColumnType::Boolean),
496                        ],
497                    );
498                    return alias_table_schema(&schema, qualifier, column_aliases);
499                }
500                let view = self.catalog.view_resolved(&self.resolution, name)?;
501                if let Some(view) = view {
502                    self.record_relation_dependency(name)?;
503                    if view.materialized {
504                        let columns = view.output_columns.unwrap_or_default();
505                        if columns.len() != view.materialized_column_types.len() {
506                            return Err(SQLError::Internal(format!(
507                                "materialized view `{name}` has {} columns but {} stored column types",
508                                columns.len(),
509                                view.materialized_column_types.len()
510                            )));
511                        }
512                        let schema = RowSchema::with_qualified_types(
513                            qualifier,
514                            columns,
515                            view.materialized_column_types,
516                        );
517                        return alias_table_schema(&schema, qualifier, column_aliases);
518                    }
519                    let key = name.to_ascii_lowercase();
520                    if !self.visiting_views.insert(key.clone()) {
521                        return Err(SQLError::Internal(format!(
522                            "view `{name}` has a recursive schema dependency"
523                        )));
524                    }
525                    self.record_stored_query_dependencies(&view.query);
526                    let result = self
527                        .bind_query(routines, &view.query, params, outer)
528                        .and_then(|schema| {
529                            let schema = rename_schema(
530                                &schema,
531                                view.output_columns.as_deref().unwrap_or(&[]),
532                                Some(qualifier),
533                            );
534                            alias_table_schema(&schema, qualifier, column_aliases)
535                        });
536                    self.visiting_views.remove(&key);
537                    return result;
538                }
539                let table = self.catalog.table_resolved(&self.resolution, name)?;
540                if let Some(table) = table {
541                    self.record_relation_dependency(name)?;
542                    let columns = table
543                        .columns
544                        .iter()
545                        .map(|column| column.name.clone())
546                        .collect();
547                    let types = table
548                        .columns
549                        .iter()
550                        .map(|column| Some(column.ty.clone()))
551                        .collect();
552                    let schema = RowSchema::with_qualified_types(qualifier, columns, types);
553                    let schema =
554                        crate::semantics::bound_source_schema(&schema, bound_columns.as_deref())?;
555                    let schema = alias_table_schema(&schema, qualifier, column_aliases)?;
556                    let schema = if table.columns.is_empty() && !table.columns_declared {
557                        RowSchema::with_open_columns(&schema, Some(qualifier))
558                    } else {
559                        schema
560                    };
561                    return Ok(analysis::with_table_pseudo_columns(&schema, qualifier));
562                }
563                let foreign_table = self
564                    .catalog
565                    .foreign_table_resolved(&self.resolution, name)?;
566                if let Some(foreign_table) = foreign_table {
567                    self.record_relation_dependency(name)?;
568                    let typed_columns = foreign_table
569                        .columns
570                        .iter()
571                        .map(|column| (column.name.clone(), column.ty.clone()))
572                        .collect::<Vec<_>>();
573                    let columns = typed_columns
574                        .iter()
575                        .map(|(column, _)| column.clone())
576                        .collect();
577                    let types = typed_columns.into_iter().map(|(_, ty)| Some(ty)).collect();
578                    let schema = RowSchema::with_qualified_types(qualifier, columns, types);
579                    let schema =
580                        crate::semantics::bound_source_schema(&schema, bound_columns.as_deref())?;
581                    return alias_table_schema(&schema, qualifier, column_aliases);
582                }
583                if let Some(schema) = self
584                    .catalog
585                    .virtual_relation_schema(&self.resolution, name)?
586                {
587                    self.record_relation_dependency(name)?;
588                    let (columns, types): (Vec<_>, Vec<_>) = schema
589                        .into_iter()
590                        .map(|(column, ty)| (column, Some(ty)))
591                        .unzip();
592                    let schema = RowSchema::with_qualified_types(qualifier, columns, types);
593                    return alias_table_schema(&schema, qualifier, column_aliases);
594                }
595                if let Some(relation) = self.catalog.unopenable_relation(&self.resolution, name)? {
596                    return Err(relation.error());
597                }
598                Err(SQLError::UnknownTable(name.clone()))
599            }
600            SourcePlan::Values {
601                rows,
602                alias,
603                column_aliases,
604                internal_relation,
605                internal_column_types,
606            } => {
607                if let Some(relation) = internal_relation {
608                    return Ok(RowSchema::with_internal_relation_types(
609                        *relation,
610                        internal_column_types.clone(),
611                    ));
612                }
613                let columns = if column_aliases.is_empty() {
614                    (1..=rows.first().map_or(0, Vec::len))
615                        .map(|index| format!("column{index}"))
616                        .collect::<Vec<_>>()
617                } else {
618                    column_aliases.clone()
619                };
620                let binding_schema = outer.cloned().unwrap_or_default();
621                let types = self.bind_values_types(
622                    routines,
623                    rows,
624                    subqueries,
625                    Some(&binding_schema),
626                    params,
627                )?;
628                Ok(match alias.as_deref() {
629                    Some(qualifier) => RowSchema::with_qualified_types(qualifier, columns, types),
630                    None => RowSchema::with_types(columns, types),
631                })
632            }
633            SourcePlan::Function {
634                name,
635                binding,
636                output_name,
637                relations,
638                args,
639                alias,
640                column_aliases,
641                ordinality,
642                column_types,
643                ..
644            } => {
645                let lower = crate::semantics::builtin_function_dispatch_name(name);
646                let operator_join = crate::registry::is_operator_join_table_function(&lower);
647                let operator_inputs = operator_join
648                    .then(|| {
649                        operator_join_relation_schemas(
650                            &self.catalog,
651                            &self.resolution,
652                            relations.as_ref(),
653                        )
654                    })
655                    .transpose()?;
656                let input = outer.cloned().unwrap_or_default();
657                let type_resolver = self.query_function_type_resolver_for_subqueries(
658                    routines, args, &input, subqueries, params,
659                )?;
660                let user_function = if let Some((left, right)) = operator_inputs.as_ref() {
661                    if self.validate_references {
662                        let constant = RowSchema::default();
663                        for (position, argument) in args.iter().enumerate() {
664                            let schema = match position {
665                                0 => left,
666                                1 => right,
667                                _ => &constant,
668                            };
669                            self.validate_expression_references(
670                                routines, argument, schema, None, subqueries, params,
671                            )?;
672                        }
673                    }
674                    None
675                } else if self.validate_references {
676                    self.validate_table_function_source(
677                        routines,
678                        analysis::TableFunctionSourceValidation {
679                            name,
680                            binding: binding.as_ref(),
681                            args,
682                            subqueries,
683                            input: &input,
684                            params,
685                        },
686                    )?
687                } else {
688                    crate::semantics::resolve_user_table_function(
689                        routines,
690                        name,
691                        binding.as_ref(),
692                        args,
693                        &input,
694                        params,
695                        &type_resolver,
696                    )?
697                };
698                validate_table_function_column_definition(
699                    name,
700                    binding.as_ref(),
701                    user_function
702                        .as_ref()
703                        .map(|resolved| resolved.function.as_ref()),
704                    column_types,
705                )?;
706                if let Some(function) = &user_function {
707                    self.record_routine_dependency(&function.binding);
708                }
709                let catalog_columns = if !self.validate_references && user_function.is_none() {
710                    user_function_output_columns(self.catalog.as_ref(), &self.resolution, name)?
711                } else {
712                    None
713                };
714                let columns = user_function
715                    .as_ref()
716                    .map(|resolved| {
717                        crate::semantics::user_function_composite_result(
718                            routines,
719                            &resolved.function,
720                            resolved.binding.invocation.as_deref(),
721                        )
722                        .map(|descriptor| {
723                            descriptor
724                                .map(|descriptor| {
725                                    descriptor
726                                        .attributes
727                                        .iter()
728                                        .map(|attribute| attribute.name.clone())
729                                        .collect()
730                                })
731                                .or_else(|| {
732                                    crate::semantics::user_function_output_columns_for(
733                                        &resolved.function,
734                                    )
735                                })
736                        })
737                    })
738                    .transpose()?
739                    .flatten()
740                    .or(catalog_columns)
741                    .map_or_else(
742                        || {
743                            table_function_empty_schema(
744                                name,
745                                output_name,
746                                alias.as_deref(),
747                                column_aliases,
748                                args.len(),
749                                *ordinality,
750                            )
751                        },
752                        |columns| {
753                            apply_table_function_aliases(columns, column_aliases, *ordinality)
754                        },
755                    );
756                validate_table_function_alias_count(
757                    alias.as_deref().unwrap_or(output_name),
758                    columns.len(),
759                    column_aliases.len(),
760                )?;
761                let types = table_function_column_types(
762                    routines,
763                    TableFunctionTypeRequest {
764                        name,
765                        binding: binding.as_ref(),
766                        args,
767                        user_function: user_function
768                            .as_ref()
769                            .map(|resolved| resolved.function.as_ref()),
770                        user_invocation: user_function
771                            .as_ref()
772                            .and_then(|resolved| resolved.binding.invocation.as_deref()),
773                        declared_types: column_types,
774                        columns: &columns,
775                        ordinality: *ordinality,
776                    },
777                    &input,
778                    params,
779                    &type_resolver,
780                );
781                let qualifier = alias.as_deref().unwrap_or(output_name);
782                let schema = RowSchema::with_qualified_types(qualifier, columns, types);
783                Ok(
784                    if user_function.is_none()
785                        && column_types.is_empty()
786                        && routines.has_registered_table_function(name)
787                    {
788                        RowSchema::with_open_columns(&schema, Some(qualifier))
789                    } else {
790                        schema
791                    },
792                )
793            }
794            SourcePlan::FunctionGroup {
795                functions,
796                alias,
797                column_aliases,
798                ordinality,
799            } => {
800                let first = functions
801                    .first()
802                    .ok_or_else(|| SQLError::Internal("ROWS FROM group has no functions".into()))?;
803                let mut columns = Vec::new();
804                let mut types = Vec::new();
805                let mut open = false;
806                for function in functions {
807                    let member = table_function_member_source(function);
808                    let schema = self.bind_source(routines, &member, subqueries, params, outer)?;
809                    open |= schema.columns_are_open(None);
810                    columns.extend(schema.iter().enumerate().map(|(position, column)| {
811                        schema.public_name(position).unwrap_or(column).to_string()
812                    }));
813                    types.extend(schema.column_types().iter().cloned());
814                }
815                if *ordinality {
816                    columns.push("ordinality".into());
817                    types.push(Some(ColumnType::BigInteger));
818                }
819                let qualifier = alias.as_deref().unwrap_or(&first.output_name);
820                validate_table_function_alias_count(
821                    qualifier,
822                    columns.len(),
823                    column_aliases.len(),
824                )?;
825                for (column, alias) in columns.iter_mut().zip(column_aliases) {
826                    column.clone_from(alias);
827                }
828                let schema = RowSchema::with_qualified_types(qualifier, columns, types);
829                Ok(if open {
830                    RowSchema::with_open_columns(&schema, Some(qualifier))
831                } else {
832                    schema
833                })
834            }
835            SourcePlan::Subquery {
836                body,
837                alias,
838                column_aliases,
839            } => {
840                let schema = self.bind_query(routines, body, params, outer)?;
841                Ok(rename_schema(&schema, column_aliases, alias.as_deref()))
842            }
843            SourcePlan::Join {
844                left,
845                right,
846                kind,
847                on,
848                using,
849                natural,
850                alias,
851                column_aliases,
852                lateral,
853                ..
854            } => {
855                let left_schema = self.bind_source(routines, left, subqueries, params, outer)?;
856                let implicit_lateral_function = matches!(
857                    right.as_ref(),
858                    SourcePlan::Function { .. } | SourcePlan::FunctionGroup { .. }
859                );
860                let right_scope = (*lateral || implicit_lateral_function)
861                    .then(|| overlay_outer_schema(&left_schema, outer));
862                let right_schema = self.bind_source(
863                    routines,
864                    right,
865                    subqueries,
866                    params,
867                    right_scope.as_ref().or(outer),
868                )?;
869                self.bind_join_output_schema(JoinSchemaBinding {
870                    routines,
871                    kind: *kind,
872                    on: on.as_ref(),
873                    using: using.as_ref(),
874                    natural: *natural,
875                    alias: alias.as_deref(),
876                    column_aliases,
877                    left: &left_schema,
878                    right: &right_schema,
879                    subqueries,
880                    params,
881                    outer,
882                })
883            }
884        }
885    }
886}
887
888#[cfg(test)]
889pub(crate) mod fixture;
890
891pub mod correlation;
892
893pub mod stored_columns;
894pub mod stored_relations;
895pub mod stored_routines;
896pub mod stored_types;
897pub mod syntax_sites;
898
899pub mod scoped_types;
900pub mod snapshot;
901
902pub mod view_dependencies;
903
904pub mod portals;