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