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 outer_slot = slot - outer_start;
56            let Some(column) = outer.unique_internal_column_for_slot(outer_slot) else {
57                return;
58            };
59            *node = ScalarExpr::InternalColumn(column);
60        });
61    }
62
63    fn bind_cte_routines_for_storage(
64        &mut self,
65        routines: &dyn RoutineResolution,
66        body: &mut crate::plan::CtePlanBody,
67        params: &[SQLParam],
68        outer: Option<&RowSchema>,
69    ) -> Result<RowSchema, SQLError> {
70        let crate::plan::CtePlanBody::Command(command) = body else {
71            return self.bind_query_routines_for_storage(
72                routines,
73                body.query_mut().expect("query CTE body"),
74                params,
75                outer,
76            );
77        };
78        self.bind_command_routines_for_storage(routines, command, params, outer)?;
79        self.bind_command_returning(routines, command, params)
80    }
81
82    pub(super) fn bind_cte_routine_schemas(
83        &mut self,
84        routines: &dyn RoutineResolution,
85        ctes: &mut [crate::plan::CtePlan],
86        params: &[SQLParam],
87        outer: Option<&RowSchema>,
88    ) -> Result<Vec<(String, bool, Option<RowSchema>)>, SQLError> {
89        let ordered_names = crate::semantics::ordered_cte_plans(ctes)?
90            .into_iter()
91            .map(|cte| cte.name.clone())
92            .collect::<Vec<_>>();
93        let mut previous = Vec::with_capacity(ordered_names.len());
94        for name in ordered_names {
95            let position = ctes
96                .iter()
97                .position(|cte| cte.name == name)
98                .ok_or_else(|| SQLError::Internal(format!("ordered CTE `{name}` disappeared")))?;
99            let self_recursive = cte_references_own_name(&ctes[position]);
100            if let Some(cycle) = ctes[position].cycle.as_mut() {
101                let schema = RowSchema::default();
102                self.bind_scalar_routines_for_storage(
103                    routines,
104                    &mut cycle.mark_value,
105                    &schema,
106                    &[],
107                    params,
108                )?;
109                self.bind_scalar_routines_for_storage(
110                    routines,
111                    &mut cycle.mark_default,
112                    &schema,
113                    &[],
114                    params,
115                )?;
116            }
117            let provisional = if self_recursive {
118                self.bind_recursive_seed(
119                    routines,
120                    ctes[position]
121                        .body
122                        .query()
123                        .ok_or_else(|| SQLError::Routine {
124                            sqlstate: "42P19".into(),
125                            message: format!(
126                                "recursive query \"{}\" must not contain data-modifying statements",
127                                ctes[position].name
128                            ),
129                        })?,
130                    params,
131                    outer,
132                )?
133            } else {
134                self.bind_cte_routines_for_storage(
135                    routines,
136                    &mut ctes[position].body,
137                    params,
138                    outer,
139                )?
140            };
141            let columns = ctes[position].columns.clone();
142            let provisional = rename_schema(&provisional, &columns, None);
143            let provisional = if self_recursive {
144                extend_recursive_cte_binding_schema(routines, &ctes[position], provisional, params)?
145            } else {
146                extend_cte_generated_schema(routines, &ctes[position], provisional, params)?
147            };
148            previous.push((
149                name.clone(),
150                self.set_cte_returning(&ctes[position]),
151                self.ctes.insert(name.clone(), provisional),
152            ));
153            if self_recursive {
154                let complete = self.bind_cte_routines_for_storage(
155                    routines,
156                    &mut ctes[position].body,
157                    params,
158                    outer,
159                )?;
160                let complete = rename_schema(&complete, &columns, None);
161                let complete =
162                    extend_cte_generated_schema(routines, &ctes[position], complete, params)?;
163                self.ctes.insert(name, complete);
164            }
165        }
166
167        Ok(previous)
168    }
169
170    pub(super) fn bind_query_routines_for_storage(
171        &mut self,
172        routines: &dyn RoutineResolution,
173        plan: &mut QueryPlan,
174        params: &[SQLParam],
175        outer: Option<&RowSchema>,
176    ) -> Result<RowSchema, SQLError> {
177        let previous = self.bind_cte_routine_schemas(routines, &mut plan.ctes, params, outer)?;
178
179        let result = self.bind_root_routines_for_storage(routines, &mut plan.root, params, outer);
180        self.restore_cte_schemas(previous);
181        result
182    }
183
184    #[expect(
185        clippy::too_many_lines,
186        reason = "preserves SELECT schema and row identity"
187    )]
188    fn bind_root_routines_for_storage(
189        &mut self,
190        routines: &dyn RoutineResolution,
191        root: &mut RelationalPlan,
192        params: &[SQLParam],
193        outer: Option<&RowSchema>,
194    ) -> Result<RowSchema, SQLError> {
195        match root {
196            RelationalPlan::QueryBlock(block) => {
197                // The sources' own expressions resolve before the sources are analyzed, so that an analysis sees the names a nested query resolved, such as its output names in GROUP BY.
198                if let Some(source) = block.from.as_mut() {
199                    self.bind_source_routines_for_storage(
200                        routines,
201                        source,
202                        &block.subqueries,
203                        params,
204                        outer,
205                    )?;
206                }
207                let source_schema = match block.from.as_mut() {
208                    Some(source) => self.bind_source_for_execution(
209                        routines,
210                        source,
211                        &block.subqueries,
212                        params,
213                        outer,
214                    )?,
215                    None => RowSchema::default(),
216                };
217                let expression_schema = overlay_outer_schema(&source_schema, outer);
218                for subquery in &mut block.subqueries {
219                    self.bind_query_routines_for_storage(
220                        routines,
221                        subquery,
222                        params,
223                        Some(&expression_schema),
224                    )?;
225                }
226            }
227            RelationalPlan::SetOp {
228                left,
229                right,
230                subqueries,
231                ..
232            } => {
233                self.bind_query_routines_for_storage(routines, left, params, outer)?;
234                self.bind_query_routines_for_storage(routines, right, params, outer)?;
235                for subquery in subqueries {
236                    self.bind_query_routines_for_storage(routines, subquery, params, outer)?;
237                }
238            }
239            RelationalPlan::Values { subqueries, .. } => {
240                for subquery in subqueries {
241                    self.bind_query_routines_for_storage(routines, subquery, params, outer)?;
242                }
243            }
244        }
245
246        let set_output = match &*root {
247            RelationalPlan::SetOp { .. } => Some(self.bind_root(routines, root, params, outer)?),
248            RelationalPlan::QueryBlock(_) | RelationalPlan::Values { .. } => None,
249        };
250        match root {
251            RelationalPlan::QueryBlock(block) => {
252                if block.from.is_none()
253                    && block
254                        .projections
255                        .iter()
256                        .any(|projection| matches!(projection.expr, ScalarExpr::Star))
257                    && outer.is_none()
258                {
259                    return Err(SQLError::Routine {
260                        sqlstate: "42601".into(),
261                        message: "SELECT * with no tables specified is not valid".into(),
262                    });
263                }
264                let source_schema = block.from.as_ref().map_or_else(
265                    || Ok(RowSchema::default()),
266                    |source| self.bind_source(routines, source, &block.subqueries, params, outer),
267                )?;
268                let expression_schema = overlay_outer_schema(&source_schema, outer);
269                if let Some(filter) = block.r#where.as_mut() {
270                    self.bind_scalar_routines_for_storage(
271                        routines,
272                        filter,
273                        &expression_schema,
274                        &block.subqueries,
275                        params,
276                    )?;
277                }
278                let labels = super::routine_parameters::column_labels(&block.projections);
279                for projection in &mut block.projections {
280                    self.bind_scalar_routines_for_storage(
281                        routines,
282                        &mut projection.expr,
283                        &expression_schema,
284                        &block.subqueries,
285                        params,
286                    )?;
287                }
288                super::routine_parameters::keep_column_labels(&mut block.projections, labels);
289                if self.preserve_syntax_shape {
290                    // Stored syntax keeps `*` and output-name references as written; expanding a copy still reports their errors.
291                    let mut expanded = (**block).clone();
292                    expanded.projections = crate::semantics::expand_bound_projection_stars(
293                        &block.projections,
294                        &source_schema,
295                    )?;
296                    self.bind_grouping_variable_sites(&mut expanded, &source_schema);
297                    crate::semantics::grouping_sets::bind_grouping_names(
298                        routines,
299                        &mut expanded,
300                        &source_schema,
301                        None,
302                        params,
303                    )?;
304                } else {
305                    block.projections = crate::semantics::expand_bound_projection_stars(
306                        &block.projections,
307                        &source_schema,
308                    )?;
309                    self.bind_grouping_variable_sites(block, &source_schema);
310                    crate::semantics::grouping_sets::bind_grouping_names(
311                        routines,
312                        block,
313                        &source_schema,
314                        None,
315                        params,
316                    )?;
317                }
318                let output_names = self.output_names(&block.projections);
319                for expression in block
320                    .group_by
321                    .iter_mut()
322                    .chain(block.grouping_sets.iter_mut().flatten())
323                {
324                    // A preserved GROUP BY output name wins over a routine parameter when no local input column takes it. Its expression was already validated in the expanded copy.
325                    if self.preserve_syntax_shape
326                        && matches!(expression, ScalarExpr::Column(name)
327                            if !source_schema.has_unqualified_column(name)
328                                && !source_schema.column_is_ambiguous(name)
329                                && output_names.contains(name))
330                    {
331                        continue;
332                    }
333                    self.bind_scalar_routines_for_storage(
334                        routines,
335                        expression,
336                        &expression_schema,
337                        &block.subqueries,
338                        params,
339                    )?;
340                }
341                if let Some(having) = block.having.as_mut() {
342                    self.bind_scalar_routines_for_storage(
343                        routines,
344                        having,
345                        &expression_schema,
346                        &block.subqueries,
347                        params,
348                    )?;
349                }
350                // A bare name in ORDER BY or DISTINCT ON names an output column before any input column or parameter, as `findTargetlistEntrySQL92` resolves it.
351                let names_output = |expression: &ScalarExpr| match expression {
352                    ScalarExpr::Column(name) => output_names.contains(name),
353                    _ => false,
354                };
355                for order in &mut block.order_by {
356                    if names_output(&order.expr)
357                        || self.output_takes_variable_site(&order.expr, &output_names)
358                    {
359                        continue;
360                    }
361                    self.bind_scalar_routines_for_storage(
362                        routines,
363                        &mut order.expr,
364                        &expression_schema,
365                        &block.subqueries,
366                        params,
367                    )?;
368                }
369                if let Some(limit) = block.limit.as_mut() {
370                    self.bind_scalar_routines_for_storage(
371                        routines,
372                        limit,
373                        &expression_schema,
374                        &block.subqueries,
375                        params,
376                    )?;
377                }
378                if let Some(offset) = block.offset.as_mut() {
379                    self.bind_scalar_routines_for_storage(
380                        routines,
381                        offset,
382                        &expression_schema,
383                        &block.subqueries,
384                        params,
385                    )?;
386                }
387                for expression in &mut block.distinct_on {
388                    if names_output(expression)
389                        || self.output_takes_variable_site(expression, &output_names)
390                    {
391                        continue;
392                    }
393                    self.bind_scalar_routines_for_storage(
394                        routines,
395                        expression,
396                        &expression_schema,
397                        &block.subqueries,
398                        params,
399                    )?;
400                }
401                for expression in block
402                    .windows
403                    .iter_mut()
404                    .flat_map(|window| window.spec.expressions_mut())
405                {
406                    self.bind_scalar_routines_for_storage(
407                        routines,
408                        expression,
409                        &expression_schema,
410                        &block.subqueries,
411                        params,
412                    )?;
413                }
414            }
415            RelationalPlan::SetOp {
416                order_by,
417                limit,
418                offset,
419                subqueries,
420                ..
421            } => {
422                let output = overlay_outer_schema(
423                    set_output
424                        .as_ref()
425                        .expect("set-operation output schema was bound before routine expressions"),
426                    outer,
427                );
428                let output = &output;
429                for order in order_by {
430                    self.bind_scalar_routines_for_storage(
431                        routines,
432                        &mut order.expr,
433                        output,
434                        subqueries,
435                        params,
436                    )?;
437                }
438                if let Some(limit) = limit {
439                    self.bind_scalar_routines_for_storage(
440                        routines, limit, output, subqueries, params,
441                    )?;
442                }
443                if let Some(offset) = offset {
444                    self.bind_scalar_routines_for_storage(
445                        routines, offset, output, subqueries, params,
446                    )?;
447                }
448            }
449            RelationalPlan::Values { rows, subqueries } => {
450                let input = outer.cloned().unwrap_or_default();
451                for expression in rows.iter_mut().flatten() {
452                    self.bind_scalar_routines_for_storage(
453                        routines, expression, &input, subqueries, params,
454                    )?;
455                }
456            }
457        }
458        self.bind_root(routines, root, params, outer)
459    }
460
461    #[expect(
462        clippy::too_many_lines,
463        reason = "preserves SELECT schema and row identity"
464    )]
465    pub(super) fn bind_source_routines_for_storage(
466        &mut self,
467        engine: &dyn RoutineResolution,
468        source: &mut SourcePlan,
469        subqueries: &[QueryPlan],
470        params: &[SQLParam],
471        outer: Option<&RowSchema>,
472    ) -> Result<(), SQLError> {
473        match source {
474            SourcePlan::Join {
475                left,
476                right,
477                on,
478                lateral,
479                ..
480            } => {
481                self.bind_source_routines_for_storage(engine, left, subqueries, params, outer)?;
482                let left_schema = self.bind_source(engine, left, subqueries, params, outer)?;
483                let implicit_lateral_function = matches!(
484                    right.as_ref(),
485                    SourcePlan::Function { .. } | SourcePlan::FunctionGroup { .. }
486                );
487                let right_scope = (*lateral || implicit_lateral_function)
488                    .then(|| overlay_outer_schema(&left_schema, outer));
489                let right_outer = right_scope.as_ref().or(outer);
490                self.bind_source_routines_for_storage(
491                    engine,
492                    right,
493                    subqueries,
494                    params,
495                    right_outer,
496                )?;
497                if let Some(on) = on {
498                    let right_schema =
499                        self.bind_source(engine, right, subqueries, params, right_outer)?;
500                    let input = RowSchema::join(&left_schema, &right_schema, std::iter::empty());
501                    let input = overlay_outer_schema(&input, outer);
502                    self.bind_scalar_routines_for_storage(engine, on, &input, subqueries, params)?;
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,
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,
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,
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,
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,
578                        )?;
579                    }
580                }
581                Ok(())
582            }
583            SourcePlan::Table { .. } => Ok(()),
584        }
585    }
586
587    pub(super) 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    ) -> Result<(), SQLError> {
595        let schema = self.with_stored_outer_internal_aliases(schema);
596        let schema = &schema;
597        self.canonicalize_stored_outer_columns(expression, schema);
598        self.expand_stored_whole_rows(expression, schema);
599        self.canonicalize_routine_parameters(expression, schema);
600        self.resolve_variable_sites(expression, schema);
601        match self.scalar_binding {
602            super::ScalarBindingMode::References => return Ok(()),
603            super::ScalarBindingMode::CompositeInputs => {
604                return self.retain_composite_inputs_in_scope(
605                    engine, expression, schema, subqueries, params,
606                )
607            }
608            super::ScalarBindingMode::Stored => {}
609        }
610        let mut failure = None;
611        crate::plan::rewrite_scalar_expression(expression, &mut |expression| {
612            if failure.is_some() {
613                return;
614            }
615            if matches!(expression, ScalarExpr::Func { order_syntax, name, binding, order_by, .. }
616                if super::ordered_calls::uses_ordered_arguments(*order_syntax, name, binding.as_ref(), order_by.len())
617                    || super::ordered_calls::is_ordered_set(name))
618            {
619                if let Err(error) = self.bind_ordered_function_for_storage(
620                    engine, expression, schema, subqueries, params,
621                ) {
622                    failure = Some(error);
623                }
624                return;
625            }
626            let ScalarExpr::Func {
627                name,
628                binding,
629                args,
630                ..
631            } = expression
632            else {
633                return;
634            };
635            if let Some(dispatch) = binding.as_ref().and_then(|binding| binding.dispatch) {
636                if let crate::ast::FunctionDispatch::NumericOperator(operator) = dispatch {
637                    let selected = (|| {
638                        let resolver = self.query_function_type_resolver_for_subqueries(
639                            engine, args, schema, subqueries, params,
640                        )?;
641                        let (_, types, _) = crate::function_call_argument_signature(
642                            args,
643                            schema,
644                            params,
645                            Some(&resolver),
646                        )?;
647                        crate::type_resolution::numeric_operator_types(operator, &types)
648                    })();
649                    match selected {
650                        Ok(selected) => {
651                            binding
652                                .as_mut()
653                                .expect("structural operator binding")
654                                .argument_types = selected
655                                .arguments
656                                .iter()
657                                .map(crate::ColumnType::sql_name)
658                                .collect();
659                        }
660                        Err(error) => failure = Some(error),
661                    }
662                }
663                return;
664            }
665            if let Err(error) = self.bind_scalar_function_for_storage(
666                engine, name, binding, args, schema, subqueries, params,
667            ) {
668                failure = Some(error);
669                return;
670            }
671            if !self.preserve_syntax_shape {
672                if let Err(error) =
673                    crate::type_resolution::json_path::normalize_retained(expression)
674                {
675                    failure = Some(error);
676                }
677            }
678        });
679        failure.map_or(Ok(()), Err)?;
680        let mut comparison_inputs = false;
681        expression.visit(&mut |node| {
682            comparison_inputs |= matches!(node, ScalarExpr::Func { binding: Some(binding), .. }
683                if matches!(binding.dispatch, Some(crate::ast::FunctionDispatch::IsDistinct | crate::ast::FunctionDispatch::ValueNullTest { .. })));
684        });
685        if comparison_inputs {
686            self.retain_composite_inputs_in_scope(engine, expression, schema, subqueries, params)?;
687        }
688        self.bind_stored_scalar_types(engine, expression, schema, subqueries, params)?;
689        self.record_composite_dependencies(engine, expression, schema, subqueries, params)
690    }
691
692    /// Name user-defined types by OID identity and keep the enum constants that binding coerces from `unknown` literals by label identity, as `PostgreSQL` stores type and label OIDs in analyzed expressions.
693    fn bind_stored_scalar_types(
694        &mut self,
695        engine: &dyn RoutineResolution,
696        expression: &mut ScalarExpr,
697        schema: &RowSchema,
698        subqueries: &[QueryPlan],
699        params: &[SQLParam],
700    ) -> Result<(), SQLError> {
701        super::stored_types::bind_scalar_type_identities(expression, &mut |name| {
702            engine.resolve_type_name(name)
703        })?;
704        let resolver = self.query_function_type_resolver_for_subqueries(
705            engine,
706            std::slice::from_ref(expression),
707            schema,
708            subqueries,
709            params,
710        )?;
711        // The coercions binding adds to an operator's operands, the constants it reads from `unknown` literals and the relabels of `oid` alias operands, are stored, as `PostgreSQL` stores them in an analyzed expression.
712        if self.preserve_syntax_shape {
713            crate::type_resolution::store_syntax_operand_coercions(
714                expression, schema, params, &resolver,
715            )?;
716        } else {
717            crate::type_resolution::store_operand_coercions(expression, schema, params, &resolver)?;
718        }
719        if !crate::type_resolution::contains_unknown_literal(expression) {
720            return Ok(());
721        }
722        crate::type_resolution::fold_stored_enum_constants(expression, schema, params, &resolver)
723            .map(drop)
724    }
725
726    #[expect(
727        clippy::too_many_arguments,
728        reason = "keeps execution context inputs aligned"
729    )]
730    fn bind_scalar_function_for_storage(
731        &mut self,
732        engine: &dyn RoutineResolution,
733        name: &str,
734        binding: &mut Option<FunctionBinding>,
735        args: &[ScalarExpr],
736        schema: &RowSchema,
737        subqueries: &[QueryPlan],
738        params: &[SQLParam],
739    ) -> Result<(), SQLError> {
740        let resolver = self.query_function_type_resolver_for_subqueries(
741            engine, args, schema, subqueries, params,
742        )?;
743        let (argument_names, argument_types, explicit_variadic) =
744            crate::function_call_argument_signature(args, schema, params, Some(&resolver))?;
745        let selected = if crate::is_fixed_builtin(name) {
746            crate::resolve_fixed_builtin_call(
747                name,
748                binding.as_ref(),
749                &argument_names,
750                &argument_types,
751                explicit_variadic,
752                Some(&resolver),
753            )?
754            .map(|resolved| resolved.selected)
755        } else if crate::catalog::json_path::JsonPathFunction::lookup(name).is_some() {
756            crate::type_resolution::json_path::resolve_call(
757                name,
758                binding.as_ref(),
759                crate::type_resolution::RoutineCallDescriptor {
760                    argument_names: &argument_names,
761                    argument_types: &argument_types,
762                    explicit_variadic,
763                },
764                Some(&resolver),
765                &uqa_core::memory::ProductionControl::uncontrolled(),
766            )?
767            .map(|selected| {
768                selected
769                    .into_uncontrolled()
770                    .expect("ordinary stored binding")
771            })
772        } else if let Some(array) = crate::type_resolution::resolve_array_transform_call(
773            name,
774            binding.as_ref(),
775            args,
776            &argument_types,
777            params,
778            explicit_variadic,
779            &resolver,
780        )? {
781            array.overload
782        } else {
783            resolver.resolve_function_overload(
784                name,
785                binding.as_ref(),
786                &argument_names,
787                &argument_types,
788                explicit_variadic,
789            )?
790        };
791        if let Some(selected) = selected {
792            *binding = Some(selected.binding);
793        }
794        Ok(())
795    }
796}
797
798pub fn bind_query_plan_routines_for_storage(
799    engine: &dyn RoutineResolution,
800    plan: &mut QueryPlan,
801    params: &[SQLParam],
802    ctes: &BindingContext,
803    outer: Option<&RowSchema>,
804) -> Result<RowSchema, SQLError> {
805    SchemaScope::for_analysis(ctes)?.bind_query_routines_for_storage(engine, plan, params, outer)
806}
807
808/// Bind a copy of a query lowered from stored syntax, keeping its shape so every bound identity can be carried back to that syntax.
809pub fn bind_syntax_query_plan_routines(
810    engine: &dyn RoutineResolution,
811    plan: &mut QueryPlan,
812    params: &[SQLParam],
813    ctes: &BindingContext,
814    outer: Option<&RowSchema>,
815) -> Result<RowSchema, SQLError> {
816    let mut scope = SchemaScope::for_analysis(ctes)?;
817    scope.preserve_syntax_shape = true;
818    scope.bind_query_routines_for_storage(engine, plan, params, outer)
819}
820
821/// Bind every routine call owned by a stored scalar expression and validate its complete query-valued descendants against the expression's row scope. The plan keeps the shape of the syntax it was lowered from.
822pub fn bind_expression_plan_routines_for_storage(
823    engine: &dyn RoutineResolution,
824    plan: &mut ExpressionPlan,
825    params: &[SQLParam],
826    ctes: &BindingContext,
827    schema: &RowSchema,
828) -> Result<Option<ColumnType>, SQLError> {
829    bind_stored_expression(engine, plan, params, ctes, schema, None)
830}
831
832pub(super) fn bind_rule_condition_routines(
833    engine: &dyn RoutineResolution,
834    plan: &mut ExpressionPlan,
835    ctes: &BindingContext,
836    schema: &RowSchema,
837    event: crate::ast::RuleEvent,
838) -> Result<Option<ColumnType>, SQLError> {
839    bind_stored_expression(engine, plan, &[], ctes, schema, Some(event))
840}
841
842fn bind_stored_expression(
843    engine: &dyn RoutineResolution,
844    plan: &mut ExpressionPlan,
845    params: &[SQLParam],
846    ctes: &BindingContext,
847    schema: &RowSchema,
848    rule_event: Option<crate::ast::RuleEvent>,
849) -> Result<Option<ColumnType>, SQLError> {
850    let mut scope = SchemaScope::for_analysis(ctes)?;
851    scope.rule_condition_event = rule_event;
852    scope.preserve_syntax_shape = true;
853    scope.stored_expression_outer = Some(schema.clone());
854    for subquery in &mut plan.subqueries {
855        scope.bind_query_routines_for_storage(engine, subquery, params, Some(schema))?;
856    }
857    scope.bind_scalar_routines_for_storage(
858        engine,
859        &mut plan.scalar,
860        schema,
861        &plan.subqueries,
862        params,
863    )?;
864    scope.bind_expression_type(engine, &plan.scalar, schema, &plan.subqueries, params)
865}