Skip to main content

uqa_sql/expr/call_dispatch/
production.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Evaluated arguments for validated generated built-ins keep their existing payload owner through SQL dispatch.
8
9use super::named::builtin_named_args;
10use crate::{
11    ast::{FunctionBinding, FunctionDispatch},
12    error::{Result, SQLError},
13};
14use uqa_core::{
15    memory::{MemoryReservation, Produced, ProductionControl, ProductionVec},
16    Value,
17};
18
19/// Execute a built-in selected by generated-column validation. This boundary has no session, host-function or query callbacks; general runtime dispatch remains with `eval_function_call`. Argument values arrive already admitted by their producers.
20pub fn eval_generated_function_call_with_control(
21    name: &str,
22    binding: Option<&FunctionBinding>,
23    arguments: Produced<Vec<(Option<String>, Value)>>,
24    control: &ProductionControl<'_>,
25) -> Result<Produced<Value>> {
26    eval_builtin_function_call_with_control(
27        name,
28        binding,
29        arguments,
30        control,
31        &super::EvalContext::new(None, &[]),
32    )
33}
34
35/// Execute SQL-owned native built-ins with admitted arguments and the caller's catalog/comparison context. Session and user-function callbacks keep their separate execution boundaries.
36pub fn eval_builtin_function_call_with_control(
37    name: &str,
38    binding: Option<&FunctionBinding>,
39    arguments: Produced<Vec<(Option<String>, Value)>>,
40    control: &ProductionControl<'_>,
41    context: &super::EvalContext<'_>,
42) -> Result<Produced<Value>> {
43    if let Some(error) = binding.and_then(|binding| binding.resolution_error.as_ref()) {
44        return Err(error.sql_error());
45    }
46    control.check()?;
47    if binding.is_some_and(|binding| !binding.builtin) {
48        return Err(SQLError::Unsupported(
49            "bound user function requires a logical engine session".into(),
50        ));
51    }
52    if let (Some(binding), Some(hook)) = (binding, context.engine) {
53        hook.require_builtin_execute(binding)?;
54    }
55    if let Some((binding, dispatch)) =
56        binding.and_then(|binding| binding.dispatch.map(|dispatch| (binding, dispatch)))
57    {
58        if arguments.iter().any(|(name, _)| name.is_some()) {
59            return Err(SQLError::Internal(format!(
60                "bound {} expression retained a named argument",
61                dispatch.label(),
62            )));
63        }
64        let evaluated = MovedArguments::new(arguments, control)?;
65        return eval_dispatched(binding, dispatch, &evaluated.values, control, context);
66    }
67    let name = binding.map_or(name, |binding| binding.name.as_str());
68    let normalized =
69        super::super::call_arguments::normalized_function_name_with_control(name, control)?;
70    let name = normalized.as_ref();
71    let value = if arguments.iter().any(|(name, _)| name.is_some()) {
72        let Some(positional) = builtin_named_args(name, &arguments, control)? else {
73            return Err(super::super::diagnostics::unknown_function_error(
74                name, &arguments,
75            ));
76        };
77        eval_scalar(name, &positional, control, context)?
78    } else {
79        let evaluated = MovedArguments::new(arguments, control)?;
80        eval_scalar(name, &evaluated.values, control, context)?
81    };
82    if matches!(*value, Value::Int(_) | Value::Float(_)) {
83        if let Some(binding) = binding {
84            if let Some(ty) = crate::fixed_builtin_return_type_with_control(binding, control)? {
85                if matches!(
86                    *ty,
87                    crate::ColumnType::SmallInteger
88                        | crate::ColumnType::Integer
89                        | crate::ColumnType::Real
90                ) {
91                    let name = ty.sql_name_with_control(control)?;
92                    return super::super::cast_value_from_with_control(
93                        &value, &name, None, control,
94                    );
95                }
96            }
97        }
98    }
99    Ok(value)
100}
101
102fn eval_dispatched(
103    binding: &FunctionBinding,
104    dispatch: FunctionDispatch,
105    arguments: &[Value],
106    control: &ProductionControl<'_>,
107    context: &super::EvalContext<'_>,
108) -> Result<Produced<Value>> {
109    if let FunctionDispatch::Enum {
110        operation,
111        type_oid,
112    } = dispatch
113    {
114        return super::super::enums::enum_function_value_with_control(
115            context
116                .engine
117                .and_then(super::super::EngineHook::enum_labels),
118            operation,
119            type_oid,
120            arguments,
121            context.enum_comparison_state(),
122            control,
123        );
124    }
125    if let Some(result) = super::super::scalar_postgres::eval_comparison_with_context(
126        dispatch, arguments, control, context,
127    ) {
128        return result;
129    }
130    super::super::builtin::eval_dispatched_builtin_with_control(
131        binding, dispatch, arguments, control, context,
132    )
133}
134
135fn eval_scalar(
136    name: &str,
137    arguments: &[Value],
138    control: &ProductionControl<'_>,
139    context: &super::EvalContext<'_>,
140) -> Result<Produced<Value>> {
141    if matches!(name, "coalesce" | "nullif" | "greatest" | "least") {
142        return super::super::scalar_core::selection_with_context(
143            name, arguments, control, context,
144        );
145    }
146    super::super::scalar_dispatch::eval_generated_scalar_function(name, arguments, control)
147}
148
149/// Moving positional values must not make a second payload copy. The new vector's complete capacity is admitted by Core before movement; the original argument buffer and payload lease remain with this guard until dispatch finishes. Both vectors precede the shared lease on every failure path.
150struct MovedArguments {
151    values: Vec<Value>,
152    remaining: std::vec::IntoIter<(Option<String>, Value)>,
153    _memory: Option<MemoryReservation>,
154}
155
156impl MovedArguments {
157    fn new(
158        arguments: Produced<Vec<(Option<String>, Value)>>,
159        control: &ProductionControl<'_>,
160    ) -> Result<Self> {
161        let mut values = ProductionVec::new(*control);
162        values.reserve(arguments.len())?;
163        let (values, values_memory) = values.finish()?.into_parts();
164        let (arguments, argument_memory) = arguments.into_parts();
165        let mut output = Self {
166            values,
167            remaining: arguments.into_iter(),
168            _memory: control.combine(values_memory, argument_memory),
169        };
170        for (_, value) in output.remaining.by_ref() {
171            control.check()?;
172            output.values.push(value);
173        }
174        control.check()?;
175        Ok(output)
176    }
177}
178
179#[cfg(test)]
180mod tests;