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