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                crate::semantics::grouping_sets::bind_grouping_names(
344                    routines,
345                    block,
346                    &source_schema,
347                    params,
348                )?;
349                for expression in &mut block.group_by {
350                    self.bind_scalar_routines_for_storage(
351                        routines,
352                        expression,
353                        &expression_schema,
354                        &block.subqueries,
355                        params,
356                        outer,
357                    )?;
358                }
359                for set in &mut block.grouping_sets {
360                    for expression in set {
361                        self.bind_scalar_routines_for_storage(
362                            routines,
363                            expression,
364                            &expression_schema,
365                            &block.subqueries,
366                            params,
367                            outer,
368                        )?;
369                    }
370                }
371                if let Some(having) = block.having.as_mut() {
372                    self.bind_scalar_routines_for_storage(
373                        routines,
374                        having,
375                        &expression_schema,
376                        &block.subqueries,
377                        params,
378                        outer,
379                    )?;
380                }
381                for order in &mut block.order_by {
382                    self.bind_scalar_routines_for_storage(
383                        routines,
384                        &mut order.expr,
385                        &expression_schema,
386                        &block.subqueries,
387                        params,
388                        outer,
389                    )?;
390                }
391                if let Some(limit) = block.limit.as_mut() {
392                    self.bind_scalar_routines_for_storage(
393                        routines,
394                        limit,
395                        &expression_schema,
396                        &block.subqueries,
397                        params,
398                        outer,
399                    )?;
400                }
401                if let Some(offset) = block.offset.as_mut() {
402                    self.bind_scalar_routines_for_storage(
403                        routines,
404                        offset,
405                        &expression_schema,
406                        &block.subqueries,
407                        params,
408                        outer,
409                    )?;
410                }
411                for expression in &mut block.distinct_on {
412                    self.bind_scalar_routines_for_storage(
413                        routines,
414                        expression,
415                        &expression_schema,
416                        &block.subqueries,
417                        params,
418                        outer,
419                    )?;
420                }
421            }
422            RelationalPlan::SetOp {
423                order_by,
424                limit,
425                offset,
426                subqueries,
427                ..
428            } => {
429                let output = set_output
430                    .as_ref()
431                    .expect("set-operation output schema was bound before routine expressions");
432                for order in order_by {
433                    self.bind_scalar_routines_for_storage(
434                        routines,
435                        &mut order.expr,
436                        output,
437                        subqueries,
438                        params,
439                        outer,
440                    )?;
441                }
442                if let Some(limit) = limit {
443                    self.bind_scalar_routines_for_storage(
444                        routines, limit, output, subqueries, params, outer,
445                    )?;
446                }
447                if let Some(offset) = offset {
448                    self.bind_scalar_routines_for_storage(
449                        routines, offset, output, subqueries, params, outer,
450                    )?;
451                }
452            }
453            RelationalPlan::Values { rows, subqueries } => {
454                let input = outer.cloned().unwrap_or_default();
455                for expression in rows.iter_mut().flatten() {
456                    self.bind_scalar_routines_for_storage(
457                        routines, expression, &input, subqueries, params, outer,
458                    )?;
459                }
460            }
461        }
462        self.bind_root(routines, root, params, outer, false)
463    }
464
465    #[expect(
466        clippy::too_many_lines,
467        reason = "preserves SELECT schema and row identity"
468    )]
469    fn bind_source_routines_for_storage(
470        &mut self,
471        engine: &dyn RoutineResolution,
472        source: &mut SourcePlan,
473        subqueries: &[QueryPlan],
474        params: &[SQLParam],
475        outer: Option<&RowSchema>,
476    ) -> Result<(), SQLError> {
477        match source {
478            SourcePlan::Join {
479                left,
480                right,
481                on,
482                lateral,
483                ..
484            } => {
485                self.bind_source_routines_for_storage(engine, left, subqueries, params, outer)?;
486                let left_schema = self.bind_source(engine, left, subqueries, params, outer)?;
487                let implicit_lateral_function = matches!(
488                    right.as_ref(),
489                    SourcePlan::Function { .. } | SourcePlan::FunctionGroup { .. }
490                );
491                let right_scope = (*lateral || implicit_lateral_function)
492                    .then(|| overlay_outer_schema(&left_schema, outer));
493                let right_outer = right_scope.as_ref().or(outer);
494                self.bind_source_routines_for_storage(
495                    engine,
496                    right,
497                    subqueries,
498                    params,
499                    right_outer,
500                )?;
501                if let Some(on) = on {
502                    let right_schema =
503                        self.bind_source(engine, right, subqueries, params, right_outer)?;
504                    let input = RowSchema::join(&left_schema, &right_schema, std::iter::empty());
505                    let input = overlay_outer_schema(&input, outer);
506                    self.bind_scalar_routines_for_storage(
507                        engine, on, &input, subqueries, params, outer,
508                    )?;
509                }
510                Ok(())
511            }
512            SourcePlan::Subquery { body, .. } => self
513                .bind_query_routines_for_storage(engine, body, params, outer)
514                .map(|_| ()),
515            SourcePlan::Values { rows, .. } => {
516                let input = outer.cloned().unwrap_or_default();
517                for expression in rows.iter_mut().flatten() {
518                    self.bind_scalar_routines_for_storage(
519                        engine, expression, &input, subqueries, params, outer,
520                    )?;
521                }
522                Ok(())
523            }
524            SourcePlan::Function {
525                name,
526                relations,
527                args,
528                ..
529            } => {
530                let local = crate::semantics::builtin_function_dispatch_name(name);
531                if crate::registry::is_operator_join_table_function(&local) {
532                    let (left, right) = operator_join_relation_schemas(
533                        &self.catalog,
534                        &self.resolution,
535                        relations.as_ref(),
536                    )?;
537                    let constant = RowSchema::default();
538                    for (position, expression) in args.iter_mut().enumerate() {
539                        let input = match position {
540                            0 => &left,
541                            1 => &right,
542                            _ => &constant,
543                        };
544                        self.bind_scalar_routines_for_storage(
545                            engine, expression, input, subqueries, params, outer,
546                        )?;
547                    }
548                    return Ok(());
549                }
550                let input = outer.cloned().unwrap_or_default();
551                for expression in args {
552                    self.bind_scalar_routines_for_storage(
553                        engine, expression, &input, subqueries, params, outer,
554                    )?;
555                }
556                Ok(())
557            }
558            SourcePlan::FunctionGroup { functions, .. } => {
559                for function in functions {
560                    let local = crate::semantics::builtin_function_dispatch_name(&function.name);
561                    if crate::registry::is_operator_join_table_function(&local) {
562                        let (left, right) = operator_join_relation_schemas(
563                            &self.catalog,
564                            &self.resolution,
565                            function.relations.as_ref(),
566                        )?;
567                        let constant = RowSchema::default();
568                        for (position, expression) in function.args.iter_mut().enumerate() {
569                            let input = match position {
570                                0 => &left,
571                                1 => &right,
572                                _ => &constant,
573                            };
574                            self.bind_scalar_routines_for_storage(
575                                engine, expression, input, subqueries, params, outer,
576                            )?;
577                        }
578                        continue;
579                    }
580                    let input = outer.cloned().unwrap_or_default();
581                    for expression in &mut function.args {
582                        self.bind_scalar_routines_for_storage(
583                            engine, expression, &input, subqueries, params, outer,
584                        )?;
585                    }
586                }
587                Ok(())
588            }
589            SourcePlan::Table { .. } => Ok(()),
590        }
591    }
592
593    fn bind_scalar_routines_for_storage(
594        &mut self,
595        engine: &dyn RoutineResolution,
596        expression: &mut ScalarExpr,
597        schema: &RowSchema,
598        subqueries: &[QueryPlan],
599        params: &[SQLParam],
600        outer: Option<&RowSchema>,
601    ) -> Result<(), SQLError> {
602        let schema = self.with_stored_outer_internal_aliases(schema);
603        let schema = &schema;
604        self.canonicalize_stored_outer_columns(expression, schema);
605        let mut failure = None;
606        crate::plan::rewrite_scalar_expression(expression, &mut |expression| {
607            if failure.is_some() {
608                return;
609            }
610            let ScalarExpr::Func {
611                name,
612                binding,
613                args,
614                ..
615            } = expression
616            else {
617                return;
618            };
619            if let Some(dispatch) = binding.as_ref().and_then(|binding| binding.dispatch) {
620                if let crate::ast::FunctionDispatch::NumericOperator(operator) = dispatch {
621                    let selected = (|| {
622                        let resolver = self.query_function_type_resolver_for_subqueries(
623                            engine, args, schema, subqueries, params, outer,
624                        )?;
625                        let (_, types, _) = crate::function_call_argument_signature(
626                            args,
627                            schema,
628                            params,
629                            Some(&resolver),
630                        )?;
631                        crate::type_resolution::numeric_operator_types(operator, &types)
632                    })();
633                    match selected {
634                        Ok(selected) => {
635                            binding
636                                .as_mut()
637                                .expect("structural operator binding")
638                                .argument_types = selected
639                                .arguments
640                                .iter()
641                                .map(crate::ColumnType::sql_name)
642                                .collect();
643                        }
644                        Err(error) => failure = Some(error),
645                    }
646                }
647                return;
648            }
649            if let Err(error) = self.bind_scalar_function_for_storage(
650                engine, name, binding, args, schema, subqueries, params, outer,
651            ) {
652                failure = Some(error);
653            }
654        });
655        failure.map_or(Ok(()), Err)
656    }
657
658    #[expect(
659        clippy::too_many_arguments,
660        reason = "keeps execution context inputs aligned"
661    )]
662    fn bind_scalar_function_for_storage(
663        &mut self,
664        engine: &dyn RoutineResolution,
665        name: &str,
666        binding: &mut Option<FunctionBinding>,
667        args: &[ScalarExpr],
668        schema: &RowSchema,
669        subqueries: &[QueryPlan],
670        params: &[SQLParam],
671        outer: Option<&RowSchema>,
672    ) -> Result<(), SQLError> {
673        let resolver = self.query_function_type_resolver_for_subqueries(
674            engine, args, schema, subqueries, params, outer,
675        )?;
676        let (argument_names, argument_types, explicit_variadic) =
677            crate::function_call_argument_signature(args, schema, params, Some(&resolver))?;
678        let selected = if crate::is_fixed_builtin(name) {
679            crate::resolve_fixed_builtin_call(
680                name,
681                binding.as_ref(),
682                &argument_names,
683                &argument_types,
684                explicit_variadic,
685                Some(&resolver),
686            )?
687            .map(|resolved| resolved.selected)
688        } else {
689            resolver.resolve_function_overload(
690                name,
691                binding.as_ref(),
692                &argument_names,
693                &argument_types,
694                explicit_variadic,
695            )?
696        };
697        if let Some(selected) = selected {
698            *binding = Some(selected.binding);
699        }
700        Ok(())
701    }
702}
703
704pub fn bind_query_plan_routines_for_storage(
705    engine: &dyn RoutineResolution,
706    plan: &mut QueryPlan,
707    params: &[SQLParam],
708    ctes: &BindingContext,
709    outer: Option<&RowSchema>,
710) -> Result<RowSchema, SQLError> {
711    SchemaScope::for_analysis(ctes)?.bind_query_routines_for_storage(engine, plan, params, outer)
712}
713
714/// Bind every routine call owned by a stored scalar expression and validate its complete query-valued descendants against the expression's row scope.
715pub fn bind_expression_plan_routines_for_storage(
716    engine: &dyn RoutineResolution,
717    plan: &mut ExpressionPlan,
718    params: &[SQLParam],
719    ctes: &BindingContext,
720    schema: &RowSchema,
721) -> Result<Option<ColumnType>, SQLError> {
722    let mut scope = SchemaScope::for_analysis(ctes)?;
723    scope.stored_expression_outer = Some(schema.clone());
724    for subquery in &mut plan.subqueries {
725        scope.bind_query_routines_for_storage(engine, subquery, params, Some(schema))?;
726    }
727    scope.bind_scalar_routines_for_storage(
728        engine,
729        &mut plan.scalar,
730        schema,
731        &plan.subqueries,
732        params,
733        None,
734    )?;
735    scope.bind_expression_type(engine, &plan.scalar, schema, &plan.subqueries, params, None)
736}