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::scalar_dispatch::{eval_scalar_function, eval_sequence_function};
19use super::{array_transform, json_strip, random};
20
21/// Execute a scalar function after its argument expressions have already been evaluated.
22///
23/// 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.
24pub fn eval_function_call(
25    name: &str,
26    call_args: Vec<(Option<String>, Value)>,
27    ctx: &EvalContext<'_>,
28) -> Result<Value> {
29    eval_function_call_inner(name, call_args, ctx, true)
30}
31
32/// Execute a call whose stored binding selects a built-in routine. Dynamic
33/// runtime callbacks and SQL routines must not override this stable binding.
34pub fn eval_builtin_function_call(
35    name: &str,
36    call_args: Vec<(Option<String>, Value)>,
37    ctx: &EvalContext<'_>,
38) -> Result<Value> {
39    eval_function_call_inner(name, call_args, ctx, false)
40}
41
42#[expect(
43    clippy::too_many_lines,
44    reason = "builtin dispatch preserves arity, NULL, and error precedence"
45)]
46fn eval_function_call_inner(
47    name: &str,
48    call_args: Vec<(Option<String>, Value)>,
49    ctx: &EvalContext<'_>,
50    allow_dynamic_dispatch: bool,
51) -> Result<Value> {
52    let lower = normalized_function_name(name);
53    let lower = lower.as_ref();
54    let evaluated: Vec<Value> = call_args.iter().map(|(_, value)| value.clone()).collect();
55
56    if let Some(result) = super::current_time::eval_current_time(lower, &evaluated, Some(ctx)) {
57        return result;
58    }
59
60    if let Some(result) = random::eval_random_function(lower, &call_args, ctx) {
61        return result;
62    }
63    if lower == "random" && !evaluated.is_empty() {
64        return Err(SQLError::TypeMismatch("random takes no arguments".into()));
65    }
66    if lower == "setseed" {
67        let [value] = evaluated.as_slice() else {
68            return Err(SQLError::TypeMismatch("setseed takes 1 arg".into()));
69        };
70        let seed = to_f64(value)?;
71        if !seed.is_finite() || !(-1.0..=1.0).contains(&seed) {
72            return Err(SQLError::Routine {
73                sqlstate: "22023".into(),
74                message: format!("setseed parameter {seed} is out of allowed range [-1,1]"),
75            });
76        }
77        let engine = ctx.engine.ok_or_else(|| {
78            SQLError::Unsupported("setseed requires a logical engine session".into())
79        })?;
80        if !engine.set_random_seed(seed).map_err(SQLError::Internal)? {
81            return Err(SQLError::Unsupported(
82                "engine hook does not provide a session random stream".into(),
83            ));
84        }
85        return Ok(Value::Str(String::new()));
86    }
87
88    if lower == "current_schema" {
89        if !evaluated.is_empty() {
90            return Err(SQLError::TypeMismatch(
91                "current_schema takes no arguments".into(),
92            ));
93        }
94        let schema = ctx
95            .engine
96            .map(|engine| engine.current_schema())
97            .transpose()
98            .map_err(SQLError::Internal)?
99            .flatten()
100            .unwrap_or_else(|| "public".to_string());
101        return Ok(Value::Str(schema));
102    }
103    if lower == "current_schemas" {
104        let [Value::Bool(include_implicit)] = evaluated.as_slice() else {
105            return Err(SQLError::TypeMismatch(
106                "current_schemas takes one boolean argument".into(),
107            ));
108        };
109        let schemas = ctx
110            .engine
111            .map(|engine| engine.current_schemas(*include_implicit))
112            .transpose()
113            .map_err(SQLError::Internal)?
114            .flatten()
115            .unwrap_or_else(|| {
116                let mut schemas = Vec::new();
117                if *include_implicit {
118                    schemas.push("pg_catalog".to_string());
119                }
120                schemas.push("public".to_string());
121                schemas
122            });
123        return ArrayValue::try_new(schemas.into_iter().map(Value::Str).collect())
124            .map(Value::Array)
125            .ok_or_else(|| SQLError::TypeMismatch("invalid current_schemas result".into()));
126    }
127    if matches!(lower, "current_user" | "session_user") {
128        if !evaluated.is_empty() {
129            return Err(SQLError::TypeMismatch(format!(
130                "{lower} takes no arguments"
131            )));
132        }
133        let user = ctx
134            .engine
135            .map(|engine| {
136                if lower == "current_user" {
137                    engine.current_user()
138                } else {
139                    engine.session_user()
140                }
141            })
142            .transpose()
143            .map_err(SQLError::Internal)?
144            .flatten()
145            .unwrap_or_else(|| "uqa".to_string());
146        return Ok(Value::Str(user));
147    }
148    let regobject_type = match lower {
149        "to_regproc" => Some(crate::ast::ColumnType::Regproc),
150        "to_regprocedure" => Some(crate::ast::ColumnType::Regprocedure),
151        "to_regclass" => Some(crate::ast::ColumnType::Regclass),
152        "to_regnamespace" => Some(crate::ast::ColumnType::Regnamespace),
153        "to_regrole" => Some(crate::ast::ColumnType::Regrole),
154        "to_regtype" => Some(crate::ast::ColumnType::Regtype),
155        _ => None,
156    };
157    if let Some(regobject_type) = regobject_type {
158        let [value] = evaluated.as_slice() else {
159            return Err(SQLError::BadArity {
160                name: lower.into(),
161                expected: "1".into(),
162                actual: evaluated.len(),
163            });
164        };
165        let name = match value {
166            Value::Null => return Ok(Value::Null),
167            Value::Str(name) | Value::FixedChar(name) => name,
168            value => {
169                return Err(SQLError::TypeMismatch(format!(
170                    "{lower} requires text, got {}",
171                    value_type_name(value)
172                )));
173            }
174        };
175        let oid = ctx
176            .engine
177            .map(|engine| engine.resolve_regobject(&regobject_type, name))
178            .transpose()?
179            .flatten();
180        return Ok(oid.map_or(Value::Null, Value::Int));
181    }
182
183    // Functions registered in the operator registry (text_match,
184    // knn_match, ...) are dispatched by the relational/access-path
185    // executor. JSONPath fts_match is the scalar exception.
186    if crate::registry::is_registered(lower) {
187        if lower == "fts_match" && jsonpath_candidate(&evaluated) {
188            return jsonpath_match(&evaluated);
189        }
190        return Err(SQLError::Unsupported(format!(
191            "scalar evaluation of `{name}` is not supported (use the function registry)"
192        )));
193    }
194
195    if call_args.iter().any(|(name, _)| name.is_some()) {
196        if let Some(positional) = builtin_named_args(lower, &call_args) {
197            return eval_scalar_function(lower, &positional);
198        }
199        if let Some(engine) = ctx.engine.filter(|_| allow_dynamic_dispatch) {
200            if let Some(result) = engine.call_user_function(lower, &call_args) {
201                return result;
202            }
203        }
204        return Err(unknown_function_error(lower, &call_args));
205    }
206
207    // Sequence functions use engine-owned session state and therefore precede pure built-in dispatch.
208    if matches!(lower, "nextval" | "currval" | "lastval" | "setval") {
209        return eval_sequence_function(lower, &evaluated, ctx);
210    }
211    if let Some(engine) = ctx
212        .engine
213        .filter(|engine| allow_dynamic_dispatch && engine.has_scalar_functions())
214    {
215        if let Some(result) = engine.call_scalar_function(lower, &evaluated) {
216            return result;
217        }
218    }
219    match eval_scalar_function(lower, &evaluated) {
220        // Unknown built-in: fall through to user-defined functions,
221        // mirroring PostgreSQL's search-path order.
222        Err(SQLError::UnknownFunction(_)) => {
223            if let Some(engine) = ctx.engine.filter(|_| allow_dynamic_dispatch) {
224                if let Some(result) = engine.call_user_function(lower, &call_args) {
225                    return result;
226                }
227            }
228            Err(unknown_function_error(lower, &call_args))
229        }
230        other => other,
231    }
232}
233
234fn builtin_named_args(function: &str, call_args: &[(Option<String>, Value)]) -> Option<Vec<Value>> {
235    if matches!(function, "array_sort" | "array_reverse") {
236        return array_transform::reorder_named_values(function, call_args);
237    }
238    if matches!(function, "json_strip_nulls" | "jsonb_strip_nulls") {
239        return json_strip::reorder_named_values(function, call_args);
240    }
241    let names: &[&str] = match function {
242        "regexp_count" => match call_args.len() {
243            2 => &["string", "pattern"],
244            3 => &["string", "pattern", "start"],
245            4 => &["string", "pattern", "start", "flags"],
246            _ => return None,
247        },
248        "regexp_like" => match call_args.len() {
249            2 => &["string", "pattern"],
250            3 => &["string", "pattern", "flags"],
251            _ => return None,
252        },
253        "regexp_substr" => match call_args.len() {
254            2 => &["string", "pattern"],
255            3 => &["string", "pattern", "start"],
256            4 => &["string", "pattern", "start", "N"],
257            5 => &["string", "pattern", "start", "N", "flags"],
258            6 => &["string", "pattern", "start", "N", "flags", "subexpr"],
259            _ => return None,
260        },
261        "regexp_instr" => match call_args.len() {
262            2 => &["string", "pattern"],
263            3 => &["string", "pattern", "start"],
264            4 => &["string", "pattern", "start", "N"],
265            5 => &["string", "pattern", "start", "N", "endoption"],
266            6 => &["string", "pattern", "start", "N", "endoption", "flags"],
267            7 => &[
268                "string",
269                "pattern",
270                "start",
271                "N",
272                "endoption",
273                "flags",
274                "subexpr",
275            ],
276            _ => return None,
277        },
278        "regexp_replace" => match call_args.len() {
279            3 => &["string", "pattern", "replacement"],
280            4 if call_args
281                .iter()
282                .any(|(name, _)| name.as_deref() == Some("flags")) =>
283            {
284                &["string", "pattern", "replacement", "flags"]
285            }
286            4 => &["string", "pattern", "replacement", "start"],
287            5 => &["string", "pattern", "replacement", "start", "N"],
288            6 => &["string", "pattern", "replacement", "start", "N", "flags"],
289            _ => return None,
290        },
291        "make_interval" => return make_interval_named_args(call_args),
292        _ => return None,
293    };
294    reorder_named_args(call_args, names)
295}
296
297fn reorder_named_args(
298    call_args: &[(Option<String>, Value)],
299    parameter_names: &[&str],
300) -> Option<Vec<Value>> {
301    if call_args.len() != parameter_names.len() {
302        return None;
303    }
304    let mut slots = vec![None; parameter_names.len()];
305    let mut positional_index = 0;
306    let mut saw_named = false;
307    for (name, value) in call_args {
308        let slot = if let Some(name) = name {
309            saw_named = true;
310            parameter_names
311                .iter()
312                .position(|candidate| candidate == name)?
313        } else {
314            if saw_named {
315                return None;
316            }
317            let slot = positional_index;
318            positional_index += 1;
319            slot
320        };
321        if slots.get(slot)?.is_some() {
322            return None;
323        }
324        slots[slot] = Some(value.clone());
325    }
326    slots.into_iter().collect()
327}
328
329/// Map `make_interval(name => value, ...)` onto the positional
330/// `(years, months, weeks, days, hours, mins, secs)` argument list.
331/// Returns `None` when an unknown parameter name appears.
332fn make_interval_named_args(call_args: &[(Option<String>, Value)]) -> Option<Vec<Value>> {
333    const NAMES: [&str; 7] = ["years", "months", "weeks", "days", "hours", "mins", "secs"];
334    let mut positional = vec![Value::Int(0); NAMES.len()];
335    let mut positional_index = 0;
336    let mut saw_named = false;
337    let mut assigned = [false; NAMES.len()];
338    for (name, value) in call_args {
339        let slot = if let Some(name) = name {
340            saw_named = true;
341            NAMES.iter().position(|candidate| candidate == name)?
342        } else {
343            if saw_named {
344                return None;
345            }
346            let slot = positional_index;
347            positional_index += 1;
348            slot
349        };
350        if slot >= NAMES.len() || assigned[slot] {
351            return None;
352        }
353        assigned[slot] = true;
354        positional[slot] = value.clone();
355    }
356    Some(positional)
357}