Skip to main content

uqa_sql/binding/
routine_binding.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Persistent exact routine identity binding for catalog-owned query plans.
8
9use super::{
10    cte_references_own_name, extend_cte_generated_schema, extend_recursive_cte_binding_schema,
11    operator_join_relation_schemas, overlay_outer_schema, rename_schema, BindingContext,
12    ColumnType, QueryPlan, RelationalPlan, RowSchema, SQLError, SQLParam, ScalarExpr, SchemaScope,
13    SourcePlan,
14};
15use crate::ast::FunctionBinding;
16use crate::plan::ExpressionPlan;
17use crate::routines::RoutineResolution;
18use crate::{ColumnIdentity, FunctionTypeResolver};
19
20impl SchemaScope {
21    pub(super) fn with_stored_outer_internal_aliases(&self, schema: &RowSchema) -> RowSchema {
22        self.stored_expression_outer.as_ref().map_or_else(
23            || schema.clone(),
24            |outer| {
25                if schema.physical_width() < outer.physical_width() {
26                    schema.clone()
27                } else {
28                    RowSchema::with_trailing_internal_aliases(schema, outer)
29                }
30            },
31        )
32    }
33
34    fn canonicalize_stored_outer_columns(&self, expression: &mut ScalarExpr, schema: &RowSchema) {
35        let Some(outer) = self.stored_expression_outer.as_ref() else {
36            return;
37        };
38        let Some(outer_start) = schema.physical_width().checked_sub(outer.physical_width()) else {
39            return;
40        };
41        crate::plan::rewrite_scalar_expression(expression, &mut |node| {
42            let lookup = match node {
43                ScalarExpr::Column(column) => ColumnIdentity::unqualified(column.as_str()),
44                ScalarExpr::QualifiedColumn { qualifier, column } => {
45                    ColumnIdentity::qualified(qualifier.as_str(), column.as_str())
46                }
47                _ => return,
48            };
49            let Some(slot) = schema.physical_slot_for_identity(&lookup) else {
50                return;
51            };
52            if slot < outer_start {
53                return;
54            }
55            let public_qualifier = match node {
56                ScalarExpr::QualifiedColumn { qualifier, .. } => Some(qualifier.as_str()),
57                ScalarExpr::Column(column) => {
58                    let mut qualifiers = outer
59                        .identities()
60                        .iter()
61                        .filter(|identity| identity.column() == column)
62                        .filter_map(ColumnIdentity::qualifier);
63                    let qualifier = qualifiers.next();
64                    (qualifiers.next().is_none()).then_some(qualifier).flatten()
65                }
66                _ => None,
67            };
68            let Some(public_qualifier) = public_qualifier.filter(|qualifier| {
69                qualifier.eq_ignore_ascii_case("old") || qualifier.eq_ignore_ascii_case("new")
70            }) else {
71                return;
72            };
73            let outer_lookup = ColumnIdentity::qualified(public_qualifier, lookup.column());
74            let Some(outer_slot) = outer.physical_slot_for_identity(&outer_lookup) else {
75                return;
76            };
77            let Some(column) = outer.unique_internal_column_for_slot(outer_slot) else {
78                return;
79            };
80            *node = ScalarExpr::InternalColumn(column);
81        });
82    }
83
84    fn bind_cte_routines_for_storage(
85        &mut self,
86        routines: &dyn RoutineResolution,
87        body: &mut crate::plan::CtePlanBody,
88        params: &[SQLParam],
89        outer: Option<&RowSchema>,
90    ) -> Result<RowSchema, SQLError> {
91        let crate::plan::CtePlanBody::Command(command) = body else {
92            return self.bind_query_routines_for_storage(
93                routines,
94                body.query_mut().expect("query CTE body"),
95                params,
96                outer,
97            );
98        };
99        let previous = match command.ctes_mut() {
100            Some(ctes) => self.bind_cte_routine_schemas(routines, ctes, params, None)?,
101            None => Vec::new(),
102        };
103        let result = (|| {
104            let (_, expression) = self.command_expression_schema(routines, command, params)?;
105            let subqueries = command.scalar_subqueries().to_vec();
106            if let Some(source) = command.source_input_mut() {
107                self.bind_source_routines_for_storage(routines, source, &subqueries, params, None)?;
108            }
109            for query in command.query_inputs_mut() {
110                self.bind_query_routines_for_storage(routines, query, params, Some(&expression))?;
111            }
112            let subqueries = command.scalar_subqueries().to_vec();
113            for scalar in command.expressions_mut() {
114                self.bind_scalar_routines_for_storage(
115                    routines,
116                    scalar,
117                    &expression,
118                    &subqueries,
119                    params,
120                    None,
121                )?;
122            }
123            self.bind_command_returning(routines, command, params)
124        })();
125        self.restore_cte_schemas(previous);
126        result
127    }
128
129    fn bind_cte_routine_schemas(
130        &mut self,
131        routines: &dyn RoutineResolution,
132        ctes: &mut [crate::plan::CtePlan],
133        params: &[SQLParam],
134        outer: Option<&RowSchema>,
135    ) -> Result<Vec<(String, bool, Option<RowSchema>)>, SQLError> {
136        let ordered_names = crate::semantics::ordered_cte_plans(ctes)?
137            .into_iter()
138            .map(|cte| cte.name.clone())
139            .collect::<Vec<_>>();
140        let mut previous = Vec::with_capacity(ordered_names.len());
141        for name in ordered_names {
142            let position = ctes
143                .iter()
144                .position(|cte| cte.name == name)
145                .ok_or_else(|| SQLError::Internal(format!("ordered CTE `{name}` disappeared")))?;
146            let self_recursive = cte_references_own_name(&ctes[position]);
147            if let Some(cycle) = ctes[position].cycle.as_mut() {
148                let schema = RowSchema::default();
149                self.bind_scalar_routines_for_storage(
150                    routines,
151                    &mut cycle.mark_value,
152                    &schema,
153                    &[],
154                    params,
155                    outer,
156                )?;
157                self.bind_scalar_routines_for_storage(
158                    routines,
159                    &mut cycle.mark_default,
160                    &schema,
161                    &[],
162                    params,
163                    outer,
164                )?;
165            }
166            let provisional = if self_recursive {
167                self.bind_recursive_seed(
168                    routines,
169                    ctes[position]
170                        .body
171                        .query()
172                        .ok_or_else(|| SQLError::Routine {
173                            sqlstate: "42P19".into(),
174                            message: format!(
175                                "recursive query \"{}\" must not contain data-modifying statements",
176                                ctes[position].name
177                            ),
178                        })?,
179                    params,
180                    outer,
181                )?
182            } else {
183                self.bind_cte_routines_for_storage(
184                    routines,
185                    &mut ctes[position].body,
186                    params,
187                    outer,
188                )?
189            };
190            let columns = ctes[position].columns.clone();
191            let provisional = rename_schema(&provisional, &columns, None);
192            let provisional = if self_recursive {
193                extend_recursive_cte_binding_schema(routines, &ctes[position], provisional, params)?
194            } else {
195                extend_cte_generated_schema(routines, &ctes[position], provisional, params)?
196            };
197            previous.push((
198                name.clone(),
199                self.set_cte_returning(&ctes[position]),
200                self.ctes.insert(name.clone(), provisional),
201            ));
202            if self_recursive {
203                let complete = self.bind_cte_routines_for_storage(
204                    routines,
205                    &mut ctes[position].body,
206                    params,
207                    outer,
208                )?;
209                let complete = rename_schema(&complete, &columns, None);
210                let complete =
211                    extend_cte_generated_schema(routines, &ctes[position], complete, params)?;
212                self.ctes.insert(name, complete);
213            }
214        }
215
216        Ok(previous)
217    }
218
219    fn bind_query_routines_for_storage(
220        &mut self,
221        routines: &dyn RoutineResolution,
222        plan: &mut QueryPlan,
223        params: &[SQLParam],
224        outer: Option<&RowSchema>,
225    ) -> Result<RowSchema, SQLError> {
226        let previous = self.bind_cte_routine_schemas(routines, &mut plan.ctes, params, outer)?;
227
228        let result = self.bind_root_routines_for_storage(routines, &mut plan.root, params, outer);
229        self.restore_cte_schemas(previous);
230        result
231    }
232
233    #[expect(
234        clippy::too_many_lines,
235        reason = "preserves SELECT schema and row identity"
236    )]
237    fn bind_root_routines_for_storage(
238        &mut self,
239        routines: &dyn RoutineResolution,
240        root: &mut RelationalPlan,
241        params: &[SQLParam],
242        outer: Option<&RowSchema>,
243    ) -> Result<RowSchema, SQLError> {
244        match root {
245            RelationalPlan::QueryBlock(block) => {
246                let source_schema = match block.from.as_mut() {
247                    Some(source) => self.bind_source_for_execution(
248                        routines,
249                        source,
250                        &block.subqueries,
251                        params,
252                        outer,
253                    )?,
254                    None => RowSchema::default(),
255                };
256                if let Some(source) = block.from.as_mut() {
257                    self.bind_source_routines_for_storage(
258                        routines,
259                        source,
260                        &block.subqueries,
261                        params,
262                        outer,
263                    )?;
264                }
265                let expression_schema = overlay_outer_schema(&source_schema, outer);
266                for subquery in &mut block.subqueries {
267                    self.bind_query_routines_for_storage(
268                        routines,
269                        subquery,
270                        params,
271                        Some(&expression_schema),
272                    )?;
273                }
274            }
275            RelationalPlan::SetOp {
276                left,
277                right,
278                subqueries,
279                ..
280            } => {
281                self.bind_query_routines_for_storage(routines, left, params, outer)?;
282                self.bind_query_routines_for_storage(routines, right, params, outer)?;
283                for subquery in subqueries {
284                    self.bind_query_routines_for_storage(routines, subquery, params, outer)?;
285                }
286            }
287            RelationalPlan::Values { subqueries, .. } => {
288                for subquery in subqueries {
289                    self.bind_query_routines_for_storage(routines, subquery, params, outer)?;
290                }
291            }
292        }
293
294        let set_output = match &*root {
295            RelationalPlan::SetOp { .. } => {
296                Some(self.bind_root(routines, root, params, outer, false)?)
297            }
298            RelationalPlan::QueryBlock(_) | RelationalPlan::Values { .. } => None,
299        };
300        match root {
301            RelationalPlan::QueryBlock(block) => {
302                if block.from.is_none()
303                    && block
304                        .projections
305                        .iter()
306                        .any(|projection| matches!(projection.expr, ScalarExpr::Star))
307                    && outer.is_none()
308                {
309                    return Err(SQLError::Routine {
310                        sqlstate: "42601".into(),
311                        message: "SELECT * with no tables specified is not valid".into(),
312                    });
313                }
314                let source_schema = block.from.as_ref().map_or_else(
315                    || Ok(RowSchema::default()),
316                    |source| self.bind_source(routines, source, &block.subqueries, params, outer),
317                )?;
318                let expression_schema = overlay_outer_schema(&source_schema, outer);
319                if let Some(filter) = block.r#where.as_mut() {
320                    self.bind_scalar_routines_for_storage(
321                        routines,
322                        filter,
323                        &expression_schema,
324                        &block.subqueries,
325                        params,
326                        outer,
327                    )?;
328                }
329                for projection in &mut block.projections {
330                    self.bind_scalar_routines_for_storage(
331                        routines,
332                        &mut projection.expr,
333                        &expression_schema,
334                        &block.subqueries,
335                        params,
336                        outer,
337                    )?;
338                }
339                block.projections = crate::semantics::expand_bound_projection_stars(
340                    &block.projections,
341                    &source_schema,
342                )?;
343                for expression in &mut block.group_by {
344                    self.bind_scalar_routines_for_storage(
345                        routines,
346                        expression,
347                        &expression_schema,
348                        &block.subqueries,
349                        params,
350                        outer,
351                    )?;
352                }
353                for set in &mut block.grouping_sets {
354                    for expression in set {
355                        self.bind_scalar_routines_for_storage(
356                            routines,
357                            expression,
358                            &expression_schema,
359                            &block.subqueries,
360                            params,
361                            outer,
362                        )?;
363                    }
364                }
365                if let Some(having) = block.having.as_mut() {
366                    self.bind_scalar_routines_for_storage(
367                        routines,
368                        having,
369                        &expression_schema,
370                        &block.subqueries,
371                        params,
372                        outer,
373                    )?;
374                }
375                for order in &mut block.order_by {
376                    self.bind_scalar_routines_for_storage(
377                        routines,
378                        &mut order.expr,
379                        &expression_schema,
380                        &block.subqueries,
381                        params,
382                        outer,
383                    )?;
384                }
385                if let Some(limit) = block.limit.as_mut() {
386                    self.bind_scalar_routines_for_storage(
387                        routines,
388                        limit,
389                        &expression_schema,
390                        &block.subqueries,
391                        params,
392                        outer,
393                    )?;
394                }
395                if let Some(offset) = block.offset.as_mut() {
396                    self.bind_scalar_routines_for_storage(
397                        routines,
398                        offset,
399                        &expression_schema,
400                        &block.subqueries,
401                        params,
402                        outer,
403                    )?;
404                }
405                for expression in &mut block.distinct_on {
406                    self.bind_scalar_routines_for_storage(
407                        routines,
408                        expression,
409                        &expression_schema,
410                        &block.subqueries,
411                        params,
412                        outer,
413                    )?;
414                }
415            }
416            RelationalPlan::SetOp {
417                order_by,
418                limit,
419                offset,
420                subqueries,
421                ..
422            } => {
423                let output = set_output
424                    .as_ref()
425                    .expect("set-operation output schema was bound before routine expressions");
426                for order in order_by {
427                    self.bind_scalar_routines_for_storage(
428                        routines,
429                        &mut order.expr,
430                        output,
431                        subqueries,
432                        params,
433                        outer,
434                    )?;
435                }
436                if let Some(limit) = limit {
437                    self.bind_scalar_routines_for_storage(
438                        routines, limit, output, subqueries, params, outer,
439                    )?;
440                }
441                if let Some(offset) = offset {
442                    self.bind_scalar_routines_for_storage(
443                        routines, offset, output, subqueries, params, outer,
444                    )?;
445                }
446            }
447            RelationalPlan::Values { rows, subqueries } => {
448                let input = outer.cloned().unwrap_or_default();
449                for expression in rows.iter_mut().flatten() {
450                    self.bind_scalar_routines_for_storage(
451                        routines, expression, &input, subqueries, params, outer,
452                    )?;
453                }
454            }
455        }
456        self.bind_root(routines, root, params, outer, false)
457    }
458
459    #[expect(
460        clippy::too_many_lines,
461        reason = "preserves SELECT schema and row identity"
462    )]
463    fn bind_source_routines_for_storage(
464        &mut self,
465        engine: &dyn RoutineResolution,
466        source: &mut SourcePlan,
467        subqueries: &[QueryPlan],
468        params: &[SQLParam],
469        outer: Option<&RowSchema>,
470    ) -> Result<(), SQLError> {
471        match source {
472            SourcePlan::Join {
473                left,
474                right,
475                on,
476                lateral,
477                ..
478            } => {
479                self.bind_source_routines_for_storage(engine, left, subqueries, params, outer)?;
480                let left_schema = self.bind_source(engine, left, subqueries, params, outer)?;
481                let implicit_lateral_function = matches!(
482                    right.as_ref(),
483                    SourcePlan::Function { .. } | SourcePlan::FunctionGroup { .. }
484                );
485                let right_scope = (*lateral || implicit_lateral_function)
486                    .then(|| overlay_outer_schema(&left_schema, outer));
487                let right_outer = right_scope.as_ref().or(outer);
488                self.bind_source_routines_for_storage(
489                    engine,
490                    right,
491                    subqueries,
492                    params,
493                    right_outer,
494                )?;
495                if let Some(on) = on {
496                    let right_schema =
497                        self.bind_source(engine, right, subqueries, params, right_outer)?;
498                    let input = RowSchema::join(&left_schema, &right_schema, std::iter::empty());
499                    let input = overlay_outer_schema(&input, outer);
500                    self.bind_scalar_routines_for_storage(
501                        engine, on, &input, subqueries, params, outer,
502                    )?;
503                }
504                Ok(())
505            }
506            SourcePlan::Subquery { body, .. } => self
507                .bind_query_routines_for_storage(engine, body, params, outer)
508                .map(|_| ()),
509            SourcePlan::Values { rows, .. } => {
510                let input = outer.cloned().unwrap_or_default();
511                for expression in rows.iter_mut().flatten() {
512                    self.bind_scalar_routines_for_storage(
513                        engine, expression, &input, subqueries, params, outer,
514                    )?;
515                }
516                Ok(())
517            }
518            SourcePlan::Function {
519                name,
520                relations,
521                args,
522                ..
523            } => {
524                let local = crate::semantics::builtin_function_dispatch_name(name);
525                if crate::registry::is_operator_join_table_function(&local) {
526                    let (left, right) = operator_join_relation_schemas(
527                        &self.catalog,
528                        &self.resolution,
529                        relations.as_ref(),
530                    )?;
531                    let constant = RowSchema::default();
532                    for (position, expression) in args.iter_mut().enumerate() {
533                        let input = match position {
534                            0 => &left,
535                            1 => &right,
536                            _ => &constant,
537                        };
538                        self.bind_scalar_routines_for_storage(
539                            engine, expression, input, subqueries, params, outer,
540                        )?;
541                    }
542                    return Ok(());
543                }
544                let input = outer.cloned().unwrap_or_default();
545                for expression in args {
546                    self.bind_scalar_routines_for_storage(
547                        engine, expression, &input, subqueries, params, outer,
548                    )?;
549                }
550                Ok(())
551            }
552            SourcePlan::FunctionGroup { functions, .. } => {
553                for function in functions {
554                    let local = crate::semantics::builtin_function_dispatch_name(&function.name);
555                    if crate::registry::is_operator_join_table_function(&local) {
556                        let (left, right) = operator_join_relation_schemas(
557                            &self.catalog,
558                            &self.resolution,
559                            function.relations.as_ref(),
560                        )?;
561                        let constant = RowSchema::default();
562                        for (position, expression) in function.args.iter_mut().enumerate() {
563                            let input = match position {
564                                0 => &left,
565                                1 => &right,
566                                _ => &constant,
567                            };
568                            self.bind_scalar_routines_for_storage(
569                                engine, expression, input, subqueries, params, outer,
570                            )?;
571                        }
572                        continue;
573                    }
574                    let input = outer.cloned().unwrap_or_default();
575                    for expression in &mut function.args {
576                        self.bind_scalar_routines_for_storage(
577                            engine, expression, &input, subqueries, params, outer,
578                        )?;
579                    }
580                }
581                Ok(())
582            }
583            SourcePlan::Table { .. } => Ok(()),
584        }
585    }
586
587    fn bind_scalar_routines_for_storage(
588        &mut self,
589        engine: &dyn RoutineResolution,
590        expression: &mut ScalarExpr,
591        schema: &RowSchema,
592        subqueries: &[QueryPlan],
593        params: &[SQLParam],
594        outer: Option<&RowSchema>,
595    ) -> Result<(), SQLError> {
596        let schema = self.with_stored_outer_internal_aliases(schema);
597        let schema = &schema;
598        self.canonicalize_stored_outer_columns(expression, schema);
599        let mut failure = None;
600        crate::plan::rewrite_scalar_expression(expression, &mut |expression| {
601            if failure.is_some() {
602                return;
603            }
604            let ScalarExpr::Func {
605                name,
606                binding,
607                args,
608                ..
609            } = expression
610            else {
611                return;
612            };
613            if binding
614                .as_ref()
615                .and_then(|binding| binding.dispatch)
616                .is_some()
617            {
618                return;
619            }
620            if let Err(error) = self.bind_scalar_function_for_storage(
621                engine, name, binding, args, schema, subqueries, params, outer,
622            ) {
623                failure = Some(error);
624            }
625        });
626        failure.map_or(Ok(()), Err)
627    }
628
629    #[expect(
630        clippy::too_many_arguments,
631        reason = "keeps execution context inputs aligned"
632    )]
633    fn bind_scalar_function_for_storage(
634        &mut self,
635        engine: &dyn RoutineResolution,
636        name: &str,
637        binding: &mut Option<FunctionBinding>,
638        args: &[ScalarExpr],
639        schema: &RowSchema,
640        subqueries: &[QueryPlan],
641        params: &[SQLParam],
642        outer: Option<&RowSchema>,
643    ) -> Result<(), SQLError> {
644        let resolver = self.query_function_type_resolver_for_subqueries(
645            engine, args, schema, subqueries, params, outer,
646        )?;
647        let (argument_names, argument_types, explicit_variadic) =
648            crate::function_call_argument_signature(args, schema, params, Some(&resolver))?;
649        let selected = if crate::is_fixed_builtin(name) {
650            crate::resolve_fixed_builtin_call(
651                name,
652                binding.as_ref(),
653                &argument_names,
654                &argument_types,
655                explicit_variadic,
656                Some(&resolver),
657            )?
658            .map(|resolved| resolved.selected)
659        } else {
660            resolver.resolve_function_overload(
661                name,
662                binding.as_ref(),
663                &argument_names,
664                &argument_types,
665                explicit_variadic,
666            )?
667        };
668        if let Some(selected) = selected {
669            *binding = Some(selected.binding);
670        }
671        Ok(())
672    }
673}
674
675pub fn bind_query_plan_routines_for_storage(
676    engine: &dyn RoutineResolution,
677    plan: &mut QueryPlan,
678    params: &[SQLParam],
679    ctes: &BindingContext,
680    outer: Option<&RowSchema>,
681) -> Result<RowSchema, SQLError> {
682    SchemaScope::for_analysis(ctes)?.bind_query_routines_for_storage(engine, plan, params, outer)
683}
684
685/// Bind every routine call owned by a stored scalar expression and validate its complete query-valued descendants against the expression's row scope.
686pub fn bind_expression_plan_routines_for_storage(
687    engine: &dyn RoutineResolution,
688    plan: &mut ExpressionPlan,
689    params: &[SQLParam],
690    ctes: &BindingContext,
691    schema: &RowSchema,
692) -> Result<Option<ColumnType>, SQLError> {
693    let mut scope = SchemaScope::for_analysis(ctes)?;
694    scope.stored_expression_outer = Some(schema.clone());
695    for subquery in &mut plan.subqueries {
696        scope.bind_query_routines_for_storage(engine, subquery, params, Some(schema))?;
697    }
698    scope.bind_scalar_routines_for_storage(
699        engine,
700        &mut plan.scalar,
701        schema,
702        &plan.subqueries,
703        params,
704        None,
705    )?;
706    scope.bind_expression_type(engine, &plan.scalar, schema, &plan.subqueries, params, None)
707}