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
38impl SchemaScope {
39    pub(super) fn alias_relation_schema(
40        &self,
41        schema: &RowSchema,
42        name: &str,
43        qualifier: &str,
44        column_aliases: &[String],
45    ) -> Result<RowSchema, SQLError> {
46        let schema = alias_table_schema(schema, qualifier, column_aliases)?;
47        match self.catalog.relation_row_type(&self.resolution, name)? {
48            Some(row_type) => {
49                schema.with_relation_row_type(qualifier, crate::ColumnType::Composite(row_type))
50            }
51            None => Ok(schema),
52        }
53    }
54}
55
56pub(super) struct JoinSchemaBinding<'a> {
57    pub(super) routines: &'a dyn RoutineResolution,
58    pub(super) kind: JoinKind,
59    pub(super) on: Option<&'a ScalarExpr>,
60    pub(super) using: Option<&'a JoinUsing>,
61    pub(super) natural: bool,
62    pub(super) alias: Option<&'a str>,
63    pub(super) column_aliases: &'a [String],
64    pub(super) left: &'a RowSchema,
65    pub(super) right: &'a RowSchema,
66    pub(super) subqueries: &'a [QueryPlan],
67    pub(super) params: &'a [SQLParam],
68    pub(super) outer: Option<&'a RowSchema>,
69}
70
71pub(super) fn table_function_member_source(function: &TableFunctionPlan) -> SourcePlan {
72    SourcePlan::Function {
73        name: function.name.clone(),
74        binding: function.binding.clone(),
75        output_name: function.output_name.clone(),
76        relations: function.relations.clone(),
77        args: function.args.clone(),
78        alias: None,
79        column_aliases: function.column_aliases.clone(),
80        ordinality: false,
81        column_types: function.column_types.clone(),
82    }
83}
84
85/// Derive the exact row type of one FROM source without executing it.
86pub fn bind_source_plan_schema(
87    routines: &dyn RoutineResolution,
88    source: &SourcePlan,
89    params: &[SQLParam],
90    ctes: &BindingContext,
91    outer: Option<&RowSchema>,
92) -> Result<RowSchema, SQLError> {
93    SchemaScope::from_context(ctes)?.bind_source(
94        routines,
95        source,
96        ctes.scalar_subqueries,
97        params,
98        outer,
99    )
100}
101
102/// 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.
103pub fn with_query_table_pseudo_columns(schema: &RowSchema) -> RowSchema {
104    analysis::with_unqualified_table_pseudo_columns(schema)
105}
106
107/// Derive and validate one FROM source's exact row type without executing it.
108pub fn analyze_source_plan_schema(
109    routines: &dyn RoutineResolution,
110    source: &SourcePlan,
111    params: &[SQLParam],
112    ctes: &BindingContext,
113    outer: Option<&RowSchema>,
114) -> Result<RowSchema, SQLError> {
115    SchemaScope::for_analysis(ctes)?.bind_source(
116        routines,
117        source,
118        ctes.scalar_subqueries,
119        params,
120        outer,
121    )
122}
123
124/// 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.
125pub fn bind_source_plan_schema_for_execution(
126    routines: &dyn RoutineResolution,
127    source: &mut SourcePlan,
128    params: &[SQLParam],
129    ctes: &BindingContext,
130    outer: Option<&RowSchema>,
131) -> Result<RowSchema, SQLError> {
132    SchemaScope::from_context(ctes)?.bind_source_for_execution(
133        routines,
134        source,
135        ctes.scalar_subqueries,
136        params,
137        outer,
138    )
139}
140
141impl SchemaScope {
142    pub(super) fn bind_source(
143        &mut self,
144        routines: &dyn RoutineResolution,
145        source: &SourcePlan,
146        subqueries: &[QueryPlan],
147        params: &[SQLParam],
148        outer: Option<&RowSchema>,
149    ) -> Result<RowSchema, SQLError> {
150        let bound = matches!(
151            source,
152            SourcePlan::Table {
153                bound_columns: Some(_),
154                ..
155            }
156        );
157        let previous = bound.then(|| {
158            self.resolution
159                .set_lookup_mode(crate::catalog::resolution::RelationLookupMode::Bound)
160        });
161        let result = self.bind_source_inner(routines, source, subqueries, params, outer);
162        if let Some(previous) = previous {
163            self.resolution.set_lookup_mode(previous);
164        }
165        result
166    }
167}
168
169impl SchemaScope {
170    pub(super) fn bind_source_for_execution(
171        &mut self,
172        routines: &dyn RoutineResolution,
173        source: &mut SourcePlan,
174        subqueries: &[QueryPlan],
175        params: &[SQLParam],
176        outer: Option<&RowSchema>,
177    ) -> Result<RowSchema, SQLError> {
178        match source {
179            SourcePlan::Join {
180                left,
181                right,
182                kind,
183                on,
184                using,
185                natural,
186                alias,
187                column_aliases,
188                lateral,
189                ..
190            } => {
191                let left_schema =
192                    self.bind_source_for_execution(routines, left, subqueries, params, outer)?;
193                let implicit_lateral_function = matches!(
194                    right.as_ref(),
195                    SourcePlan::Function { .. } | SourcePlan::FunctionGroup { .. }
196                );
197                let right_scope = (*lateral || implicit_lateral_function)
198                    .then(|| overlay_outer_schema(&left_schema, outer));
199                let right_schema = self.bind_source_for_execution(
200                    routines,
201                    right,
202                    subqueries,
203                    params,
204                    right_scope.as_ref().or(outer),
205                )?;
206                return self.bind_join_output_schema(JoinSchemaBinding {
207                    routines,
208                    kind: *kind,
209                    on: on.as_ref(),
210                    using: using.as_ref(),
211                    natural: *natural,
212                    alias: alias.as_deref(),
213                    column_aliases,
214                    left: &left_schema,
215                    right: &right_schema,
216                    subqueries,
217                    params,
218                    outer,
219                });
220            }
221            SourcePlan::Function {
222                name,
223                binding,
224                relations,
225                args,
226                ..
227            } => {
228                let lower = crate::semantics::builtin_function_dispatch_name(name);
229                if crate::registry::is_operator_join_table_function(&lower) {
230                    operator_join_relation_schemas(
231                        &self.catalog,
232                        &self.resolution,
233                        relations.as_ref(),
234                    )?;
235                    return self.bind_source(routines, source, subqueries, params, outer);
236                }
237                let input = outer.cloned().unwrap_or_default();
238                let resolver = self.query_function_type_resolver_for_subqueries(
239                    routines, args, &input, subqueries, params,
240                )?;
241                if let Some(selected) = crate::semantics::resolve_table_function_binding(
242                    routines,
243                    name,
244                    binding.as_ref(),
245                    args,
246                    &input,
247                    params,
248                    &resolver,
249                )? {
250                    *binding = Some(selected);
251                }
252            }
253            SourcePlan::FunctionGroup { functions, .. } => {
254                for function in functions {
255                    let mut member = table_function_member_source(function);
256                    self.bind_source_for_execution(
257                        routines,
258                        &mut member,
259                        subqueries,
260                        params,
261                        outer,
262                    )?;
263                    let SourcePlan::Function { binding, .. } = member else {
264                        unreachable!("table-function member changed source kind during binding")
265                    };
266                    function.binding = binding;
267                }
268            }
269            SourcePlan::Table { .. } | SourcePlan::Values { .. } | SourcePlan::Subquery { .. } => {}
270        }
271        self.bind_source(routines, source, subqueries, params, outer)
272    }
273}