Skip to main content

uqa_sql/expr/
variadic_any.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Built-ins declared `VARIADIC "any"`: an explicit `VARIADIC array` argument supplies the variadic values element by element, flattening every dimension, as `PostgreSQL`'s `extract_variadic_args` does.
8
9use uqa_core::Value;
10
11use super::{Result, SQLError};
12
13/// A call's evaluated arguments after an explicit `VARIADIC` array has been expanded.
14pub enum VariadicAnyArguments {
15    Arguments(Vec<(Option<String>, Value)>),
16    /// A NULL `VARIADIC` array makes the function return NULL without reading any value.
17    NullResult,
18}
19
20fn local_name(name: &str) -> String {
21    let lower = name.to_ascii_lowercase();
22    lower
23        .strip_prefix("pg_catalog.")
24        .map_or_else(|| lower.clone(), str::to_owned)
25}
26
27/// The number of fixed parameters before the `VARIADIC "any"` parameter, or `None` for any other routine.
28fn fixed_parameters(name: &str) -> Option<usize> {
29    Some(match local_name(name).as_str() {
30        "concat" | "json_build_object" | "json_build_array" | "jsonb_build_object"
31        | "jsonb_build_array" | "num_nulls" | "num_nonnulls" => 0,
32        "concat_ws" | "format" => 1,
33        _ => return None,
34    })
35}
36
37/// Whether a built-in declares a `VARIADIC "any"` parameter.
38#[must_use]
39pub fn is_variadic_any(name: &str) -> bool {
40    fixed_parameters(name).is_some()
41}
42
43pub(crate) fn not_an_array() -> SQLError {
44    SQLError::Routine {
45        sqlstate: "42804".into(),
46        message: "VARIADIC argument must be an array".into(),
47    }
48}
49
50fn flatten(values: &[Value], output: &mut Vec<(Option<String>, Value)>) {
51    for value in values {
52        match value {
53            Value::List(values) => flatten(values, output),
54            value => output.push((None, value.clone())),
55        }
56    }
57}
58
59/// Expand the final explicit `VARIADIC` argument of a `VARIADIC "any"` built-in. Other calls keep their arguments.
60pub fn expand_variadic_any(
61    name: &str,
62    mut arguments: Vec<(Option<String>, Value)>,
63) -> Result<VariadicAnyArguments> {
64    let Some(fixed) = fixed_parameters(name) else {
65        return Ok(VariadicAnyArguments::Arguments(arguments));
66    };
67    if arguments.len() != fixed + 1 {
68        return Ok(VariadicAnyArguments::Arguments(arguments));
69    }
70    let Some((_, variadic)) = arguments.pop() else {
71        return Ok(VariadicAnyArguments::Arguments(arguments));
72    };
73    match variadic {
74        // `format` treats a NULL array as zero values; the other functions return NULL.
75        Value::Null if local_name(name) == "format" => {
76            Ok(VariadicAnyArguments::Arguments(arguments))
77        }
78        Value::Null => Ok(VariadicAnyArguments::NullResult),
79        Value::Array(array) => {
80            flatten(array.elements(), &mut arguments);
81            Ok(VariadicAnyArguments::Arguments(arguments))
82        }
83        Value::List(values) => {
84            flatten(&values, &mut arguments);
85            Ok(VariadicAnyArguments::Arguments(arguments))
86        }
87        _ => Err(not_an_array()),
88    }
89}