Skip to main content

uqa_sql/binding/
sources.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Source row-type adapters and join binding inputs.
8
9use crate::ast::{JoinKind, JoinUsing};
10use crate::plan::{QueryPlan, SourcePlan, TableFunctionPlan};
11use crate::RowSchema;
12use crate::{SQLError, SQLParam};
13
14use super::{
15    analysis, operator_join_relation_schemas, overlay_outer_schema, projection::rename_schema,
16    BindingContext, ScalarExpr, SchemaScope,
17};
18use crate::routines::RoutineResolution;
19
20pub(super) fn alias_table_schema(
21    schema: &RowSchema,
22    qualifier: &str,
23    column_aliases: &[String],
24) -> Result<RowSchema, SQLError> {
25    if column_aliases.len() > schema.len() {
26        return Err(SQLError::Routine {
27            sqlstate: "42P10".into(),
28            message: format!(
29                "table \"{qualifier}\" has {} columns available but {} columns specified",
30                schema.len(),
31                column_aliases.len()
32            ),
33        });
34    }
35    Ok(rename_schema(schema, column_aliases, Some(qualifier)))
36}
37
38pub(super) struct JoinSchemaBinding<'a> {
39    pub(super) routines: &'a dyn RoutineResolution,
40    pub(super) kind: JoinKind,
41    pub(super) on: Option<&'a ScalarExpr>,
42    pub(super) using: Option<&'a JoinUsing>,
43    pub(super) natural: bool,
44    pub(super) alias: Option<&'a str>,
45    pub(super) column_aliases: &'a [String],
46    pub(super) left: &'a RowSchema,
47    pub(super) right: &'a RowSchema,
48    pub(super) subqueries: &'a [QueryPlan],
49    pub(super) params: &'a [SQLParam],
50    pub(super) outer: Option<&'a RowSchema>,
51}
52
53pub(super) fn table_function_member_source(function: &TableFunctionPlan) -> SourcePlan {
54    SourcePlan::Function {
55        name: function.name.clone(),
56        binding: function.binding.clone(),
57        output_name: function.output_name.clone(),
58        relations: function.relations.clone(),
59        args: function.args.clone(),
60        alias: None,
61        column_aliases: function.column_aliases.clone(),
62        ordinality: false,
63        column_types: function.column_types.clone(),
64    }
65}
66
67/// Derive the exact row type of one FROM source without executing it.
68pub fn bind_source_plan_schema(
69    routines: &dyn RoutineResolution,
70    source: &SourcePlan,
71    params: &[SQLParam],
72    ctes: &BindingContext,
73    outer: Option<&RowSchema>,
74) -> Result<RowSchema, SQLError> {
75    SchemaScope::from_context(ctes)?.bind_source(
76        routines,
77        source,
78        ctes.scalar_subqueries,
79        params,
80        outer,
81    )
82}
83
84/// Add query-block pseudo columns after the complete source scope is known, so `_meta` is exposed only for one unambiguous local-table source and never shadows a real relation alias.
85pub fn with_query_table_pseudo_columns(schema: &RowSchema) -> RowSchema {
86    analysis::with_unqualified_table_pseudo_columns(schema)
87}
88
89/// Derive and validate one FROM source's exact row type without executing it.
90pub fn analyze_source_plan_schema(
91    routines: &dyn RoutineResolution,
92    source: &SourcePlan,
93    params: &[SQLParam],
94    ctes: &BindingContext,
95    outer: Option<&RowSchema>,
96) -> Result<RowSchema, SQLError> {
97    SchemaScope::for_analysis(ctes)?.bind_source(
98        routines,
99        source,
100        ctes.scalar_subqueries,
101        params,
102        outer,
103    )
104}
105
106/// Bind every table-function source in one execution-owned source plan to its exact routine identity and return the schema derived from those same bindings.
107pub fn bind_source_plan_schema_for_execution(
108    routines: &dyn RoutineResolution,
109    source: &mut SourcePlan,
110    params: &[SQLParam],
111    ctes: &BindingContext,
112    outer: Option<&RowSchema>,
113) -> Result<RowSchema, SQLError> {
114    SchemaScope::from_context(ctes)?.bind_source_for_execution(
115        routines,
116        source,
117        ctes.scalar_subqueries,
118        params,
119        outer,
120    )
121}
122
123impl SchemaScope {
124    pub(super) fn bind_source(
125        &mut self,
126        routines: &dyn RoutineResolution,
127        source: &SourcePlan,
128        subqueries: &[QueryPlan],
129        params: &[SQLParam],
130        outer: Option<&RowSchema>,
131    ) -> Result<RowSchema, SQLError> {
132        let bound = matches!(
133            source,
134            SourcePlan::Table {
135                bound_columns: Some(_),
136                ..
137            }
138        );
139        let previous = bound.then(|| {
140            self.resolution
141                .set_lookup_mode(crate::catalog::resolution::RelationLookupMode::Bound)
142        });
143        let result = self.bind_source_inner(routines, source, subqueries, params, outer);
144        if let Some(previous) = previous {
145            self.resolution.set_lookup_mode(previous);
146        }
147        result
148    }
149}
150
151impl SchemaScope {
152    pub(super) fn bind_source_for_execution(
153        &mut self,
154        routines: &dyn RoutineResolution,
155        source: &mut SourcePlan,
156        subqueries: &[QueryPlan],
157        params: &[SQLParam],
158        outer: Option<&RowSchema>,
159    ) -> Result<RowSchema, SQLError> {
160        match source {
161            SourcePlan::Join {
162                left,
163                right,
164                kind,
165                on,
166                using,
167                natural,
168                alias,
169                column_aliases,
170                lateral,
171                ..
172            } => {
173                let left_schema =
174                    self.bind_source_for_execution(routines, left, subqueries, params, outer)?;
175                let implicit_lateral_function = matches!(
176                    right.as_ref(),
177                    SourcePlan::Function { .. } | SourcePlan::FunctionGroup { .. }
178                );
179                let right_scope = (*lateral || implicit_lateral_function)
180                    .then(|| overlay_outer_schema(&left_schema, outer));
181                let right_schema = self.bind_source_for_execution(
182                    routines,
183                    right,
184                    subqueries,
185                    params,
186                    right_scope.as_ref().or(outer),
187                )?;
188                return self.bind_join_output_schema(JoinSchemaBinding {
189                    routines,
190                    kind: *kind,
191                    on: on.as_ref(),
192                    using: using.as_ref(),
193                    natural: *natural,
194                    alias: alias.as_deref(),
195                    column_aliases,
196                    left: &left_schema,
197                    right: &right_schema,
198                    subqueries,
199                    params,
200                    outer,
201                });
202            }
203            SourcePlan::Function {
204                name,
205                binding,
206                relations,
207                args,
208                ..
209            } => {
210                let lower = crate::semantics::builtin_function_dispatch_name(name);
211                if crate::registry::is_operator_join_table_function(&lower) {
212                    operator_join_relation_schemas(
213                        &self.catalog,
214                        &self.resolution,
215                        relations.as_ref(),
216                    )?;
217                    return self.bind_source(routines, source, subqueries, params, outer);
218                }
219                let input = outer.cloned().unwrap_or_default();
220                let resolver = self.query_function_type_resolver_for_subqueries(
221                    routines, args, &input, subqueries, params,
222                )?;
223                if let Some(selected) = crate::semantics::resolve_table_function_binding(
224                    routines,
225                    name,
226                    binding.as_ref(),
227                    args,
228                    &input,
229                    params,
230                    &resolver,
231                )? {
232                    *binding = Some(selected);
233                }
234            }
235            SourcePlan::FunctionGroup { functions, .. } => {
236                for function in functions {
237                    let mut member = table_function_member_source(function);
238                    self.bind_source_for_execution(
239                        routines,
240                        &mut member,
241                        subqueries,
242                        params,
243                        outer,
244                    )?;
245                    let SourcePlan::Function { binding, .. } = member else {
246                        unreachable!("table-function member changed source kind during binding")
247                    };
248                    function.binding = binding;
249                }
250            }
251            SourcePlan::Table { .. } | SourcePlan::Values { .. } | SourcePlan::Subquery { .. } => {}
252        }
253        self.bind_source(routines, source, subqueries, params, outer)
254    }
255}