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