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/// Minimum arity across the variadic and nonvariadic overloads. A variadic `"any"` signature requires an argument; JSON builders have zero-argument overloads and `format(text)` omits the variadic tail.
38pub(crate) fn minimum_arguments(name: &str) -> Option<usize> {
39    let fixed = fixed_parameters(name)?;
40    Some(match local_name(name).as_str() {
41        "json_build_object" | "json_build_array" | "jsonb_build_object" | "jsonb_build_array" => 0,
42        "format" => 1,
43        _ => fixed + 1,
44    })
45}
46
47/// Whether a built-in declares a `VARIADIC "any"` parameter.
48#[must_use]
49pub fn is_variadic_any(name: &str) -> bool {
50    fixed_parameters(name).is_some()
51}
52
53pub(crate) fn not_an_array() -> SQLError {
54    SQLError::Routine {
55        sqlstate: "42804".into(),
56        message: "VARIADIC argument must be an array".into(),
57    }
58}
59
60fn flatten(values: &[Value], output: &mut Vec<(Option<String>, Value)>) {
61    for value in values {
62        match value {
63            Value::List(values) => flatten(values, output),
64            value => output.push((None, value.clone())),
65        }
66    }
67}
68
69/// Expand the final explicit `VARIADIC` argument of a `VARIADIC "any"` built-in. Other calls keep their arguments.
70pub fn expand_variadic_any(
71    name: &str,
72    mut arguments: Vec<(Option<String>, Value)>,
73) -> Result<VariadicAnyArguments> {
74    let Some(fixed) = fixed_parameters(name) else {
75        return Ok(VariadicAnyArguments::Arguments(arguments));
76    };
77    if arguments.len() != fixed + 1 {
78        return Ok(VariadicAnyArguments::Arguments(arguments));
79    }
80    let Some((_, variadic)) = arguments.pop() else {
81        return Ok(VariadicAnyArguments::Arguments(arguments));
82    };
83    match variadic {
84        // `format` treats a NULL array as zero values; the other functions return NULL.
85        Value::Null if local_name(name) == "format" => {
86            Ok(VariadicAnyArguments::Arguments(arguments))
87        }
88        Value::Null => Ok(VariadicAnyArguments::NullResult),
89        Value::Array(array) => {
90            flatten(array.elements(), &mut arguments);
91            Ok(VariadicAnyArguments::Arguments(arguments))
92        }
93        Value::List(values) => {
94            flatten(&values, &mut arguments);
95            Ok(VariadicAnyArguments::Arguments(arguments))
96        }
97        _ => Err(not_an_array()),
98    }
99}