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::{
15    BinaryOp, ColumnType, Expr, FunctionBinding, FunctionDispatch, FunctionResolutionError,
16    InternalColumnRef,
17};
18use crate::error::{Result, SQLError};
19use crate::params::SQLParam;
20use crate::result::ResultRow;
21
22mod array_transform;
23mod encoding;
24mod json;
25mod json_strip;
26mod random;
27mod range;
28mod time;
29mod uuid;
30
31pub use array_transform::argument_positions as array_transform_argument_positions;
32use encoding::{base64_decode, base64_encode, md5_hex};
33pub use json::value_to_json_text;
34use json::{
35    format_jsonb_pretty, json_build_array_value, json_build_object_value, json_concat,
36    json_contained_by, json_contains, json_delete, json_delete_path, json_extract_path,
37    json_has_key, json_has_keys, json_typeof, jsonb_insert, jsonb_set, jsonpath_candidate,
38    jsonpath_exists, jsonpath_match, parse_json, strip_nulls, typed_json_value, value_to_json,
39};
40pub use json_strip::argument_positions as json_strip_nulls_argument_positions;
41use json_strip::strip_json_nulls_text;
42pub use range::{
43    multirange_from_ranges, parse_multirange, parse_range, CanonicalMultirange, CanonicalRange,
44};
45use time::{
46    age_between, coerce_temporal, date_trunc_value, extract_from_value, format_pg_number,
47    format_temporal, hex_encode, make_timestamp, parse_timestamp, pg_to_chrono_fmt,
48};
49pub use uuid::parse_uuid_bytes;
50use uuid::{extract_uuid_timestamp, extract_uuid_version, generate_random_uuid, generate_uuid_v7};
51mod binary;
52mod casting;
53mod conversion;
54mod scalar_array;
55mod scalar_core;
56mod scalar_dispatch;
57mod scalar_geospatial;
58mod scalar_helpers;
59mod scalar_json;
60mod scalar_math;
61mod scalar_postgres;
62mod scalar_range;
63mod scalar_temporal;
64
65use binary::{
66    compare, compare_nullable, eval_binary, eval_comparison_op, values_equal, values_equal_nullable,
67};
68pub(crate) use binary::{division_by_zero, out_of_range};
69pub use binary::{
70    eval_binary_values, eval_binary_values_with_integer_width, eval_comparison_truth,
71    integer_width_for_literal, integer_width_for_type, truthy, IntegerWidth,
72};
73pub use casting::{
74    array_dimensions, cast_value, cast_value_from, negate_value, parse_pg_array_literal,
75};
76pub(crate) use conversion::to_f64;
77use conversion::{
78    allocation_error, coerce_i64, expect_str, float1, float_to_i64_rounded, float_to_i64_trunc,
79    gcd_i64, initcap_str, nonnegative_usize, string1, to_decimal, to_i64,
80};
81pub use conversion::{array_value_to_string, value_to_string, vector_value_to_string};
82pub use conversion::{value_to_tensor, value_to_vector};
83use scalar_dispatch::{eval_scalar_function, eval_sequence_function};
84use scalar_helpers::{
85    compile_pg_regex, point_xy, quote_literal, similar_to_regex, trim_chars, typeof_value,
86};
87pub use scalar_helpers::{quote_ident, CompiledLikePattern};
88
89#[must_use]
90pub fn coercion_type_name(ty: &ColumnType) -> String {
91    match ty {
92        ColumnType::Domain { base, .. } => coercion_type_name(base),
93        ColumnType::Array(element) => format!("{}[]", coercion_type_name(element)),
94        _ => ty.sql_name(),
95    }
96}
97
98/// Engine-side hook that scalar function evaluation calls for stateful
99/// sequence and user-defined functions. Query-valued expressions are not
100/// accepted here: lowering assigns them physical query-plan slots executed by
101/// `uqa-execution::ScalarSubqueryRunner`.
102pub trait EngineHook {
103    fn nextval(&self, name: &str) -> Result<i64>;
104    fn currval(&self, name: &str) -> Result<i64>;
105    fn setval(&self, name: &str, value: i64) -> Result<i64>;
106
107    fn call_scalar_function(&self, _name: &str, _args: &[Value]) -> Option<Result<Value>> {
108        None
109    }
110
111    /// Invoke an engine-backed built-in after an exact catalog binding has
112    /// selected it. Unlike `call_scalar_function`, this path is also available
113    /// when dynamic dispatch is disabled, so runtime callbacks cannot override
114    /// the stored built-in identity.
115    fn call_bound_builtin_function(
116        &self,
117        _binding: &crate::ast::FunctionBinding,
118        _args: &[(Option<String>, Value)],
119    ) -> Option<Result<Value>> {
120        None
121    }
122
123    fn has_scalar_functions(&self) -> bool {
124        true
125    }
126
127    /// Resolve a catalog-owned SQL type name for casts evaluated with an engine context.
128    fn resolve_type_name(&self, _name: &str) -> std::result::Result<Option<ColumnType>, String> {
129        Ok(None)
130    }
131
132    /// Resolve a relation name to the OID carrier used by `regclass`.
133    fn resolve_regclass(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
134        Ok(None)
135    }
136
137    /// Resolve one OID-backed alias type to its `PostgreSQL` text output.
138    fn resolve_regtype_output(
139        &self,
140        _ty: &ColumnType,
141        _oid: i64,
142    ) -> std::result::Result<Option<String>, String> {
143        Ok(None)
144    }
145
146    /// Resolve the first existing schema on the logical session's search
147    /// path. `None` lets standalone expression evaluation use its `public`
148    /// compatibility default.
149    fn current_schema(&self) -> std::result::Result<Option<String>, String> {
150        Ok(None)
151    }
152
153    fn current_user(&self) -> std::result::Result<Option<String>, String> {
154        Ok(None)
155    }
156
157    fn session_user(&self) -> std::result::Result<Option<String>, String> {
158        Ok(None)
159    }
160
161    /// Resolve the existing schemas visible to the logical session.
162    fn current_schemas(
163        &self,
164        _include_implicit: bool,
165    ) -> std::result::Result<Option<Vec<String>>, String> {
166        Ok(None)
167    }
168
169    /// Draw from an engine-owned logical-session PRNG. `None` keeps pure,
170    /// engine-free expression evaluation available for library callers.
171    fn random_value(&self) -> std::result::Result<Option<f64>, String> {
172        Ok(None)
173    }
174
175    /// Draw every bit of one engine-owned logical-session PRNG word. Range
176    /// functions use this instead of a floating-point sample so `bigint` and
177    /// arbitrary-precision `numeric` bounds remain uniform.
178    fn random_u64(&self) -> std::result::Result<Option<u64>, String> {
179        Ok(None)
180    }
181
182    /// Reseed the logical-session PRNG. `false` means the hook does not own a
183    /// mutable random stream and the caller must report the unsupported call.
184    fn set_random_seed(&self, _seed: f64) -> std::result::Result<bool, String> {
185        Ok(false)
186    }
187
188    /// Invoke a user-defined SQL / `PL/pgSQL` function. Consulted
189    /// after built-in dispatch misses (and immediately for calls with
190    /// named arguments, which built-ins never accept). `None` means
191    /// no user-defined function with this name exists.
192    fn call_user_function(
193        &self,
194        _name: &str,
195        _args: &[(Option<String>, Value)],
196    ) -> Option<Result<Value>> {
197        None
198    }
199
200    fn call_bound_user_function(
201        &self,
202        _binding: &crate::ast::FunctionBinding,
203        _args: &[(Option<String>, Value)],
204    ) -> Option<Result<Value>> {
205        None
206    }
207}
208
209/// Format a scalar or array OID carrier using the catalog-aware output function of a `reg*` type. `None` means the declared type is not one of the supported alias types or the value is SQL NULL.
210pub fn format_regtype_value(
211    value: &Value,
212    ty: &ColumnType,
213    engine: Option<&dyn EngineHook>,
214) -> Result<Option<String>> {
215    if matches!(value, Value::Null) {
216        return Ok(None);
217    }
218    if let ColumnType::Array(element) = ty {
219        if !matches!(
220            element.as_ref(),
221            ColumnType::Regproc
222                | ColumnType::Regclass
223                | ColumnType::Regnamespace
224                | ColumnType::Regtype
225        ) {
226            return Ok(None);
227        }
228        let Value::Array(array) = value else {
229            return Ok(Some(value_to_string(value)));
230        };
231        let elements = format_regtype_array_elements(array.elements(), element, engine)?;
232        let formatted = array.with_elements(elements).ok_or_else(|| {
233            SQLError::Internal("regtype array output changed the array dimensions".into())
234        })?;
235        return Ok(Some(array_value_to_string(&formatted)));
236    }
237    if !matches!(
238        ty,
239        ColumnType::Regproc | ColumnType::Regclass | ColumnType::Regnamespace | ColumnType::Regtype
240    ) {
241        return Ok(None);
242    }
243    let Value::Int(oid) = value else {
244        return Ok(Some(value_to_string(value)));
245    };
246    if *oid == 0 {
247        return Ok(Some("-".into()));
248    }
249    let resolved = engine
250        .map(|engine| engine.resolve_regtype_output(ty, *oid))
251        .transpose()
252        .map_err(SQLError::Internal)?
253        .flatten();
254    Ok(Some(resolved.unwrap_or_else(|| oid.to_string())))
255}
256
257fn format_regtype_array_elements(
258    values: &[Value],
259    element: &ColumnType,
260    engine: Option<&dyn EngineHook>,
261) -> Result<Vec<Value>> {
262    values
263        .iter()
264        .map(|value| match value {
265            Value::Null => Ok(Value::Null),
266            Value::List(nested) => {
267                format_regtype_array_elements(nested, element, engine).map(Value::List)
268            }
269            other => format_regtype_value(other, element, engine)
270                .map(|text| text.map_or_else(|| other.clone(), Value::Str)),
271        })
272        .collect()
273}
274
275/// Cast a value after resolving catalog-owned source and target types and flattening domains to their coercion types.
276pub fn cast_value_with_type_resolution(
277    value: &Value,
278    source_ty: Option<&str>,
279    target_ty: &str,
280    engine: Option<&dyn EngineHook>,
281) -> Result<Value> {
282    let resolved_source = match (engine, source_ty) {
283        (Some(engine), Some(source_ty)) => engine
284            .resolve_type_name(source_ty)
285            .map_err(SQLError::Internal)?
286            .map(|ty| coercion_type_name(&ty)),
287        _ => None,
288    };
289    let source_ty = resolved_source.as_deref().or(source_ty);
290    let resolved_target = engine
291        .map(|engine| engine.resolve_type_name(target_ty))
292        .transpose()
293        .map_err(SQLError::Internal)?
294        .flatten();
295    let target_ty = resolved_target.as_ref().map_or_else(
296        || Cow::Borrowed(target_ty),
297        |ty| Cow::Owned(coercion_type_name(ty)),
298    );
299    if target_ty.eq_ignore_ascii_case("text") {
300        if let Some(source_ty) = source_ty.and_then(|source| ColumnType::from_sql_name(source).ok())
301        {
302            if let Some(text) = format_regtype_value(value, &source_ty, engine)? {
303                return Ok(Value::Str(text));
304            }
305        }
306    }
307    if target_ty.eq_ignore_ascii_case("regclass") {
308        if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
309            return engine
310                .resolve_regclass(name)
311                .map_err(SQLError::Internal)?
312                .map(Value::Int)
313                .ok_or_else(|| SQLError::Routine {
314                    sqlstate: "42P01".into(),
315                    message: format!("relation \"{name}\" does not exist"),
316                });
317        }
318    }
319    cast_value_from(value, &target_ty, source_ty)
320}
321
322/// Read-only row interface used by the expression evaluator. Most callers
323/// use a materialised [`ResultRow`], while hot execution paths can expose a
324/// projected value slice without rebuilding a string-keyed map for every row.
325pub trait RowLookup {
326    fn column(&self, name: &str) -> Option<&Value>;
327
328    /// Whether an unqualified name identifies more than one visible input
329    /// column. Callers must report SQLSTATE 42702 instead of selecting an
330    /// arbitrary suffix match.
331    fn column_is_ambiguous(&self, _name: &str) -> bool {
332        false
333    }
334
335    fn qualified_column(&self, qualifier: &str, column: &str) -> Option<&Value>;
336
337    /// Whether a qualified identity names more than one visible input column.
338    fn qualified_column_is_ambiguous(&self, _qualifier: &str, _column: &str) -> bool {
339        false
340    }
341
342    /// Return a value by the physical schema position used to construct this
343    /// row view. Materialized named rows do not expose positional access;
344    /// projected execution sources override it so compiled hot paths can avoid
345    /// repeating string lookup for every expression and row.
346    fn positional_column(&self, _index: usize) -> Option<&Value> {
347        None
348    }
349
350    /// Resolve an executor-only relation attribute. Materialized SQL rows do
351    /// not expose these structural slots.
352    fn internal_column(&self, _column: InternalColumnRef) -> Option<&Value> {
353        None
354    }
355
356    /// Read the structurally carried retrieval score for one relation. The qualifier selects a score-bearing source without exposing an executor field in the SQL column namespace.
357    fn score_source(&self, _qualifier: Option<&str>) -> Option<&Value> {
358        None
359    }
360
361    /// Whether the requested score source resolves to more than one retrieval relation.
362    fn score_source_is_ambiguous(&self, _qualifier: Option<&str>) -> bool {
363        false
364    }
365
366    /// Visit every logical column in schema order. Named rows use their map
367    /// order; positional execution rows override this without materializing a
368    /// map. The default keeps narrow projected lookup implementations source
369    /// compatible when they deliberately do not expose whole-row semantics.
370    fn visit_columns(&self, _visitor: &mut dyn FnMut(&str, &Value)) {}
371}
372
373impl RowLookup for ResultRow {
374    fn column(&self, name: &str) -> Option<&Value> {
375        self.get(name)
376    }
377
378    fn qualified_column(&self, _qualifier: &str, _column: &str) -> Option<&Value> {
379        None
380    }
381
382    fn visit_columns(&self, visitor: &mut dyn FnMut(&str, &Value)) {
383        for (column, value) in self {
384            visitor(column, value);
385        }
386    }
387}
388
389pub struct EvalContext<'a> {
390    pub row: Option<&'a ResultRow>,
391    row_lookup: Option<&'a dyn RowLookup>,
392    pub params: &'a [SQLParam],
393    pub engine: Option<&'a dyn EngineHook>,
394}
395
396impl<'a> EvalContext<'a> {
397    pub fn new(row: Option<&'a ResultRow>, params: &'a [SQLParam]) -> Self {
398        Self {
399            row,
400            row_lookup: row.map(|row| row as &dyn RowLookup),
401            params,
402            engine: None,
403        }
404    }
405
406    pub fn from_row_lookup(row: &'a dyn RowLookup, params: &'a [SQLParam]) -> Self {
407        Self {
408            // Whole-row materialization is needed only by correlated
409            // subqueries. Ordinary scalar evaluation must remain on the
410            // lookup/slot path.
411            row: None,
412            row_lookup: Some(row),
413            params,
414            engine: None,
415        }
416    }
417
418    pub fn with_engine(mut self, engine: &'a dyn EngineHook) -> Self {
419        self.engine = Some(engine);
420        self
421    }
422
423    fn row_lookup(&self) -> Result<&'a dyn RowLookup> {
424        self.row_lookup
425            .ok_or_else(|| SQLError::Internal("column reference without row context".into()))
426    }
427
428    /// Resolve an unqualified column through the same row semantics used by
429    /// the AST evaluator. Physical scalar IR evaluators call this instead of
430    /// reconstructing an [`Expr::Column`] carrier.
431    pub fn column_value(&self, name: &str) -> Result<Value> {
432        if self.row_lookup()?.column_is_ambiguous(name) {
433            return Err(SQLError::AmbiguousColumn(name.to_string()));
434        }
435        Ok(self
436            .row_lookup()?
437            .column(name)
438            .cloned()
439            .unwrap_or(Value::Null))
440    }
441
442    /// Resolve a qualified column without constructing an AST expression.
443    pub fn qualified_column_value(&self, qualifier: &str, column: &str) -> Result<Value> {
444        if self
445            .row_lookup()?
446            .qualified_column_is_ambiguous(qualifier, column)
447        {
448            return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
449        }
450        Ok(self
451            .row_lookup()?
452            .qualified_column(qualifier, column)
453            .cloned()
454            .unwrap_or(Value::Null))
455    }
456}
457
458/// Evaluate a value-producing expression. Function calls are *not*
459/// dispatched here; the compiler routes them through the function
460/// registry instead. Calling `eval` on a `Func` expr returns
461/// `Unsupported` so latent function-in-projection bugs surface loudly.
462pub fn eval(expr: &Expr, ctx: &EvalContext<'_>) -> Result<Value> {
463    match expr {
464        Expr::Default => Err(SQLError::Internal(
465            "DEFAULT reached scalar expression evaluation without a mutation target".into(),
466        )),
467        Expr::Literal(v) => Ok(v.clone()),
468        Expr::Param(i) => match i.checked_sub(1).and_then(|index| ctx.params.get(index)) {
469            Some(SQLParam::Scalar(v) | SQLParam::TypedScalar { value: v, .. }) => Ok(v.clone()),
470            Some(SQLParam::Vector(v)) => Ok(Value::List(
471                v.iter().map(|x| Value::Float(f64::from(*x))).collect(),
472            )),
473            Some(SQLParam::Tensor(vectors)) => Ok(Value::List(
474                vectors
475                    .iter()
476                    .map(|vector| {
477                        Value::List(vector.iter().map(|x| Value::Float(f64::from(*x))).collect())
478                    })
479                    .collect(),
480            )),
481            None => Err(SQLError::MissingParam(*i)),
482        },
483        Expr::Column(name) => {
484            // Plain column refs match either an unqualified key or the
485            // suffix of a qualified `table.col` key, so the same row
486            // shape works for single-table SELECTs and JOIN tuples.
487            if ctx.row_lookup()?.column_is_ambiguous(name) {
488                return Err(SQLError::AmbiguousColumn(name.clone()));
489            }
490            Ok(ctx
491                .row_lookup()?
492                .column(name)
493                .cloned()
494                .unwrap_or(Value::Null))
495        }
496        Expr::QualifiedColumn { qualifier, column } => {
497            if ctx
498                .row_lookup()?
499                .qualified_column_is_ambiguous(qualifier, column)
500            {
501                return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
502            }
503            Ok(ctx
504                .row_lookup()?
505                .qualified_column(qualifier, column)
506                .cloned()
507                .unwrap_or(Value::Null))
508        }
509        Expr::InternalColumn(column) => ctx
510            .row_lookup()?
511            .internal_column(*column)
512            .cloned()
513            .ok_or_else(|| {
514                SQLError::Internal(format!(
515                    "internal relation attribute {column:?} is unavailable"
516                ))
517            }),
518        Expr::Array(elements) => {
519            let mut out = Vec::with_capacity(elements.len());
520            for e in elements {
521                out.push(eval(e, ctx)?);
522            }
523            ArrayValue::try_new(out).map(Value::Array).ok_or_else(|| {
524                SQLError::TypeMismatch(
525                    "multidimensional arrays must have matching dimensions".into(),
526                )
527            })
528        }
529        Expr::Row(elements) => {
530            let mut out = Vec::with_capacity(elements.len());
531            for element in elements {
532                out.push(eval(element, ctx)?);
533            }
534            Ok(Value::Row(out))
535        }
536        Expr::Star | Expr::QualifiedStar(_) => {
537            Err(SQLError::Internal("`*` cannot be evaluated".into()))
538        }
539        Expr::Func {
540            name,
541            binding,
542            args,
543            ..
544        } => {
545            let call_args = evaluate_call_args(args, ctx)?;
546            if let Some(binding) = binding {
547                if let Some(FunctionResolutionError::UndefinedFunction { signature }) =
548                    binding.resolution_error.as_ref()
549                {
550                    return Err(SQLError::Routine {
551                        sqlstate: "42883".into(),
552                        message: format!("function {signature} does not exist"),
553                    });
554                }
555                if binding.builtin {
556                    return eval_bound_builtin_function_call(binding, call_args, ctx);
557                }
558                let engine = ctx.engine.ok_or_else(|| {
559                    SQLError::Unsupported(
560                        "bound user function requires a logical engine session".into(),
561                    )
562                })?;
563                engine
564                    .call_bound_user_function(binding, &call_args)
565                    .unwrap_or_else(|| Err(SQLError::UnknownFunction(binding.name.clone())))
566            } else {
567                eval_function_call(name, call_args, ctx)
568            }
569        }
570        Expr::WindowCall { name, .. } => Err(SQLError::Unsupported(format!(
571            "window function `{name}` must be evaluated by the window-aware executor"
572        ))),
573        Expr::Case {
574            base,
575            when,
576            else_branch,
577        } => {
578            let base_value = match base {
579                Some(b) => Some(eval(b, ctx)?),
580                None => None,
581            };
582            for (cond, result) in when {
583                let matched = match &base_value {
584                    Some(bv) => values_equal(bv, &eval(cond, ctx)?),
585                    None => truthy(&eval(cond, ctx)?),
586                };
587                if matched {
588                    return eval(result, ctx);
589                }
590            }
591            match else_branch {
592                Some(e) => eval(e, ctx),
593                None => Ok(Value::Null),
594            }
595        }
596        Expr::Cast { expr, ty } => {
597            let source_ty = explicit_expr_type(expr);
598            let v = eval(expr, ctx)?;
599            cast_value_with_type_resolution(&v, source_ty, ty, ctx.engine)
600        }
601        Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. } => {
602            Err(SQLError::Unsupported(
603                "query-valued expressions must be lowered to physical ScalarExpr/QueryPlan slots"
604                    .into(),
605            ))
606        }
607        Expr::Binary { op, lhs, rhs } => eval_binary(*op, lhs, rhs, ctx),
608        Expr::UnaryMinus(inner) => {
609            let source_ty = explicit_expr_type(inner);
610            let value = eval(inner, ctx)?;
611            negate_value(&value, source_ty)
612        }
613        Expr::Not(inner) => {
614            // SQL three-valued logic: NOT NULL -> NULL.
615            let v = eval(inner, ctx)?;
616            if matches!(v, Value::Null) {
617                return Ok(Value::Null);
618            }
619            Ok(Value::Bool(!truthy(&v)))
620        }
621        Expr::And(items) => {
622            // Kleene AND: FALSE dominates, otherwise NULL taints.
623            let mut saw_null = false;
624            for item in items {
625                let v = eval(item, ctx)?;
626                if matches!(v, Value::Null) {
627                    saw_null = true;
628                } else if !truthy(&v) {
629                    return Ok(Value::Bool(false));
630                }
631            }
632            if saw_null {
633                return Ok(Value::Null);
634            }
635            Ok(Value::Bool(true))
636        }
637        Expr::Or(items) => {
638            // Kleene OR: TRUE dominates, otherwise NULL taints.
639            let mut saw_null = false;
640            for item in items {
641                let v = eval(item, ctx)?;
642                if matches!(v, Value::Null) {
643                    saw_null = true;
644                } else if truthy(&v) {
645                    return Ok(Value::Bool(true));
646                }
647            }
648            if saw_null {
649                return Ok(Value::Null);
650            }
651            Ok(Value::Bool(false))
652        }
653        Expr::IsNull { expr, negated } => {
654            let v = eval(expr, ctx)?;
655            let is_null = matches!(v, Value::Null);
656            Ok(Value::Bool(if *negated { !is_null } else { is_null }))
657        }
658        Expr::Between { expr, low, high } => {
659            let v = eval(expr, ctx)?;
660            let lo = eval(low, ctx)?;
661            let hi = eval(high, ctx)?;
662            eval_between(&v, &lo, &hi)
663        }
664        Expr::InList {
665            expr,
666            list,
667            negated,
668        } => {
669            // Three-valued IN: found -> TRUE, a NULL comparand (or a
670            // NULL needle) downgrades a miss to NULL.
671            let v = eval(expr, ctx)?;
672            let mut saw_null = matches!(v, Value::Null);
673            for item in list {
674                let candidate = eval(item, ctx)?;
675                match values_equal_nullable(&v, &candidate) {
676                    Some(true) => return Ok(Value::Bool(!*negated)),
677                    Some(false) => {}
678                    None => saw_null = true,
679                }
680            }
681            if saw_null {
682                return Ok(Value::Null);
683            }
684            Ok(Value::Bool(*negated))
685        }
686    }
687}
688
689fn explicit_expr_type(expr: &Expr) -> Option<&str> {
690    match expr {
691        Expr::Cast { ty, .. } => Some(ty),
692        Expr::Literal(Value::Int(value)) if i32::try_from(*value).is_ok() => Some("integer"),
693        Expr::Literal(Value::Int(_)) => Some("bigint"),
694        Expr::Literal(Value::Bytes(_)) => Some("bytea"),
695        _ => None,
696    }
697}
698
699/// `expr BETWEEN low AND high` under three-valued logic: a definite
700/// FALSE on either bound wins over a NULL on the other.
701fn eval_between(v: &Value, lo: &Value, hi: &Value) -> Result<Value> {
702    let ge = compare_nullable(v, lo)?.map(|ord| ord.is_ge());
703    let le = compare_nullable(v, hi)?.map(|ord| ord.is_le());
704    Ok(match (ge, le) {
705        (Some(false), _) | (_, Some(false)) => Value::Bool(false),
706        (Some(true), Some(true)) => Value::Bool(true),
707        _ => Value::Null,
708    })
709}
710
711fn normalized_function_name(name: &str) -> Cow<'_, str> {
712    let stripped = name.strip_prefix("pg_catalog.").unwrap_or(name);
713    if stripped.bytes().any(|byte| byte.is_ascii_uppercase()) {
714        Cow::Owned(stripped.to_ascii_lowercase())
715    } else {
716        Cow::Borrowed(stripped)
717    }
718}
719
720fn binding_dispatch(binding: Option<&FunctionBinding>) -> Option<FunctionDispatch> {
721    binding.and_then(|binding| binding.dispatch)
722}
723
724fn direct_variadic_argument_value(argument: &Expr) -> Option<&Expr> {
725    let Expr::Func { binding, args, .. } = argument else {
726        return None;
727    };
728    if binding_dispatch(binding.as_ref()) != Some(FunctionDispatch::VariadicArgument) {
729        return None;
730    }
731    let [value] = args.as_slice() else {
732        return None;
733    };
734    Some(value)
735}
736
737fn named_argument_value(argument: &Expr) -> Option<&Expr> {
738    let Expr::Func { binding, args, .. } = argument else {
739        return None;
740    };
741    if binding_dispatch(binding.as_ref()) == Some(FunctionDispatch::NamedArgument) {
742        args.get(1)
743    } else {
744        None
745    }
746}
747
748/// Wrap the last actual argument of an explicit `VARIADIC` invocation while preserving a named-argument marker at the top level.
749#[must_use]
750pub fn wrap_variadic_argument(mut argument: Expr) -> Expr {
751    if variadic_argument_value(&argument).is_some() {
752        return argument;
753    }
754    if let Expr::Func { binding, args, .. } = &mut argument {
755        if binding_dispatch(binding.as_ref()) == Some(FunctionDispatch::NamedArgument)
756            && args.len() == 2
757        {
758            let value = args.remove(1);
759            args.push(variadic_argument_marker(value));
760            return argument;
761        }
762    }
763    variadic_argument_marker(argument)
764}
765
766fn variadic_argument_marker(value: Expr) -> Expr {
767    let binding = FunctionBinding::dispatched(FunctionDispatch::VariadicArgument);
768    Expr::Func {
769        name: binding.name.clone(),
770        binding: Some(binding),
771        args: vec![value],
772        distinct: false,
773        order_by: Vec::new(),
774        filter: None,
775    }
776}
777
778/// Return the value expression carried by an explicit `VARIADIC` marker, including one nested inside a named argument.
779#[must_use]
780pub fn variadic_argument_value(argument: &Expr) -> Option<&Expr> {
781    let value = named_argument_value(argument).unwrap_or(argument);
782    direct_variadic_argument_value(value)
783}
784
785/// Return a call argument's value expression after stripping named and explicit `VARIADIC` syntax markers.
786#[must_use]
787pub fn call_argument_value(argument: &Expr) -> &Expr {
788    let value = named_argument_value(argument).unwrap_or(argument);
789    direct_variadic_argument_value(value).unwrap_or(value)
790}
791
792/// Enforce `PostgreSQL` function-call ordering before overload resolution.
793/// Positional arguments must precede named arguments, and each explicit name
794/// may occur only once.
795pub fn validate_named_argument_order<'a>(
796    argument_names: impl IntoIterator<Item = Option<&'a str>>,
797) -> Result<()> {
798    let mut saw_named = false;
799    let mut named = Vec::new();
800    for argument_name in argument_names {
801        let Some(argument_name) = argument_name else {
802            if saw_named {
803                return Err(SQLError::Routine {
804                    sqlstate: "42601".into(),
805                    message: "positional argument cannot follow named argument".into(),
806                });
807            }
808            continue;
809        };
810        saw_named = true;
811        if named.contains(&argument_name) {
812            return Err(SQLError::Routine {
813                sqlstate: "42601".into(),
814                message: format!("argument name \"{argument_name}\" used more than once"),
815            });
816        }
817        named.push(argument_name);
818    }
819    Ok(())
820}
821
822/// Return the `PostgreSQL` 18 strictness contract for a built-in scalar call when its implemented overload is known.
823#[must_use]
824pub fn builtin_scalar_function_strictness(name: &str, argument_count: usize) -> Option<bool> {
825    let normalized = normalized_function_name(name);
826    match normalized.as_ref() {
827        "int4range" | "int8range" | "numrange" | "daterange" | "tsrange" | "tstzrange"
828            if matches!(argument_count, 2 | 3) =>
829        {
830            Some(false)
831        }
832        "int4multirange" | "int8multirange" | "nummultirange" | "datemultirange"
833        | "tsmultirange" | "tstzmultirange"
834            if argument_count <= 1 =>
835        {
836            Some(true)
837        }
838        "multirange" if argument_count == 1 => Some(true),
839        "coalesce" | "greatest" | "least" if argument_count >= 1 => Some(false),
840        "nullif" | "concat_op" if argument_count == 2 => Some(false),
841        "concat" | "format" | "json_build_array" | "jsonb_build_array" | "json_build_object"
842        | "jsonb_build_object" | "num_nulls" | "num_nonnulls" => Some(false),
843        "concat_ws" if argument_count >= 1 => Some(false),
844        "quote_nullable" | "pg_typeof" | "typeof" if argument_count == 1 => Some(false),
845        "array_cat" | "array_append" | "array_prepend" | "array_remove" | "array_positions"
846            if argument_count == 2 =>
847        {
848            Some(false)
849        }
850        "array_position" if matches!(argument_count, 2 | 3) => Some(false),
851        "array_replace" if argument_count == 3 => Some(false),
852        "array_fill" if matches!(argument_count, 2 | 3) => Some(false),
853        "array_to_string" if argument_count == 3 => Some(false),
854        "string_to_array" | "string_to_table" if matches!(argument_count, 2 | 3) => Some(false),
855        "overlaps" if argument_count == 4 => Some(false),
856        "abs"
857        | "acos"
858        | "array_dims"
859        | "array_ndims"
860        | "array_reverse"
861        | "ascii"
862        | "asin"
863        | "atan"
864        | "bit_length"
865        | "cardinality"
866        | "casefold"
867        | "cbrt"
868        | "ceil"
869        | "ceiling"
870        | "char_length"
871        | "character_length"
872        | "chr"
873        | "cos"
874        | "cosh"
875        | "current_schemas"
876        | "degrees"
877        | "exp"
878        | "factorial"
879        | "floor"
880        | "gamma"
881        | "initcap"
882        | "isfinite"
883        | "json_array_length"
884        | "jsonb_array_length"
885        | "json_typeof"
886        | "jsonb_typeof"
887        | "jsonb_pretty"
888        | "justify_hours"
889        | "length"
890        | "lgamma"
891        | "ln"
892        | "log10"
893        | "log2"
894        | "lower"
895        | "md5"
896        | "octet_length"
897        | "quote_ident"
898        | "quote_literal"
899        | "radians"
900        | "reverse"
901        | "row_to_json"
902        | "sign"
903        | "sin"
904        | "sinh"
905        | "sqrt"
906        | "tan"
907        | "tanh"
908        | "to_bin"
909        | "to_hex"
910        | "to_oct"
911        | "to_json"
912        | "to_jsonb"
913        | "to_regclass"
914        | "to_timestamp"
915        | "upper"
916        | "uuid_extract_timestamp"
917        | "uuid_extract_version"
918            if argument_count == 1 =>
919        {
920            Some(true)
921        }
922        "random" if argument_count == 2 => Some(true),
923        "age" | "btrim" | "ltrim" | "rtrim" | "trim" | "log" | "round" | "trunc"
924        | "json_strip_nulls" | "jsonb_strip_nulls"
925            if matches!(argument_count, 1 | 2) =>
926        {
927            Some(true)
928        }
929        "array_sort" if matches!(argument_count, 1..=3) => Some(true),
930        "array_length" | "array_lower" | "array_upper" | "atan2" | "date_part" | "date_trunc"
931        | "decode" | "encode" | "extract" | "gcd" | "lcm" | "left" | "mod" | "power" | "pow"
932        | "repeat" | "right" | "starts_with" | "position" | "strpos" | "to_char" | "to_date"
933        | "to_number" | "trim_array" | "point" | "st_distance" | "st_within"
934            if argument_count == 2 =>
935        {
936            Some(true)
937        }
938        "like" | "ilike" | "similar_to" if argument_count == 2 => Some(true),
939        "like" | "ilike" | "similar_to" if argument_count == 3 => Some(false),
940        "array_to_string" if argument_count == 2 => Some(true),
941        "substring" | "substr" | "lpad" | "rpad" if matches!(argument_count, 2 | 3) => Some(true),
942        "regexp_count" if matches!(argument_count, 2..=4) => Some(true),
943        "regexp_instr" if matches!(argument_count, 2..=7) => Some(true),
944        "regexp_like" | "regexp_match" | "regexp_matches" if matches!(argument_count, 2 | 3) => {
945            Some(true)
946        }
947        "regexp_replace" if matches!(argument_count, 3..=6) => Some(true),
948        "regexp_substr" if matches!(argument_count, 2..=6) => Some(true),
949        "replace" | "split_part" | "translate" | "make_date" if argument_count == 3 => Some(true),
950        "overlay" | "jsonb_set" | "jsonb_insert" if matches!(argument_count, 3 | 4) => Some(true),
951        "json_extract_path"
952        | "jsonb_extract_path"
953        | "json_extract_path_text"
954        | "jsonb_extract_path_text"
955            if argument_count >= 2 =>
956        {
957            Some(true)
958        }
959        "json_contains" | "json_contained_by" | "json_delete_path" | "json_has_key"
960        | "json_has_any_key" | "json_has_all_keys" | "jsonb_path_exists" | "jsonpath_exists"
961        | "jsonb_path_match" | "jsonpath_match"
962            if argument_count == 2 =>
963        {
964            Some(true)
965        }
966        "make_timestamp" if matches!(argument_count, 6 | 7) => Some(true),
967        "make_interval" if argument_count <= 7 => Some(true),
968        "width_bucket" if argument_count == 4 => Some(true),
969        "st_dwithin" if matches!(argument_count, 2 | 3) => Some(true),
970        _ => None,
971    }
972}
973
974/// Return the `PostgreSQL` 18 strictness contract selected by a structural function binding. Parser-owned syntax and overload-specific built-ins must be classified by [`FunctionDispatch`], never by their diagnostic display label.
975#[must_use]
976pub fn bound_scalar_function_strictness(
977    name: &str,
978    binding: Option<&FunctionBinding>,
979    argument_count: usize,
980) -> Option<bool> {
981    let Some(binding) = binding else {
982        return builtin_scalar_function_strictness(name, argument_count);
983    };
984    if let Some(dispatch) = binding.dispatch {
985        return match dispatch {
986            FunctionDispatch::ArraySubscripts
987            | FunctionDispatch::Subscript
988            | FunctionDispatch::BetweenSymmetric
989            | FunctionDispatch::ToBinInt4
990            | FunctionDispatch::ToBinInt8
991            | FunctionDispatch::ToHexInt4
992            | FunctionDispatch::ToHexInt8
993            | FunctionDispatch::ToOctInt4
994            | FunctionDispatch::ToOctInt8
995            | FunctionDispatch::RandomInt4Range
996            | FunctionDispatch::RandomInt8Range
997            | FunctionDispatch::RandomNumericRange
998            | FunctionDispatch::ArraySortJson
999            | FunctionDispatch::Range { .. } => Some(true),
1000            FunctionDispatch::ArraySlices
1001            | FunctionDispatch::Slice
1002            | FunctionDispatch::AnyOperator
1003            | FunctionDispatch::AllOperator
1004            | FunctionDispatch::IsDistinct => Some(false),
1005            FunctionDispatch::NamedArgument | FunctionDispatch::VariadicArgument => None,
1006        };
1007    }
1008    binding
1009        .builtin
1010        .then(|| builtin_scalar_function_strictness(&binding.name, argument_count))
1011        .flatten()
1012}
1013
1014/// Evaluate a call's argument list, unwrapping `name => value`
1015/// markers into `(Some(name), value)` pairs.
1016pub fn evaluate_call_args(
1017    args: &[Expr],
1018    ctx: &EvalContext<'_>,
1019) -> Result<Vec<(Option<String>, Value)>> {
1020    args.iter()
1021        .map(|arg| match arg {
1022            Expr::Func {
1023                binding,
1024                args: inner,
1025                ..
1026            } if binding_dispatch(binding.as_ref()) == Some(FunctionDispatch::NamedArgument) => {
1027                let Some(Expr::Literal(Value::Str(arg_name))) = inner.first() else {
1028                    return Err(SQLError::Internal("named argument without a name".into()));
1029                };
1030                let value_expr = inner
1031                    .get(1)
1032                    .ok_or_else(|| SQLError::Internal("named argument without a value".into()))?;
1033                Ok((
1034                    Some(arg_name.clone()),
1035                    evaluate_call_argument_value(value_expr, ctx)?,
1036                ))
1037            }
1038            other => Ok((None, evaluate_call_argument_value(other, ctx)?)),
1039        })
1040        .collect()
1041}
1042
1043fn evaluate_call_argument_value(argument: &Expr, ctx: &EvalContext<'_>) -> Result<Value> {
1044    if let Expr::Func { binding, args, .. } = argument {
1045        if binding_dispatch(binding.as_ref()) == Some(FunctionDispatch::VariadicArgument) {
1046            let [value] = args.as_slice() else {
1047                return Err(SQLError::Internal(
1048                    "VARIADIC argument marker must contain one value".into(),
1049                ));
1050            };
1051            return eval(value, ctx);
1052        }
1053    }
1054    eval(argument, ctx)
1055}
1056
1057/// Execute a scalar function after its argument expressions have already
1058/// been evaluated.
1059///
1060/// This is the shared SQL-semantics kernel used by both the parser AST
1061/// evaluator and the physical scalar IR evaluator. Keeping dispatch here
1062/// avoids converting a physical expression back into [`Expr`] merely to
1063/// reuse built-in, sequence, registered, or user-defined function behavior.
1064pub fn eval_function_call(
1065    name: &str,
1066    call_args: Vec<(Option<String>, Value)>,
1067    ctx: &EvalContext<'_>,
1068) -> Result<Value> {
1069    eval_function_call_inner(name, call_args, ctx, true)
1070}
1071
1072/// Execute a call whose stored binding selects a built-in routine. Dynamic
1073/// runtime callbacks and SQL routines must not override this stable binding.
1074pub fn eval_builtin_function_call(
1075    name: &str,
1076    call_args: Vec<(Option<String>, Value)>,
1077    ctx: &EvalContext<'_>,
1078) -> Result<Value> {
1079    eval_function_call_inner(name, call_args, ctx, false)
1080}
1081
1082/// Execute the exact built-in implementation selected by a stored binding. Overload-specific and parser-owned operations use [`FunctionDispatch`] rather than fabricated SQL routine names.
1083pub fn eval_bound_builtin_function_call(
1084    binding: &FunctionBinding,
1085    call_args: Vec<(Option<String>, Value)>,
1086    ctx: &EvalContext<'_>,
1087) -> Result<Value> {
1088    let Some(dispatch) = binding.dispatch else {
1089        return eval_builtin_function_call(&binding.name, call_args, ctx);
1090    };
1091    if let Some(result) = random::eval_dispatched_random_function(dispatch, &call_args, ctx) {
1092        return result;
1093    }
1094    if call_args.iter().any(|(name, _)| name.is_some()) {
1095        return Err(SQLError::Internal(format!(
1096            "bound {} expression retained a named argument",
1097            dispatch.label()
1098        )));
1099    }
1100    let evaluated = call_args
1101        .into_iter()
1102        .map(|(_, value)| value)
1103        .collect::<Vec<_>>();
1104    if let Some(result) = scalar_postgres::eval_dispatched_postgres_function(dispatch, &evaluated) {
1105        return result;
1106    }
1107    match dispatch {
1108        FunctionDispatch::ArraySortJson => {
1109            scalar_array::eval_dispatched_json_array_sort(&evaluated)
1110        }
1111        FunctionDispatch::Range {
1112            operation,
1113            subtype,
1114            multirange,
1115        } => {
1116            scalar_range::eval_dispatched_range_function(operation, subtype, multirange, &evaluated)
1117        }
1118        FunctionDispatch::NamedArgument | FunctionDispatch::VariadicArgument => Err(
1119            SQLError::Internal("call-argument syntax marker reached scalar execution".into()),
1120        ),
1121        _ => Err(SQLError::Internal(format!(
1122            "{} has no scalar executor",
1123            dispatch.label()
1124        ))),
1125    }
1126}
1127
1128fn eval_function_call_inner(
1129    name: &str,
1130    call_args: Vec<(Option<String>, Value)>,
1131    ctx: &EvalContext<'_>,
1132    allow_dynamic_dispatch: bool,
1133) -> Result<Value> {
1134    let lower = normalized_function_name(name);
1135    let lower = lower.as_ref();
1136    let evaluated: Vec<Value> = call_args.iter().map(|(_, value)| value.clone()).collect();
1137
1138    if let Some(result) = random::eval_random_function(lower, &call_args, ctx) {
1139        return result;
1140    }
1141    if lower == "random" && !evaluated.is_empty() {
1142        return Err(SQLError::TypeMismatch("random takes no arguments".into()));
1143    }
1144    if lower == "setseed" {
1145        let [value] = evaluated.as_slice() else {
1146            return Err(SQLError::TypeMismatch("setseed takes 1 arg".into()));
1147        };
1148        let seed = to_f64(value)?;
1149        if !seed.is_finite() || !(-1.0..=1.0).contains(&seed) {
1150            return Err(SQLError::Routine {
1151                sqlstate: "22023".into(),
1152                message: format!("setseed parameter {seed} is out of allowed range [-1,1]"),
1153            });
1154        }
1155        let engine = ctx.engine.ok_or_else(|| {
1156            SQLError::Unsupported("setseed requires a logical engine session".into())
1157        })?;
1158        if !engine.set_random_seed(seed).map_err(SQLError::Internal)? {
1159            return Err(SQLError::Unsupported(
1160                "engine hook does not provide a session random stream".into(),
1161            ));
1162        }
1163        return Ok(Value::Str(String::new()));
1164    }
1165
1166    if lower == "current_schema" {
1167        if !evaluated.is_empty() {
1168            return Err(SQLError::TypeMismatch(
1169                "current_schema takes no arguments".into(),
1170            ));
1171        }
1172        let schema = ctx
1173            .engine
1174            .map(|engine| engine.current_schema())
1175            .transpose()
1176            .map_err(SQLError::Internal)?
1177            .flatten()
1178            .unwrap_or_else(|| "public".to_string());
1179        return Ok(Value::Str(schema));
1180    }
1181    if lower == "current_schemas" {
1182        let [Value::Bool(include_implicit)] = evaluated.as_slice() else {
1183            return Err(SQLError::TypeMismatch(
1184                "current_schemas takes one boolean argument".into(),
1185            ));
1186        };
1187        let schemas = ctx
1188            .engine
1189            .map(|engine| engine.current_schemas(*include_implicit))
1190            .transpose()
1191            .map_err(SQLError::Internal)?
1192            .flatten()
1193            .unwrap_or_else(|| {
1194                let mut schemas = Vec::new();
1195                if *include_implicit {
1196                    schemas.push("pg_catalog".to_string());
1197                }
1198                schemas.push("public".to_string());
1199                schemas
1200            });
1201        return ArrayValue::try_new(schemas.into_iter().map(Value::Str).collect())
1202            .map(Value::Array)
1203            .ok_or_else(|| SQLError::TypeMismatch("invalid current_schemas result".into()));
1204    }
1205    if matches!(lower, "current_user" | "session_user") {
1206        if !evaluated.is_empty() {
1207            return Err(SQLError::TypeMismatch(format!(
1208                "{lower} takes no arguments"
1209            )));
1210        }
1211        let user = ctx
1212            .engine
1213            .map(|engine| {
1214                if lower == "current_user" {
1215                    engine.current_user()
1216                } else {
1217                    engine.session_user()
1218                }
1219            })
1220            .transpose()
1221            .map_err(SQLError::Internal)?
1222            .flatten()
1223            .unwrap_or_else(|| "uqa".to_string());
1224        return Ok(Value::Str(user));
1225    }
1226    if lower == "to_regclass" {
1227        let [value] = evaluated.as_slice() else {
1228            return Err(SQLError::BadArity {
1229                name: "to_regclass".into(),
1230                expected: "1".into(),
1231                actual: evaluated.len(),
1232            });
1233        };
1234        let name = match value {
1235            Value::Null => return Ok(Value::Null),
1236            Value::Str(name) | Value::FixedChar(name) => name,
1237            value => {
1238                return Err(SQLError::TypeMismatch(format!(
1239                    "to_regclass requires text, got {}",
1240                    value_type_name(value)
1241                )));
1242            }
1243        };
1244        let oid = ctx
1245            .engine
1246            .map(|engine| engine.resolve_regclass(name))
1247            .transpose()
1248            .map_err(SQLError::Internal)?
1249            .flatten();
1250        return Ok(oid.map_or(Value::Null, Value::Int));
1251    }
1252
1253    // Functions registered in the operator registry (text_match,
1254    // knn_match, ...) are dispatched by the relational/access-path
1255    // executor. JSONPath fts_match is the scalar exception.
1256    if crate::registry::is_registered(lower) {
1257        if lower == "fts_match" && jsonpath_candidate(&evaluated) {
1258            return jsonpath_match(&evaluated);
1259        }
1260        return Err(SQLError::Unsupported(format!(
1261            "scalar evaluation of `{name}` is not supported (use the function registry)"
1262        )));
1263    }
1264
1265    if call_args.iter().any(|(name, _)| name.is_some()) {
1266        if let Some(positional) = builtin_named_args(lower, &call_args) {
1267            return eval_scalar_function(lower, &positional);
1268        }
1269        if let Some(engine) = ctx.engine.filter(|_| allow_dynamic_dispatch) {
1270            if let Some(result) = engine.call_user_function(lower, &call_args) {
1271                return result;
1272            }
1273        }
1274        return Err(unknown_function_error(lower, &call_args));
1275    }
1276
1277    // Sequence functions mutate engine state and therefore precede pure
1278    // built-in dispatch.
1279    if matches!(lower, "nextval" | "currval" | "setval") {
1280        return eval_sequence_function(lower, &evaluated, ctx);
1281    }
1282    if let Some(engine) = ctx
1283        .engine
1284        .filter(|engine| allow_dynamic_dispatch && engine.has_scalar_functions())
1285    {
1286        if let Some(result) = engine.call_scalar_function(lower, &evaluated) {
1287            return result;
1288        }
1289    }
1290    match eval_scalar_function(lower, &evaluated) {
1291        // Unknown built-in: fall through to user-defined functions,
1292        // mirroring PostgreSQL's search-path order.
1293        Err(SQLError::UnknownFunction(_)) => {
1294            if let Some(engine) = ctx.engine.filter(|_| allow_dynamic_dispatch) {
1295                if let Some(result) = engine.call_user_function(lower, &call_args) {
1296                    return result;
1297                }
1298            }
1299            Err(unknown_function_error(lower, &call_args))
1300        }
1301        other => other,
1302    }
1303}
1304
1305fn builtin_named_args(function: &str, call_args: &[(Option<String>, Value)]) -> Option<Vec<Value>> {
1306    if matches!(function, "array_sort" | "array_reverse") {
1307        return array_transform::reorder_named_values(function, call_args);
1308    }
1309    if matches!(function, "json_strip_nulls" | "jsonb_strip_nulls") {
1310        return json_strip::reorder_named_values(function, call_args);
1311    }
1312    let names: &[&str] = match function {
1313        "regexp_count" => match call_args.len() {
1314            2 => &["string", "pattern"],
1315            3 => &["string", "pattern", "start"],
1316            4 => &["string", "pattern", "start", "flags"],
1317            _ => return None,
1318        },
1319        "regexp_like" => match call_args.len() {
1320            2 => &["string", "pattern"],
1321            3 => &["string", "pattern", "flags"],
1322            _ => return None,
1323        },
1324        "regexp_substr" => match call_args.len() {
1325            2 => &["string", "pattern"],
1326            3 => &["string", "pattern", "start"],
1327            4 => &["string", "pattern", "start", "N"],
1328            5 => &["string", "pattern", "start", "N", "flags"],
1329            6 => &["string", "pattern", "start", "N", "flags", "subexpr"],
1330            _ => return None,
1331        },
1332        "regexp_instr" => match call_args.len() {
1333            2 => &["string", "pattern"],
1334            3 => &["string", "pattern", "start"],
1335            4 => &["string", "pattern", "start", "N"],
1336            5 => &["string", "pattern", "start", "N", "endoption"],
1337            6 => &["string", "pattern", "start", "N", "endoption", "flags"],
1338            7 => &[
1339                "string",
1340                "pattern",
1341                "start",
1342                "N",
1343                "endoption",
1344                "flags",
1345                "subexpr",
1346            ],
1347            _ => return None,
1348        },
1349        "regexp_replace" => match call_args.len() {
1350            3 => &["string", "pattern", "replacement"],
1351            4 if call_args
1352                .iter()
1353                .any(|(name, _)| name.as_deref() == Some("flags")) =>
1354            {
1355                &["string", "pattern", "replacement", "flags"]
1356            }
1357            4 => &["string", "pattern", "replacement", "start"],
1358            5 => &["string", "pattern", "replacement", "start", "N"],
1359            6 => &["string", "pattern", "replacement", "start", "N", "flags"],
1360            _ => return None,
1361        },
1362        "make_interval" => return make_interval_named_args(call_args),
1363        _ => return None,
1364    };
1365    reorder_named_args(call_args, names)
1366}
1367
1368fn reorder_named_args(
1369    call_args: &[(Option<String>, Value)],
1370    parameter_names: &[&str],
1371) -> Option<Vec<Value>> {
1372    if call_args.len() != parameter_names.len() {
1373        return None;
1374    }
1375    let mut slots = vec![None; parameter_names.len()];
1376    let mut positional_index = 0;
1377    let mut saw_named = false;
1378    for (name, value) in call_args {
1379        let slot = if let Some(name) = name {
1380            saw_named = true;
1381            parameter_names
1382                .iter()
1383                .position(|candidate| candidate == name)?
1384        } else {
1385            if saw_named {
1386                return None;
1387            }
1388            let slot = positional_index;
1389            positional_index += 1;
1390            slot
1391        };
1392        if slots.get(slot)?.is_some() {
1393            return None;
1394        }
1395        slots[slot] = Some(value.clone());
1396    }
1397    slots.into_iter().collect()
1398}
1399
1400/// Map `make_interval(name => value, ...)` onto the positional
1401/// `(years, months, weeks, days, hours, mins, secs)` argument list.
1402/// Returns `None` when an unknown parameter name appears.
1403fn make_interval_named_args(call_args: &[(Option<String>, Value)]) -> Option<Vec<Value>> {
1404    const NAMES: [&str; 7] = ["years", "months", "weeks", "days", "hours", "mins", "secs"];
1405    let mut positional = vec![Value::Int(0); NAMES.len()];
1406    let mut positional_index = 0;
1407    let mut saw_named = false;
1408    let mut assigned = [false; NAMES.len()];
1409    for (name, value) in call_args {
1410        let slot = if let Some(name) = name {
1411            saw_named = true;
1412            NAMES.iter().position(|candidate| candidate == name)?
1413        } else {
1414            if saw_named {
1415                return None;
1416            }
1417            let slot = positional_index;
1418            positional_index += 1;
1419            slot
1420        };
1421        if slot >= NAMES.len() || assigned[slot] {
1422            return None;
1423        }
1424        assigned[slot] = true;
1425        positional[slot] = value.clone();
1426    }
1427    Some(positional)
1428}
1429
1430/// `PostgreSQL`-style type name used in function-resolution errors.
1431pub fn value_type_name(v: &Value) -> &'static str {
1432    match v {
1433        Value::Null => "unknown",
1434        Value::Bool(_) => "boolean",
1435        Value::Int(_) => "integer",
1436        Value::Float(_) => "double precision",
1437        Value::Str(_) => "text",
1438        Value::FixedChar(_) => "character",
1439        Value::Bytes(_) => "bytea",
1440        Value::Temporal(TemporalValue::Interval { .. }) => "interval",
1441        Value::Temporal(_) => "timestamp",
1442        Value::Decimal(_) => "numeric",
1443        Value::Json(_) => "json",
1444        Value::JsonB(_) => "jsonb",
1445        Value::Array(_) => "anyarray",
1446        Value::List(_) => "anyarray",
1447        Value::Row(_) | Value::Record(_) => "record",
1448        Value::Map(_) => "jsonb",
1449    }
1450}
1451
1452/// `function name(arg types) does not exist` - the error `PostgreSQL`
1453/// raises when call resolution fails (SQLSTATE 42883).
1454pub fn unknown_function_error(name: &str, args: &[(Option<String>, Value)]) -> SQLError {
1455    let types = args
1456        .iter()
1457        .map(|(arg_name, value)| match arg_name {
1458            Some(arg_name) => format!("{arg_name} => {}", value_type_name(value)),
1459            None => value_type_name(value).to_string(),
1460        })
1461        .collect::<Vec<_>>()
1462        .join(", ");
1463    SQLError::Routine {
1464        sqlstate: "42883".into(),
1465        message: format!("function {name}({types}) does not exist"),
1466    }
1467}
1468
1469#[cfg(test)]
1470mod tests;