Skip to main content

uqa_sql/expr/
call_dispatch.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Scalar call dispatch after arguments have been normalized and evaluated.
8
9use uqa_core::{ArrayValue, Value};
10
11use crate::error::{Result, SQLError};
12
13use super::call_arguments::normalized_function_name;
14use super::context::EvalContext;
15use super::conversion::to_f64;
16use super::diagnostics::{unknown_function_error, value_type_name};
17use super::json::{jsonpath_candidate, jsonpath_match};
18use super::random;
19use super::scalar_dispatch::{eval_scalar_function, eval_sequence_function};
20
21mod named;
22mod production;
23mod temporal;
24use named::builtin_named_args;
25pub use production::{
26    eval_builtin_function_call_with_control, eval_generated_function_call_with_control,
27};
28
29/// Execute a scalar function after its argument expressions have already been evaluated.
30///
31/// This is the shared SQL-semantics kernel used by both the parser AST evaluator and the physical scalar IR evaluator. Keeping dispatch here avoids converting a physical expression back into an AST [`crate::ast::Expr`] merely to reuse built-in, sequence, registered, or user-defined function behavior.
32pub fn eval_function_call(
33    name: &str,
34    call_args: Vec<(Option<String>, Value)>,
35    ctx: &EvalContext<'_>,
36) -> Result<Value> {
37    eval_function_call_inner(name, call_args, ctx, true)
38}
39
40/// Execute a call whose stored binding selects a built-in routine. Dynamic
41/// runtime callbacks and SQL routines must not override this stable binding.
42pub fn eval_builtin_function_call(
43    name: &str,
44    call_args: Vec<(Option<String>, Value)>,
45    ctx: &EvalContext<'_>,
46) -> Result<Value> {
47    eval_function_call_inner(name, call_args, ctx, false)
48}
49
50#[expect(
51    clippy::too_many_lines,
52    reason = "builtin dispatch preserves arity, NULL, and error precedence"
53)]
54fn eval_function_call_inner(
55    name: &str,
56    call_args: Vec<(Option<String>, Value)>,
57    ctx: &EvalContext<'_>,
58    allow_dynamic_dispatch: bool,
59) -> Result<Value> {
60    let lower = normalized_function_name(name);
61    let lower = lower.as_ref();
62    let call_args = if super::enums::call_applies_output_functions(lower, &call_args)
63        && call_args
64            .iter()
65            .any(|(_, value)| super::enums::contains_enum_carrier(value))
66    {
67        super::enums::render_call_arguments(
68            ctx.engine.and_then(super::EngineHook::enum_labels),
69            call_args,
70        )?
71    } else {
72        call_args
73    };
74    let evaluated: Vec<Value> = call_args.iter().map(|(_, value)| value.clone()).collect();
75
76    if let Some(result) = super::current_time::eval_current_time(lower, &evaluated, Some(ctx)) {
77        return result;
78    }
79
80    if lower == "current_setting" {
81        return super::session_settings::current_setting(&evaluated, ctx);
82    }
83    if lower == "set_config" {
84        return super::session_settings::set_config(&evaluated, ctx);
85    }
86    if let Some(result) = super::session_sleep::eval_session_sleep(lower, &evaluated, ctx) {
87        return result;
88    }
89
90    if let Some(result) = random::eval_random_function(lower, &call_args, ctx) {
91        return result;
92    }
93    if lower == "random" && !evaluated.is_empty() {
94        return Err(SQLError::TypeMismatch("random takes no arguments".into()));
95    }
96    if lower == "setseed" {
97        let [value] = evaluated.as_slice() else {
98            return Err(SQLError::TypeMismatch("setseed takes 1 arg".into()));
99        };
100        let seed = to_f64(value)?;
101        if !seed.is_finite() || !(-1.0..=1.0).contains(&seed) {
102            return Err(SQLError::Routine {
103                sqlstate: "22023".into(),
104                message: format!("setseed parameter {seed} is out of allowed range [-1,1]"),
105            });
106        }
107        let engine = ctx.engine.ok_or_else(|| {
108            SQLError::Unsupported("setseed requires a logical engine session".into())
109        })?;
110        if !engine.set_random_seed(seed).map_err(SQLError::Internal)? {
111            return Err(SQLError::Unsupported(
112                "engine hook does not provide a session random stream".into(),
113            ));
114        }
115        return Ok(Value::Str(String::new()));
116    }
117
118    if lower == "current_schema" {
119        if !evaluated.is_empty() {
120            return Err(SQLError::TypeMismatch(
121                "current_schema takes no arguments".into(),
122            ));
123        }
124        let schema = match ctx.engine {
125            Some(engine) => engine.current_schema().map_err(SQLError::Internal)?,
126            None => Some("public".to_string()),
127        };
128        return Ok(schema.map_or(Value::Null, Value::Str));
129    }
130    if lower == "current_schemas" {
131        let [Value::Bool(include_implicit)] = evaluated.as_slice() else {
132            return Err(SQLError::TypeMismatch(
133                "current_schemas takes one boolean argument".into(),
134            ));
135        };
136        let schemas = ctx
137            .engine
138            .map(|engine| engine.current_schemas(*include_implicit))
139            .transpose()
140            .map_err(SQLError::Internal)?
141            .flatten()
142            .unwrap_or_else(|| {
143                let mut schemas = Vec::new();
144                if *include_implicit {
145                    schemas.push("pg_catalog".to_string());
146                }
147                schemas.push("public".to_string());
148                schemas
149            });
150        return ArrayValue::try_new(schemas.into_iter().map(Value::Str).collect())
151            .map(Value::Array)
152            .ok_or_else(|| SQLError::TypeMismatch("invalid current_schemas result".into()));
153    }
154    if matches!(lower, "current_user" | "session_user") {
155        if !evaluated.is_empty() {
156            return Err(SQLError::TypeMismatch(format!(
157                "{lower} takes no arguments"
158            )));
159        }
160        let user = ctx
161            .engine
162            .map(|engine| {
163                if lower == "current_user" {
164                    engine.current_user()
165                } else {
166                    engine.session_user()
167                }
168            })
169            .transpose()?
170            .flatten()
171            .unwrap_or_else(|| "uqa".to_string());
172        return Ok(Value::Str(user));
173    }
174    let regobject_type = match lower {
175        "to_regproc" => Some(crate::ast::ColumnType::Regproc),
176        "to_regprocedure" => Some(crate::ast::ColumnType::Regprocedure),
177        "to_regclass" => Some(crate::ast::ColumnType::Regclass),
178        "to_regcollation" => Some(crate::ast::ColumnType::Regcollation),
179        "to_regnamespace" => Some(crate::ast::ColumnType::Regnamespace),
180        "to_regrole" => Some(crate::ast::ColumnType::Regrole),
181        "to_regtype" => Some(crate::ast::ColumnType::Regtype),
182        _ => None,
183    };
184    if let Some(regobject_type) = regobject_type {
185        let [value] = evaluated.as_slice() else {
186            return Err(SQLError::BadArity {
187                name: lower.into(),
188                expected: "1".into(),
189                actual: evaluated.len(),
190            });
191        };
192        let name = match value {
193            Value::Null => return Ok(Value::Null),
194            Value::Str(name) | Value::FixedChar(name) => name,
195            value => {
196                return Err(SQLError::TypeMismatch(format!(
197                    "{lower} requires text, got {}",
198                    value_type_name(value)
199                )));
200            }
201        };
202        let oid = ctx
203            .engine
204            .map(|engine| engine.resolve_regobject(&regobject_type, name))
205            .transpose()?
206            .flatten();
207        return Ok(oid.map_or(Value::Null, Value::Int));
208    }
209
210    // Functions registered in the operator registry (text_match,
211    // knn_match, ...) are dispatched by the relational/access-path
212    // executor. JSONPath fts_match is the scalar exception.
213    if crate::registry::is_registered(lower) {
214        if lower == "fts_match" && jsonpath_candidate(&evaluated) {
215            return jsonpath_match(&evaluated);
216        }
217        return Err(SQLError::Unsupported(format!(
218            "scalar evaluation of `{name}` is not supported (use the function registry)"
219        )));
220    }
221
222    if call_args.iter().any(|(name, _)| name.is_some()) {
223        if let Some(positional) = builtin_named_args(
224            lower,
225            &call_args,
226            &uqa_core::memory::ProductionControl::uncontrolled(),
227        )? {
228            return eval_scalar_function(lower, &positional);
229        }
230        if let Some(engine) = ctx.engine.filter(|_| allow_dynamic_dispatch) {
231            if let Some(result) = engine.call_user_function(lower, &call_args) {
232                return result;
233            }
234        }
235        return Err(unknown_function_error(lower, &call_args));
236    }
237
238    // Sequence functions use engine-owned session state and therefore precede pure built-in dispatch.
239    if matches!(lower, "nextval" | "currval" | "lastval" | "setval") {
240        return eval_sequence_function(lower, &evaluated, ctx);
241    }
242    if let Some(engine) = ctx
243        .engine
244        .filter(|engine| allow_dynamic_dispatch && engine.has_scalar_functions())
245    {
246        if let Some(result) = engine.call_scalar_function(lower, &evaluated) {
247            return result;
248        }
249    }
250    if lower == "date_trunc" {
251        if let Some(result) = super::time::truncate_session_zone(&evaluated, ctx) {
252            return result;
253        }
254    }
255    if let Some(result) = temporal::extract_session_zone(lower, &evaluated, ctx) {
256        return result;
257    }
258    if matches!(lower, "coalesce" | "nullif" | "greatest" | "least") {
259        return super::scalar_core::selection_with_context(
260            lower,
261            &evaluated,
262            &uqa_core::memory::ProductionControl::uncontrolled(),
263            ctx,
264        )
265        .map(|value| {
266            value
267                .into_uncontrolled()
268                .expect("ordinary selection result")
269        });
270    }
271    match eval_scalar_function(lower, &evaluated) {
272        // Unknown built-in: fall through to user-defined functions,
273        // mirroring PostgreSQL's search-path order.
274        Err(SQLError::UnknownFunction(_)) => {
275            if let Some(engine) = ctx.engine.filter(|_| allow_dynamic_dispatch) {
276                if let Some(result) = engine.call_user_function(lower, &call_args) {
277                    return result;
278                }
279            }
280            Err(unknown_function_error(lower, &call_args))
281        }
282        other => other,
283    }
284}