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    let _interval_style = super::super::interval_style::call_scope(
56        binding.map_or(name, |binding| binding.name.as_str()),
57        &arguments,
58        context.engine,
59        control,
60    )?;
61    if let Some((binding, dispatch)) =
62        binding.and_then(|binding| binding.dispatch.map(|dispatch| (binding, dispatch)))
63    {
64        if arguments.iter().any(|(name, _)| name.is_some()) {
65            return Err(SQLError::Internal(format!(
66                "bound {} expression retained a named argument",
67                dispatch.label(),
68            )));
69        }
70        let evaluated = MovedArguments::new(arguments, control)?;
71        return eval_dispatched(binding, dispatch, &evaluated.values, control, context);
72    }
73    let name = binding.map_or(name, |binding| binding.name.as_str());
74    let normalized =
75        super::super::call_arguments::normalized_function_name_with_control(name, control)?;
76    let name = normalized.as_ref();
77    let value = if arguments.iter().any(|(name, _)| name.is_some()) {
78        let Some(positional) = builtin_named_args(name, &arguments, control)? else {
79            return Err(super::super::diagnostics::unknown_function_error(
80                name, &arguments,
81            ));
82        };
83        eval_scalar(name, &positional, control, context)?
84    } else {
85        let evaluated = MovedArguments::new(arguments, control)?;
86        eval_scalar(name, &evaluated.values, control, context)?
87    };
88    if matches!(*value, Value::Int(_) | Value::Float(_)) {
89        if let Some(binding) = binding {
90            if let Some(ty) = crate::fixed_builtin_return_type_with_control(binding, control)? {
91                if matches!(
92                    *ty,
93                    crate::ColumnType::SmallInteger
94                        | crate::ColumnType::Integer
95                        | crate::ColumnType::Real
96                ) {
97                    let name = ty.sql_name_with_control(control)?;
98                    return super::super::cast_value_from_with_control(
99                        &value, &name, None, control,
100                    );
101                }
102            }
103        }
104    }
105    Ok(value)
106}
107
108fn eval_dispatched(
109    binding: &FunctionBinding,
110    dispatch: FunctionDispatch,
111    arguments: &[Value],
112    control: &ProductionControl<'_>,
113    context: &super::EvalContext<'_>,
114) -> Result<Produced<Value>> {
115    if let FunctionDispatch::Enum {
116        operation,
117        type_oid,
118    } = dispatch
119    {
120        return super::super::enums::enum_function_value_with_control(
121            context
122                .engine
123                .and_then(super::super::EngineHook::enum_labels),
124            operation,
125            type_oid,
126            arguments,
127            context.enum_comparison_state(),
128            control,
129        );
130    }
131    if let Some(result) = super::super::scalar_postgres::eval_comparison_with_context(
132        dispatch, arguments, control, context,
133    ) {
134        return result;
135    }
136    super::super::builtin::eval_dispatched_builtin_with_control(
137        binding, dispatch, arguments, control, context,
138    )
139}
140
141fn eval_scalar(
142    name: &str,
143    arguments: &[Value],
144    control: &ProductionControl<'_>,
145    context: &super::EvalContext<'_>,
146) -> Result<Produced<Value>> {
147    if matches!(name, "coalesce" | "nullif" | "greatest" | "least") {
148        return super::super::scalar_core::selection_with_context(
149            name, arguments, control, context,
150        );
151    }
152    super::super::scalar_dispatch::eval_generated_scalar_function_with_context(
153        name, arguments, control, context,
154    )
155}
156
157/// 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.
158struct MovedArguments {
159    values: Vec<Value>,
160    remaining: std::vec::IntoIter<(Option<String>, Value)>,
161    _memory: Option<MemoryReservation>,
162}
163
164impl MovedArguments {
165    fn new(
166        arguments: Produced<Vec<(Option<String>, Value)>>,
167        control: &ProductionControl<'_>,
168    ) -> Result<Self> {
169        let mut values = ProductionVec::new(*control);
170        values.reserve(arguments.len())?;
171        let (values, values_memory) = values.finish()?.into_parts();
172        let (arguments, argument_memory) = arguments.into_parts();
173        let mut output = Self {
174            values,
175            remaining: arguments.into_iter(),
176            _memory: control.combine(values_memory, argument_memory),
177        };
178        for (_, value) in output.remaining.by_ref() {
179            control.check()?;
180            output.values.push(value);
181        }
182        control.check()?;
183        Ok(output)
184    }
185}
186
187#[cfg(test)]
188mod tests;