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