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_with_context, 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 _interval_style = super::interval_style::call_scope(
63        lower,
64        &call_args,
65        ctx.engine,
66        &uqa_core::memory::ProductionControl::uncontrolled(),
67    )?;
68    let call_args = if super::enums::call_applies_output_functions(lower, &call_args)
69        && call_args
70            .iter()
71            .any(|(_, value)| super::enums::contains_enum_carrier(value))
72    {
73        super::enums::render_call_arguments(
74            ctx.engine.and_then(super::EngineHook::enum_labels),
75            call_args,
76        )?
77    } else {
78        call_args
79    };
80    let evaluated: Vec<Value> = call_args.iter().map(|(_, value)| value.clone()).collect();
81
82    if let Some(result) = super::current_time::eval_current_time(lower, &evaluated, Some(ctx)) {
83        return result;
84    }
85
86    if lower == "current_setting" {
87        return super::session_settings::current_setting(&evaluated, ctx);
88    }
89    if lower == "set_config" {
90        return super::session_settings::set_config(&evaluated, ctx);
91    }
92    if let Some(result) = super::session_sleep::eval_session_sleep(lower, &evaluated, ctx) {
93        return result;
94    }
95
96    if let Some(result) = random::eval_random_function(lower, &call_args, ctx) {
97        return result;
98    }
99    if lower == "random" && !evaluated.is_empty() {
100        return Err(SQLError::TypeMismatch("random takes no arguments".into()));
101    }
102    if lower == "setseed" {
103        let [value] = evaluated.as_slice() else {
104            return Err(SQLError::TypeMismatch("setseed takes 1 arg".into()));
105        };
106        let seed = to_f64(value)?;
107        if !seed.is_finite() || !(-1.0..=1.0).contains(&seed) {
108            return Err(SQLError::Routine {
109                sqlstate: "22023".into(),
110                message: format!("setseed parameter {seed} is out of allowed range [-1,1]"),
111            });
112        }
113        let engine = ctx.engine.ok_or_else(|| {
114            SQLError::Unsupported("setseed requires a logical engine session".into())
115        })?;
116        if !engine.set_random_seed(seed).map_err(SQLError::Internal)? {
117            return Err(SQLError::Unsupported(
118                "engine hook does not provide a session random stream".into(),
119            ));
120        }
121        return Ok(Value::Str(String::new()));
122    }
123
124    if lower == "current_schema" {
125        if !evaluated.is_empty() {
126            return Err(SQLError::TypeMismatch(
127                "current_schema takes no arguments".into(),
128            ));
129        }
130        let schema = match ctx.engine {
131            Some(engine) => engine.current_schema().map_err(SQLError::Internal)?,
132            None => Some("public".to_string()),
133        };
134        return Ok(schema.map_or(Value::Null, Value::Str));
135    }
136    if lower == "current_schemas" {
137        let [Value::Bool(include_implicit)] = evaluated.as_slice() else {
138            return Err(SQLError::TypeMismatch(
139                "current_schemas takes one boolean argument".into(),
140            ));
141        };
142        let schemas = ctx
143            .engine
144            .map(|engine| engine.current_schemas(*include_implicit))
145            .transpose()
146            .map_err(SQLError::Internal)?
147            .flatten()
148            .unwrap_or_else(|| {
149                let mut schemas = Vec::new();
150                if *include_implicit {
151                    schemas.push("pg_catalog".to_string());
152                }
153                schemas.push("public".to_string());
154                schemas
155            });
156        return ArrayValue::try_new(schemas.into_iter().map(Value::Str).collect())
157            .map(Value::Array)
158            .ok_or_else(|| SQLError::TypeMismatch("invalid current_schemas result".into()));
159    }
160    if matches!(lower, "current_user" | "session_user") {
161        if !evaluated.is_empty() {
162            return Err(SQLError::TypeMismatch(format!(
163                "{lower} takes no arguments"
164            )));
165        }
166        let user = ctx
167            .engine
168            .map(|engine| {
169                if lower == "current_user" {
170                    engine.current_user()
171                } else {
172                    engine.session_user()
173                }
174            })
175            .transpose()?
176            .flatten()
177            .unwrap_or_else(|| "uqa".to_string());
178        return Ok(Value::Str(user));
179    }
180    let regobject_type = match lower {
181        "to_regproc" => Some(crate::ast::ColumnType::Regproc),
182        "to_regprocedure" => Some(crate::ast::ColumnType::Regprocedure),
183        "to_regclass" => Some(crate::ast::ColumnType::Regclass),
184        "to_regcollation" => Some(crate::ast::ColumnType::Regcollation),
185        "to_regnamespace" => Some(crate::ast::ColumnType::Regnamespace),
186        "to_regrole" => Some(crate::ast::ColumnType::Regrole),
187        "to_regtype" => Some(crate::ast::ColumnType::Regtype),
188        _ => None,
189    };
190    if let Some(regobject_type) = regobject_type {
191        let [value] = evaluated.as_slice() else {
192            return Err(SQLError::BadArity {
193                name: lower.into(),
194                expected: "1".into(),
195                actual: evaluated.len(),
196            });
197        };
198        let name = match value {
199            Value::Null => return Ok(Value::Null),
200            Value::Str(name) | Value::FixedChar(name) => name,
201            value => {
202                return Err(SQLError::TypeMismatch(format!(
203                    "{lower} requires text, got {}",
204                    value_type_name(value)
205                )));
206            }
207        };
208        let oid = ctx
209            .engine
210            .map(|engine| engine.resolve_regobject(&regobject_type, name))
211            .transpose()?
212            .flatten();
213        return Ok(oid.map_or(Value::Null, Value::Int));
214    }
215
216    // Functions registered in the operator registry (text_match,
217    // knn_match, ...) are dispatched by the relational/access-path
218    // executor. JSONPath fts_match is the scalar exception.
219    if crate::registry::is_registered(lower) {
220        if lower == "fts_match" && jsonpath_candidate(&evaluated) {
221            return jsonpath_match(&evaluated);
222        }
223        return Err(SQLError::Unsupported(format!(
224            "scalar evaluation of `{name}` is not supported (use the function registry)"
225        )));
226    }
227
228    if call_args.iter().any(|(name, _)| name.is_some()) {
229        if let Some(positional) = builtin_named_args(
230            lower,
231            &call_args,
232            &uqa_core::memory::ProductionControl::uncontrolled(),
233        )? {
234            return eval_scalar_function_with_context(lower, &positional, ctx);
235        }
236        if let Some(engine) = ctx.engine.filter(|_| allow_dynamic_dispatch) {
237            if let Some(result) = engine.call_user_function(lower, &call_args) {
238                return result;
239            }
240        }
241        return Err(unknown_function_error(lower, &call_args));
242    }
243
244    // Sequence functions use engine-owned session state and therefore precede pure built-in dispatch.
245    if matches!(lower, "nextval" | "currval" | "lastval" | "setval") {
246        return eval_sequence_function(lower, &evaluated, ctx);
247    }
248    if let Some(engine) = ctx
249        .engine
250        .filter(|engine| allow_dynamic_dispatch && engine.has_scalar_functions())
251    {
252        if let Some(result) = engine.call_scalar_function(lower, &evaluated) {
253            return result;
254        }
255    }
256    if lower == "date_trunc" {
257        if let Some(result) = super::time::truncate_session_zone(&evaluated, ctx) {
258            return result;
259        }
260    }
261    if let Some(result) = temporal::extract_session_zone(lower, &evaluated, ctx) {
262        return result;
263    }
264    if matches!(lower, "coalesce" | "nullif" | "greatest" | "least") {
265        return super::scalar_core::selection_with_context(
266            lower,
267            &evaluated,
268            &uqa_core::memory::ProductionControl::uncontrolled(),
269            ctx,
270        )
271        .map(|value| {
272            value
273                .into_uncontrolled()
274                .expect("ordinary selection result")
275        });
276    }
277    match eval_scalar_function_with_context(lower, &evaluated, ctx) {
278        // Unknown built-in: fall through to user-defined functions,
279        // mirroring PostgreSQL's search-path order.
280        Err(SQLError::UnknownFunction(_)) => {
281            if let Some(engine) = ctx.engine.filter(|_| allow_dynamic_dispatch) {
282                if let Some(result) = engine.call_user_function(lower, &call_args) {
283                    return result;
284                }
285            }
286            Err(unknown_function_error(lower, &call_args))
287        }
288        other => other,
289    }
290}