Skip to main content

uqa_sql/
expr.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Scalar expression evaluator: turns an [`Expr`] into a [`Value`] under
8//! a row context (column -> value) and a parameter binding.
9
10use std::borrow::Cow;
11
12use uqa_core::{ArrayValue, DecimalValue, TemporalValue, Value};
13
14use crate::ast::{BinaryOp, Expr};
15use crate::error::{Result, SQLError};
16use crate::params::SQLParam;
17use crate::result::ResultRow;
18
19mod encoding;
20mod json;
21mod time;
22
23use encoding::{base64_decode, base64_encode, md5_hex};
24pub use json::value_to_json_text;
25use json::{
26    format_jsonb_pretty, json_build_array_value, json_build_object_value, json_concat,
27    json_contained_by, json_contains, json_delete, json_delete_path, json_extract_path,
28    json_has_key, json_has_keys, json_typeof, jsonb_insert, jsonb_set, jsonpath_candidate,
29    jsonpath_exists, jsonpath_match, parse_json, strip_nulls, typed_json_value, value_to_json,
30};
31use time::{
32    age_between, coerce_temporal, date_trunc_value, extract_from_value, format_pg_number,
33    format_temporal, generate_random_uuid, generate_uuid_v7, hex_encode, make_timestamp,
34    parse_timestamp, pg_to_chrono_fmt,
35};
36mod binary;
37mod casting;
38mod conversion;
39mod scalar_array;
40mod scalar_core;
41mod scalar_dispatch;
42mod scalar_geospatial;
43mod scalar_helpers;
44mod scalar_json;
45mod scalar_math;
46mod scalar_postgres;
47mod scalar_temporal;
48
49use binary::{
50    compare, compare_nullable, eval_binary, eval_comparison_op, values_equal, values_equal_nullable,
51};
52pub(crate) use binary::{division_by_zero, out_of_range};
53pub use binary::{
54    eval_binary_values, eval_binary_values_with_integer_width, eval_comparison_truth,
55    integer_width_for_literal, integer_width_for_type, truthy, IntegerWidth,
56};
57pub use casting::{
58    array_dimensions, cast_value, cast_value_from, negate_value, parse_pg_array_literal,
59};
60pub(crate) use conversion::to_f64;
61use conversion::{
62    allocation_error, coerce_i64, expect_str, float1, float_to_i64_rounded, float_to_i64_trunc,
63    gcd_i64, initcap_str, nonnegative_usize, string1, to_decimal, to_i64,
64};
65pub use conversion::{array_value_to_string, value_to_string};
66pub use conversion::{value_to_tensor, value_to_vector};
67use scalar_dispatch::{eval_scalar_function, eval_sequence_function};
68use scalar_helpers::{
69    compile_pg_regex, like_match, point_xy, quote_literal, similar_to_regex, trim_chars,
70    typeof_value,
71};
72pub use scalar_helpers::{quote_ident, CompiledLikePattern};
73
74/// Engine-side hook that scalar function evaluation calls for stateful
75/// sequence and user-defined functions. Query-valued expressions are not
76/// accepted here: lowering assigns them physical query-plan slots executed by
77/// `uqa-execution::ScalarSubqueryRunner`.
78pub trait EngineHook {
79    fn nextval(&self, name: &str) -> std::result::Result<i64, String>;
80    fn currval(&self, name: &str) -> std::result::Result<i64, String>;
81    fn setval(&self, name: &str, value: i64) -> std::result::Result<i64, String>;
82
83    fn call_scalar_function(&self, _name: &str, _args: &[Value]) -> Option<Result<Value>> {
84        None
85    }
86
87    fn has_scalar_functions(&self) -> bool {
88        true
89    }
90
91    /// Resolve the first existing schema on the logical session's search
92    /// path. `None` lets standalone expression evaluation use its `public`
93    /// compatibility default.
94    fn current_schema(&self) -> std::result::Result<Option<String>, String> {
95        Ok(None)
96    }
97
98    /// Resolve the existing schemas visible to the logical session.
99    fn current_schemas(
100        &self,
101        _include_implicit: bool,
102    ) -> std::result::Result<Option<Vec<String>>, String> {
103        Ok(None)
104    }
105
106    /// Draw from an engine-owned logical-session PRNG. `None` keeps pure,
107    /// engine-free expression evaluation available for library callers.
108    fn random_value(&self) -> std::result::Result<Option<f64>, String> {
109        Ok(None)
110    }
111
112    /// Reseed the logical-session PRNG. `false` means the hook does not own a
113    /// mutable random stream and the caller must report the unsupported call.
114    fn set_random_seed(&self, _seed: f64) -> std::result::Result<bool, String> {
115        Ok(false)
116    }
117
118    /// Invoke a user-defined SQL / `PL/pgSQL` function. Consulted
119    /// after built-in dispatch misses (and immediately for calls with
120    /// named arguments, which built-ins never accept). `None` means
121    /// no user-defined function with this name exists.
122    fn call_user_function(
123        &self,
124        _name: &str,
125        _args: &[(Option<String>, Value)],
126    ) -> Option<Result<Value>> {
127        None
128    }
129
130    fn call_bound_user_function(
131        &self,
132        _binding: &crate::ast::FunctionBinding,
133        _args: &[(Option<String>, Value)],
134    ) -> Option<Result<Value>> {
135        None
136    }
137}
138
139/// Read-only row interface used by the expression evaluator. Most callers
140/// use a materialised [`ResultRow`], while hot execution paths can expose a
141/// projected value slice without rebuilding a string-keyed map for every row.
142pub trait RowLookup {
143    fn column(&self, name: &str) -> Option<&Value>;
144
145    /// Whether an unqualified name identifies more than one visible input
146    /// column. Callers must report SQLSTATE 42702 instead of selecting an
147    /// arbitrary suffix match.
148    fn column_is_ambiguous(&self, _name: &str) -> bool {
149        false
150    }
151
152    fn qualified_column(&self, qualifier: &str, column: &str) -> Option<&Value>;
153
154    /// Whether a qualified identity names more than one visible input column.
155    fn qualified_column_is_ambiguous(&self, _qualifier: &str, _column: &str) -> bool {
156        false
157    }
158
159    /// Return a value by the physical schema position used to construct this
160    /// row view. Materialized named rows do not expose positional access;
161    /// projected execution sources override it so compiled hot paths can avoid
162    /// repeating string lookup for every expression and row.
163    fn positional_column(&self, _index: usize) -> Option<&Value> {
164        None
165    }
166
167    /// Visit every logical column in schema order. Named rows use their map
168    /// order; positional execution rows override this without materializing a
169    /// map. The default keeps narrow projected lookup implementations source
170    /// compatible when they deliberately do not expose whole-row semantics.
171    fn visit_columns(&self, _visitor: &mut dyn FnMut(&str, &Value)) {}
172}
173
174impl RowLookup for ResultRow {
175    fn column(&self, name: &str) -> Option<&Value> {
176        self.get(name)
177    }
178
179    fn qualified_column(&self, _qualifier: &str, _column: &str) -> Option<&Value> {
180        None
181    }
182
183    fn visit_columns(&self, visitor: &mut dyn FnMut(&str, &Value)) {
184        for (column, value) in self {
185            visitor(column, value);
186        }
187    }
188}
189
190pub struct EvalContext<'a> {
191    pub row: Option<&'a ResultRow>,
192    row_lookup: Option<&'a dyn RowLookup>,
193    pub params: &'a [SQLParam],
194    pub engine: Option<&'a dyn EngineHook>,
195}
196
197impl<'a> EvalContext<'a> {
198    pub fn new(row: Option<&'a ResultRow>, params: &'a [SQLParam]) -> Self {
199        Self {
200            row,
201            row_lookup: row.map(|row| row as &dyn RowLookup),
202            params,
203            engine: None,
204        }
205    }
206
207    pub fn from_row_lookup(row: &'a dyn RowLookup, params: &'a [SQLParam]) -> Self {
208        Self {
209            // Whole-row materialization is needed only by correlated
210            // subqueries. Ordinary scalar evaluation must remain on the
211            // lookup/slot path.
212            row: None,
213            row_lookup: Some(row),
214            params,
215            engine: None,
216        }
217    }
218
219    pub fn with_engine(mut self, engine: &'a dyn EngineHook) -> Self {
220        self.engine = Some(engine);
221        self
222    }
223
224    fn row_lookup(&self) -> Result<&'a dyn RowLookup> {
225        self.row_lookup
226            .ok_or_else(|| SQLError::Internal("column reference without row context".into()))
227    }
228
229    /// Resolve an unqualified column through the same row semantics used by
230    /// the AST evaluator. Physical scalar IR evaluators call this instead of
231    /// reconstructing an [`Expr::Column`] carrier.
232    pub fn column_value(&self, name: &str) -> Result<Value> {
233        if self.row_lookup()?.column_is_ambiguous(name) {
234            return Err(SQLError::AmbiguousColumn(name.to_string()));
235        }
236        Ok(self
237            .row_lookup()?
238            .column(name)
239            .cloned()
240            .unwrap_or(Value::Null))
241    }
242
243    /// Resolve a qualified column without constructing an AST expression.
244    pub fn qualified_column_value(&self, qualifier: &str, column: &str) -> Result<Value> {
245        if self
246            .row_lookup()?
247            .qualified_column_is_ambiguous(qualifier, column)
248        {
249            return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
250        }
251        Ok(self
252            .row_lookup()?
253            .qualified_column(qualifier, column)
254            .cloned()
255            .unwrap_or(Value::Null))
256    }
257}
258
259/// Evaluate a value-producing expression. Function calls are *not*
260/// dispatched here; the compiler routes them through the function
261/// registry instead. Calling `eval` on a `Func` expr returns
262/// `Unsupported` so latent function-in-projection bugs surface loudly.
263pub fn eval(expr: &Expr, ctx: &EvalContext<'_>) -> Result<Value> {
264    match expr {
265        Expr::Default => Err(SQLError::Internal(
266            "DEFAULT reached scalar expression evaluation without a mutation target".into(),
267        )),
268        Expr::Literal(v) => Ok(v.clone()),
269        Expr::Param(i) => match i.checked_sub(1).and_then(|index| ctx.params.get(index)) {
270            Some(SQLParam::Scalar(v)) => Ok(v.clone()),
271            Some(SQLParam::Vector(v)) => Ok(Value::List(
272                v.iter().map(|x| Value::Float(f64::from(*x))).collect(),
273            )),
274            Some(SQLParam::Tensor(vectors)) => Ok(Value::List(
275                vectors
276                    .iter()
277                    .map(|vector| {
278                        Value::List(vector.iter().map(|x| Value::Float(f64::from(*x))).collect())
279                    })
280                    .collect(),
281            )),
282            None => Err(SQLError::MissingParam(*i)),
283        },
284        Expr::Column(name) => {
285            // Plain column refs match either an unqualified key or the
286            // suffix of a qualified `table.col` key, so the same row
287            // shape works for single-table SELECTs and JOIN tuples.
288            if ctx.row_lookup()?.column_is_ambiguous(name) {
289                return Err(SQLError::AmbiguousColumn(name.clone()));
290            }
291            Ok(ctx
292                .row_lookup()?
293                .column(name)
294                .cloned()
295                .unwrap_or(Value::Null))
296        }
297        Expr::QualifiedColumn { qualifier, column } => {
298            if ctx
299                .row_lookup()?
300                .qualified_column_is_ambiguous(qualifier, column)
301            {
302                return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
303            }
304            Ok(ctx
305                .row_lookup()?
306                .qualified_column(qualifier, column)
307                .cloned()
308                .unwrap_or(Value::Null))
309        }
310        Expr::Array(elements) => {
311            let mut out = Vec::with_capacity(elements.len());
312            for e in elements {
313                out.push(eval(e, ctx)?);
314            }
315            ArrayValue::try_new(out).map(Value::Array).ok_or_else(|| {
316                SQLError::TypeMismatch(
317                    "multidimensional arrays must have matching dimensions".into(),
318                )
319            })
320        }
321        Expr::Row(elements) => {
322            let mut out = Vec::with_capacity(elements.len());
323            for element in elements {
324                out.push(eval(element, ctx)?);
325            }
326            Ok(Value::Row(out))
327        }
328        Expr::Star | Expr::QualifiedStar(_) => {
329            Err(SQLError::Internal("`*` cannot be evaluated".into()))
330        }
331        Expr::Func {
332            name,
333            binding,
334            args,
335            ..
336        } => {
337            let call_args = evaluate_call_args(args, ctx)?;
338            if let Some(binding) = binding {
339                let engine = ctx.engine.ok_or_else(|| {
340                    SQLError::Unsupported(
341                        "bound user function requires a logical engine session".into(),
342                    )
343                })?;
344                engine
345                    .call_bound_user_function(binding, &call_args)
346                    .unwrap_or_else(|| Err(SQLError::UnknownFunction(binding.name.clone())))
347            } else {
348                eval_function_call(name, call_args, ctx)
349            }
350        }
351        Expr::WindowCall { name, .. } => Err(SQLError::Unsupported(format!(
352            "window function `{name}` must be evaluated by the window-aware executor"
353        ))),
354        Expr::Case {
355            base,
356            when,
357            else_branch,
358        } => {
359            let base_value = match base {
360                Some(b) => Some(eval(b, ctx)?),
361                None => None,
362            };
363            for (cond, result) in when {
364                let matched = match &base_value {
365                    Some(bv) => values_equal(bv, &eval(cond, ctx)?),
366                    None => truthy(&eval(cond, ctx)?),
367                };
368                if matched {
369                    return eval(result, ctx);
370                }
371            }
372            match else_branch {
373                Some(e) => eval(e, ctx),
374                None => Ok(Value::Null),
375            }
376        }
377        Expr::Cast { expr, ty } => {
378            let source_ty = explicit_expr_type(expr);
379            let v = eval(expr, ctx)?;
380            cast_value_from(&v, ty, source_ty)
381        }
382        Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. } => {
383            Err(SQLError::Unsupported(
384                "query-valued expressions must be lowered to physical ScalarExpr/QueryPlan slots"
385                    .into(),
386            ))
387        }
388        Expr::Binary { op, lhs, rhs } => eval_binary(*op, lhs, rhs, ctx),
389        Expr::UnaryMinus(inner) => {
390            let source_ty = explicit_expr_type(inner);
391            let value = eval(inner, ctx)?;
392            negate_value(&value, source_ty)
393        }
394        Expr::Not(inner) => {
395            // SQL three-valued logic: NOT NULL -> NULL.
396            let v = eval(inner, ctx)?;
397            if matches!(v, Value::Null) {
398                return Ok(Value::Null);
399            }
400            Ok(Value::Bool(!truthy(&v)))
401        }
402        Expr::And(items) => {
403            // Kleene AND: FALSE dominates, otherwise NULL taints.
404            let mut saw_null = false;
405            for item in items {
406                let v = eval(item, ctx)?;
407                if matches!(v, Value::Null) {
408                    saw_null = true;
409                } else if !truthy(&v) {
410                    return Ok(Value::Bool(false));
411                }
412            }
413            if saw_null {
414                return Ok(Value::Null);
415            }
416            Ok(Value::Bool(true))
417        }
418        Expr::Or(items) => {
419            // Kleene OR: TRUE dominates, otherwise NULL taints.
420            let mut saw_null = false;
421            for item in items {
422                let v = eval(item, ctx)?;
423                if matches!(v, Value::Null) {
424                    saw_null = true;
425                } else if truthy(&v) {
426                    return Ok(Value::Bool(true));
427                }
428            }
429            if saw_null {
430                return Ok(Value::Null);
431            }
432            Ok(Value::Bool(false))
433        }
434        Expr::IsNull { expr, negated } => {
435            let v = eval(expr, ctx)?;
436            let is_null = matches!(v, Value::Null);
437            Ok(Value::Bool(if *negated { !is_null } else { is_null }))
438        }
439        Expr::Between { expr, low, high } => {
440            let v = eval(expr, ctx)?;
441            let lo = eval(low, ctx)?;
442            let hi = eval(high, ctx)?;
443            eval_between(&v, &lo, &hi)
444        }
445        Expr::InList {
446            expr,
447            list,
448            negated,
449        } => {
450            // Three-valued IN: found -> TRUE, a NULL comparand (or a
451            // NULL needle) downgrades a miss to NULL.
452            let v = eval(expr, ctx)?;
453            let mut saw_null = matches!(v, Value::Null);
454            for item in list {
455                let candidate = eval(item, ctx)?;
456                match values_equal_nullable(&v, &candidate) {
457                    Some(true) => return Ok(Value::Bool(!*negated)),
458                    Some(false) => {}
459                    None => saw_null = true,
460                }
461            }
462            if saw_null {
463                return Ok(Value::Null);
464            }
465            Ok(Value::Bool(*negated))
466        }
467    }
468}
469
470fn explicit_expr_type(expr: &Expr) -> Option<&str> {
471    match expr {
472        Expr::Cast { ty, .. } => Some(ty),
473        Expr::Literal(Value::Int(value)) if i32::try_from(*value).is_ok() => Some("integer"),
474        Expr::Literal(Value::Int(_)) => Some("bigint"),
475        Expr::Literal(Value::Bytes(_)) => Some("bytea"),
476        _ => None,
477    }
478}
479
480/// `expr BETWEEN low AND high` under three-valued logic: a definite
481/// FALSE on either bound wins over a NULL on the other.
482fn eval_between(v: &Value, lo: &Value, hi: &Value) -> Result<Value> {
483    let ge = compare_nullable(v, lo)?.map(|ord| ord.is_ge());
484    let le = compare_nullable(v, hi)?.map(|ord| ord.is_le());
485    Ok(match (ge, le) {
486        (Some(false), _) | (_, Some(false)) => Value::Bool(false),
487        (Some(true), Some(true)) => Value::Bool(true),
488        _ => Value::Null,
489    })
490}
491
492fn normalized_function_name(name: &str) -> Cow<'_, str> {
493    let stripped = name.strip_prefix("pg_catalog.").unwrap_or(name);
494    if stripped.bytes().any(|byte| byte.is_ascii_uppercase()) {
495        Cow::Owned(stripped.to_ascii_lowercase())
496    } else {
497        Cow::Borrowed(stripped)
498    }
499}
500
501/// Marker function the compiler wraps `name => value` call arguments
502/// in (`NamedArgExpr` has no dedicated AST node).
503pub const NAMED_ARG_FUNCTION: &str = "__named_arg";
504
505/// Physical scalar built-ins selected after `PostgreSQL` overload resolution has preserved the declared integer width.
506pub const TO_HEX_INT4_FUNCTION: &str = "__to_hex_int4";
507pub const TO_HEX_INT8_FUNCTION: &str = "__to_hex_int8";
508
509/// Return the `PostgreSQL` 18 strictness contract for a built-in scalar call when its implemented overload is known.
510#[must_use]
511pub fn builtin_scalar_function_strictness(name: &str, argument_count: usize) -> Option<bool> {
512    let normalized = normalized_function_name(name);
513    match normalized.as_ref() {
514        "coalesce" | "greatest" | "least" if argument_count >= 1 => Some(false),
515        "nullif" | "concat_op" if argument_count == 2 => Some(false),
516        "concat" | "format" | "json_build_array" | "jsonb_build_array" | "json_build_object"
517        | "jsonb_build_object" | "num_nulls" | "num_nonnulls" => Some(false),
518        "concat_ws" if argument_count >= 1 => Some(false),
519        "quote_nullable" | "pg_typeof" | "typeof" if argument_count == 1 => Some(false),
520        "array_cat" | "array_append" | "array_prepend" | "array_remove" | "array_positions"
521            if argument_count == 2 =>
522        {
523            Some(false)
524        }
525        "array_position" if matches!(argument_count, 2 | 3) => Some(false),
526        "array_replace" if argument_count == 3 => Some(false),
527        "array_fill" if matches!(argument_count, 2 | 3) => Some(false),
528        "array_to_string" if argument_count == 3 => Some(false),
529        "string_to_array" | "string_to_table" if matches!(argument_count, 2 | 3) => Some(false),
530        "overlaps" if argument_count == 4 => Some(false),
531        "abs" | "acos" | "array_dims" | "array_ndims" | "array_reverse" | "ascii" | "asin"
532        | "atan" | "bit_length" | "cardinality" | "casefold" | "cbrt" | "ceil" | "ceiling"
533        | "char_length" | "character_length" | "chr" | "cos" | "cosh" | "current_schemas"
534        | "degrees" | "exp" | "factorial" | "floor" | "gamma" | "initcap" | "isfinite"
535        | "json_array_length" | "jsonb_array_length" | "json_typeof" | "jsonb_typeof"
536        | "jsonb_pretty" | "justify_hours" | "length" | "lgamma" | "ln" | "log10" | "log2"
537        | "lower" | "md5" | "octet_length" | "quote_ident" | "quote_literal" | "radians"
538        | "reverse" | "row_to_json" | "sign" | "sin" | "sinh" | "sqrt" | "tan" | "tanh"
539        | "to_hex" | TO_HEX_INT4_FUNCTION | TO_HEX_INT8_FUNCTION | "to_json" | "to_jsonb"
540        | "to_timestamp" | "upper"
541            if argument_count == 1 =>
542        {
543            Some(true)
544        }
545        "age" | "btrim" | "ltrim" | "rtrim" | "trim" | "log" | "round" | "trunc"
546        | "json_strip_nulls" | "jsonb_strip_nulls"
547            if matches!(argument_count, 1 | 2) =>
548        {
549            Some(true)
550        }
551        "array_sort" if matches!(argument_count, 1..=3) => Some(true),
552        "array_length" | "array_lower" | "array_upper" | "atan2" | "date_part" | "date_trunc"
553        | "decode" | "encode" | "extract" | "gcd" | "lcm" | "left" | "mod" | "power" | "pow"
554        | "repeat" | "right" | "starts_with" | "position" | "strpos" | "to_char" | "to_date"
555        | "to_number" | "trim_array" | "like" | "ilike" | "similar_to" | "point"
556        | "st_distance" | "st_within"
557            if argument_count == 2 =>
558        {
559            Some(true)
560        }
561        "array_to_string" if argument_count == 2 => Some(true),
562        "substring" | "substr" | "lpad" | "rpad" if matches!(argument_count, 2 | 3) => Some(true),
563        "regexp_count" if matches!(argument_count, 2..=4) => Some(true),
564        "regexp_instr" if matches!(argument_count, 2..=7) => Some(true),
565        "regexp_like" | "regexp_match" | "regexp_matches" if matches!(argument_count, 2 | 3) => {
566            Some(true)
567        }
568        "regexp_replace" if matches!(argument_count, 3..=6) => Some(true),
569        "regexp_substr" if matches!(argument_count, 2..=6) => Some(true),
570        "replace" | "split_part" | "translate" | "make_date" if argument_count == 3 => Some(true),
571        "__between_symmetric" if argument_count == 3 => Some(true),
572        "overlay" | "jsonb_set" | "jsonb_insert" if matches!(argument_count, 3 | 4) => Some(true),
573        "json_extract_path"
574        | "jsonb_extract_path"
575        | "json_extract_path_text"
576        | "jsonb_extract_path_text"
577            if argument_count >= 2 =>
578        {
579            Some(true)
580        }
581        "json_contains" | "json_contained_by" | "json_delete_path" | "json_has_key"
582        | "json_has_any_key" | "json_has_all_keys" | "jsonb_path_exists" | "jsonpath_exists"
583        | "jsonb_path_match" | "jsonpath_match"
584            if argument_count == 2 =>
585        {
586            Some(true)
587        }
588        "make_timestamp" if matches!(argument_count, 6 | 7) => Some(true),
589        "make_interval" if argument_count <= 7 => Some(true),
590        "width_bucket" if argument_count == 4 => Some(true),
591        "st_dwithin" if matches!(argument_count, 2 | 3) => Some(true),
592        _ => None,
593    }
594}
595
596/// Evaluate a call's argument list, unwrapping `name => value`
597/// markers into `(Some(name), value)` pairs.
598pub fn evaluate_call_args(
599    args: &[Expr],
600    ctx: &EvalContext<'_>,
601) -> Result<Vec<(Option<String>, Value)>> {
602    args.iter()
603        .map(|arg| match arg {
604            Expr::Func {
605                name, args: inner, ..
606            } if name == NAMED_ARG_FUNCTION => {
607                let Some(Expr::Literal(Value::Str(arg_name))) = inner.first() else {
608                    return Err(SQLError::Internal("named argument without a name".into()));
609                };
610                let value_expr = inner
611                    .get(1)
612                    .ok_or_else(|| SQLError::Internal("named argument without a value".into()))?;
613                Ok((Some(arg_name.clone()), eval(value_expr, ctx)?))
614            }
615            other => Ok((None, eval(other, ctx)?)),
616        })
617        .collect()
618}
619
620/// Execute a scalar function after its argument expressions have already
621/// been evaluated.
622///
623/// This is the shared SQL-semantics kernel used by both the parser AST
624/// evaluator and the physical scalar IR evaluator. Keeping dispatch here
625/// avoids converting a physical expression back into [`Expr`] merely to
626/// reuse built-in, sequence, registered, or user-defined function behavior.
627pub fn eval_function_call(
628    name: &str,
629    call_args: Vec<(Option<String>, Value)>,
630    ctx: &EvalContext<'_>,
631) -> Result<Value> {
632    let lower = normalized_function_name(name);
633    let lower = lower.as_ref();
634    let evaluated: Vec<Value> = call_args.iter().map(|(_, value)| value.clone()).collect();
635
636    if lower == "random" {
637        if !evaluated.is_empty() {
638            return Err(SQLError::TypeMismatch("random takes no arguments".into()));
639        }
640        if let Some(engine) = ctx.engine {
641            if let Some(value) = engine.random_value().map_err(SQLError::Internal)? {
642                return Ok(Value::Float(value));
643            }
644        }
645    }
646    if lower == "setseed" {
647        let [value] = evaluated.as_slice() else {
648            return Err(SQLError::TypeMismatch("setseed takes 1 arg".into()));
649        };
650        let seed = to_f64(value)?;
651        if !seed.is_finite() || !(-1.0..=1.0).contains(&seed) {
652            return Err(SQLError::Routine {
653                sqlstate: "22023".into(),
654                message: format!("setseed parameter {seed} is out of allowed range [-1,1]"),
655            });
656        }
657        let engine = ctx.engine.ok_or_else(|| {
658            SQLError::Unsupported("setseed requires a logical engine session".into())
659        })?;
660        if !engine.set_random_seed(seed).map_err(SQLError::Internal)? {
661            return Err(SQLError::Unsupported(
662                "engine hook does not provide a session random stream".into(),
663            ));
664        }
665        return Ok(Value::Str(String::new()));
666    }
667
668    if lower == "current_schema" {
669        if !evaluated.is_empty() {
670            return Err(SQLError::TypeMismatch(
671                "current_schema takes no arguments".into(),
672            ));
673        }
674        let schema = ctx
675            .engine
676            .map(|engine| engine.current_schema())
677            .transpose()
678            .map_err(SQLError::Internal)?
679            .flatten()
680            .unwrap_or_else(|| "public".to_string());
681        return Ok(Value::Str(schema));
682    }
683    if lower == "current_schemas" {
684        let [Value::Bool(include_implicit)] = evaluated.as_slice() else {
685            return Err(SQLError::TypeMismatch(
686                "current_schemas takes one boolean argument".into(),
687            ));
688        };
689        let schemas = ctx
690            .engine
691            .map(|engine| engine.current_schemas(*include_implicit))
692            .transpose()
693            .map_err(SQLError::Internal)?
694            .flatten()
695            .unwrap_or_else(|| {
696                let mut schemas = Vec::new();
697                if *include_implicit {
698                    schemas.push("pg_catalog".to_string());
699                }
700                schemas.push("public".to_string());
701                schemas
702            });
703        return ArrayValue::try_new(schemas.into_iter().map(Value::Str).collect())
704            .map(Value::Array)
705            .ok_or_else(|| SQLError::TypeMismatch("invalid current_schemas result".into()));
706    }
707
708    // Functions registered in the operator registry (text_match,
709    // knn_match, ...) are dispatched by the relational/access-path
710    // executor. JSONPath fts_match is the scalar exception.
711    if crate::registry::is_registered(lower) {
712        if lower == "fts_match" && jsonpath_candidate(&evaluated) {
713            return jsonpath_match(&evaluated);
714        }
715        return Err(SQLError::Unsupported(format!(
716            "scalar evaluation of `{name}` is not supported (use the function registry)"
717        )));
718    }
719
720    if call_args.iter().any(|(name, _)| name.is_some()) {
721        if let Some(positional) = builtin_named_args(lower, &call_args) {
722            return eval_scalar_function(lower, &positional);
723        }
724        if let Some(engine) = ctx.engine {
725            if let Some(result) = engine.call_user_function(lower, &call_args) {
726                return result;
727            }
728        }
729        return Err(unknown_function_error(lower, &call_args));
730    }
731
732    // Sequence functions mutate engine state and therefore precede pure
733    // built-in dispatch.
734    if matches!(lower, "nextval" | "currval" | "setval") {
735        return eval_sequence_function(lower, &evaluated, ctx);
736    }
737    if let Some(engine) = ctx.engine.filter(|engine| engine.has_scalar_functions()) {
738        if let Some(result) = engine.call_scalar_function(lower, &evaluated) {
739            return result;
740        }
741    }
742    match eval_scalar_function(lower, &evaluated) {
743        // Unknown built-in: fall through to user-defined functions,
744        // mirroring PostgreSQL's search-path order.
745        Err(SQLError::UnknownFunction(_)) => {
746            if let Some(engine) = ctx.engine {
747                if let Some(result) = engine.call_user_function(lower, &call_args) {
748                    return result;
749                }
750            }
751            Err(unknown_function_error(lower, &call_args))
752        }
753        other => other,
754    }
755}
756
757fn builtin_named_args(function: &str, call_args: &[(Option<String>, Value)]) -> Option<Vec<Value>> {
758    let names: &[&str] = match function {
759        "regexp_count" => match call_args.len() {
760            2 => &["string", "pattern"],
761            3 => &["string", "pattern", "start"],
762            4 => &["string", "pattern", "start", "flags"],
763            _ => return None,
764        },
765        "regexp_like" => match call_args.len() {
766            2 => &["string", "pattern"],
767            3 => &["string", "pattern", "flags"],
768            _ => return None,
769        },
770        "regexp_substr" => match call_args.len() {
771            2 => &["string", "pattern"],
772            3 => &["string", "pattern", "start"],
773            4 => &["string", "pattern", "start", "N"],
774            5 => &["string", "pattern", "start", "N", "flags"],
775            6 => &["string", "pattern", "start", "N", "flags", "subexpr"],
776            _ => return None,
777        },
778        "regexp_instr" => match call_args.len() {
779            2 => &["string", "pattern"],
780            3 => &["string", "pattern", "start"],
781            4 => &["string", "pattern", "start", "N"],
782            5 => &["string", "pattern", "start", "N", "endoption"],
783            6 => &["string", "pattern", "start", "N", "endoption", "flags"],
784            7 => &[
785                "string",
786                "pattern",
787                "start",
788                "N",
789                "endoption",
790                "flags",
791                "subexpr",
792            ],
793            _ => return None,
794        },
795        "regexp_replace" => match call_args.len() {
796            3 => &["string", "pattern", "replacement"],
797            4 if call_args
798                .iter()
799                .any(|(name, _)| name.as_deref() == Some("flags")) =>
800            {
801                &["string", "pattern", "replacement", "flags"]
802            }
803            4 => &["string", "pattern", "replacement", "start"],
804            5 => &["string", "pattern", "replacement", "start", "N"],
805            6 => &["string", "pattern", "replacement", "start", "N", "flags"],
806            _ => return None,
807        },
808        "make_interval" => return make_interval_named_args(call_args),
809        _ => return None,
810    };
811    reorder_named_args(call_args, names)
812}
813
814fn reorder_named_args(
815    call_args: &[(Option<String>, Value)],
816    parameter_names: &[&str],
817) -> Option<Vec<Value>> {
818    if call_args.len() != parameter_names.len() {
819        return None;
820    }
821    let mut slots = vec![None; parameter_names.len()];
822    let mut positional_index = 0;
823    let mut saw_named = false;
824    for (name, value) in call_args {
825        let slot = if let Some(name) = name {
826            saw_named = true;
827            parameter_names
828                .iter()
829                .position(|candidate| candidate == name)?
830        } else {
831            if saw_named {
832                return None;
833            }
834            let slot = positional_index;
835            positional_index += 1;
836            slot
837        };
838        if slots.get(slot)?.is_some() {
839            return None;
840        }
841        slots[slot] = Some(value.clone());
842    }
843    slots.into_iter().collect()
844}
845
846/// Map `make_interval(name => value, ...)` onto the positional
847/// `(years, months, weeks, days, hours, mins, secs)` argument list.
848/// Returns `None` when an unknown parameter name appears.
849fn make_interval_named_args(call_args: &[(Option<String>, Value)]) -> Option<Vec<Value>> {
850    const NAMES: [&str; 7] = ["years", "months", "weeks", "days", "hours", "mins", "secs"];
851    let mut positional = vec![Value::Int(0); NAMES.len()];
852    let mut positional_index = 0;
853    let mut saw_named = false;
854    let mut assigned = [false; NAMES.len()];
855    for (name, value) in call_args {
856        let slot = if let Some(name) = name {
857            saw_named = true;
858            NAMES.iter().position(|candidate| candidate == name)?
859        } else {
860            if saw_named {
861                return None;
862            }
863            let slot = positional_index;
864            positional_index += 1;
865            slot
866        };
867        if slot >= NAMES.len() || assigned[slot] {
868            return None;
869        }
870        assigned[slot] = true;
871        positional[slot] = value.clone();
872    }
873    Some(positional)
874}
875
876/// `PostgreSQL`-style type name used in function-resolution errors.
877pub fn value_type_name(v: &Value) -> &'static str {
878    match v {
879        Value::Null => "unknown",
880        Value::Bool(_) => "boolean",
881        Value::Int(_) => "integer",
882        Value::Float(_) => "double precision",
883        Value::Str(_) => "text",
884        Value::FixedChar(_) => "character",
885        Value::Bytes(_) => "bytea",
886        Value::Temporal(TemporalValue::Interval { .. }) => "interval",
887        Value::Temporal(_) => "timestamp",
888        Value::Decimal(_) => "numeric",
889        Value::Json(_) => "json",
890        Value::JsonB(_) => "jsonb",
891        Value::Array(_) => "anyarray",
892        Value::List(_) => "anyarray",
893        Value::Row(_) | Value::Record(_) => "record",
894        Value::Map(_) => "jsonb",
895    }
896}
897
898/// `function name(arg types) does not exist` - the error `PostgreSQL`
899/// raises when call resolution fails (SQLSTATE 42883).
900pub fn unknown_function_error(name: &str, args: &[(Option<String>, Value)]) -> SQLError {
901    let types = args
902        .iter()
903        .map(|(arg_name, value)| match arg_name {
904            Some(arg_name) => format!("{arg_name} => {}", value_type_name(value)),
905            None => value_type_name(value).to_string(),
906        })
907        .collect::<Vec<_>>()
908        .join(", ");
909    SQLError::Routine {
910        sqlstate: "42883".into(),
911        message: format!("function {name}({types}) does not exist"),
912    }
913}
914
915#[cfg(test)]
916mod tests;