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