Skip to main content

uqa_sql/ir/
call_arguments.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Named and explicit variadic SQL argument validation.
8
9use super::{ScalarExpr, ScalarOrder};
10use crate::ast::{FunctionBinding, FunctionDispatch};
11use crate::SQLError;
12use uqa_core::Value;
13
14/// A SQL call argument after removing the compiler's named and explicit `VARIADIC` syntax markers.
15#[doc(hidden)]
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct ScalarCallArgument<'a> {
18    pub name: Option<&'a str>,
19    pub value: &'a ScalarExpr,
20    pub explicit_variadic: bool,
21}
22
23/// Decode and validate all compiler-owned call-argument markers. `PostgreSQL` permits one explicit `VARIADIC` argument and requires it to be the final argument.
24#[doc(hidden)]
25pub fn scalar_call_arguments(
26    arguments: &[ScalarExpr],
27) -> Result<Vec<ScalarCallArgument<'_>>, SQLError> {
28    let mut decoded = Vec::with_capacity(arguments.len());
29    for argument in arguments {
30        decoded.push(scalar_call_argument(argument)?);
31    }
32    validate_scalar_call_arguments(&decoded)?;
33    Ok(decoded)
34}
35
36/// Validate cross-argument invariants after individual syntax markers have been decoded, returning whether the call used explicit `VARIADIC` syntax.
37#[doc(hidden)]
38pub fn validate_scalar_call_arguments(
39    arguments: &[ScalarCallArgument<'_>],
40) -> Result<bool, SQLError> {
41    let variadic_positions = arguments
42        .iter()
43        .enumerate()
44        .filter_map(|(position, argument)| argument.explicit_variadic.then_some(position))
45        .collect::<Vec<_>>();
46    if variadic_positions.len() > 1 {
47        return Err(malformed_call_argument(
48            "call contains more than one explicit VARIADIC argument",
49        ));
50    }
51    if variadic_positions
52        .first()
53        .is_some_and(|position| position + 1 != arguments.len())
54    {
55        return Err(malformed_call_argument(
56            "explicit VARIADIC argument must be the final call argument",
57        ));
58    }
59    Ok(!variadic_positions.is_empty())
60}
61
62/// Decode one compiler-owned call-argument marker. Use [`scalar_call_arguments`] for a complete call so duplicate and ordering invariants are also checked.
63#[doc(hidden)]
64pub fn scalar_call_argument(expression: &ScalarExpr) -> Result<ScalarCallArgument<'_>, SQLError> {
65    let ScalarExpr::Func {
66        name,
67        args,
68        binding,
69        distinct,
70        order_by,
71        filter,
72    } = expression
73    else {
74        return Ok(ScalarCallArgument {
75            name: None,
76            value: expression,
77            explicit_variadic: false,
78        });
79    };
80    if binding.as_ref().and_then(|binding| binding.dispatch)
81        == Some(FunctionDispatch::NamedArgument)
82    {
83        validate_marker_shape(
84            binding.as_ref(),
85            FunctionDispatch::NamedArgument,
86            *distinct,
87            order_by,
88            filter.as_deref(),
89            name,
90        )?;
91        let [ScalarExpr::Literal(Value::Str(argument_name)), value] = args.as_slice() else {
92            return Err(malformed_call_argument(
93                "named argument marker must contain a string name and one value",
94            ));
95        };
96        let (value, explicit_variadic) = direct_variadic_argument(value)?;
97        if !explicit_variadic
98            && matches!(
99                value,
100                ScalarExpr::Func { binding, .. }
101                    if binding.as_ref().and_then(|binding| binding.dispatch)
102                        == Some(FunctionDispatch::NamedArgument)
103            )
104        {
105            return Err(malformed_call_argument(
106                "call argument contains nested syntax markers",
107            ));
108        }
109        return Ok(ScalarCallArgument {
110            name: Some(argument_name),
111            value,
112            explicit_variadic,
113        });
114    }
115    let (value, explicit_variadic) = direct_variadic_argument(expression)?;
116    Ok(ScalarCallArgument {
117        name: None,
118        value,
119        explicit_variadic,
120    })
121}
122
123fn direct_variadic_argument(expression: &ScalarExpr) -> Result<(&ScalarExpr, bool), SQLError> {
124    let ScalarExpr::Func {
125        name,
126        args,
127        binding,
128        distinct,
129        order_by,
130        filter,
131    } = expression
132    else {
133        return Ok((expression, false));
134    };
135    if binding.as_ref().and_then(|binding| binding.dispatch)
136        != Some(FunctionDispatch::VariadicArgument)
137    {
138        return Ok((expression, false));
139    }
140    validate_marker_shape(
141        binding.as_ref(),
142        FunctionDispatch::VariadicArgument,
143        *distinct,
144        order_by,
145        filter.as_deref(),
146        name,
147    )?;
148    let [value] = args.as_slice() else {
149        return Err(malformed_call_argument(
150            "VARIADIC argument marker must contain exactly one value",
151        ));
152    };
153    if matches!(
154        value,
155        ScalarExpr::Func { binding, .. }
156            if matches!(
157                binding.as_ref().and_then(|binding| binding.dispatch),
158                Some(FunctionDispatch::VariadicArgument | FunctionDispatch::NamedArgument)
159            )
160    ) {
161        return Err(malformed_call_argument(
162            "call argument contains nested syntax markers",
163        ));
164    }
165    Ok((value, true))
166}
167
168fn validate_marker_shape(
169    binding: Option<&FunctionBinding>,
170    expected_dispatch: FunctionDispatch,
171    distinct: bool,
172    order_by: &[ScalarOrder],
173    filter: Option<&ScalarExpr>,
174    name: &str,
175) -> Result<(), SQLError> {
176    if binding.is_none_or(|binding| {
177        !binding.builtin
178            || binding.dispatch != Some(expected_dispatch)
179            || !binding.argument_types.is_empty()
180            || binding.invocation.is_some()
181            || binding.resolution_error.is_some()
182    }) || distinct
183        || !order_by.is_empty()
184        || filter.is_some()
185    {
186        return Err(malformed_call_argument(&format!(
187            "{name} syntax marker contains function-call metadata"
188        )));
189    }
190    Ok(())
191}
192
193fn malformed_call_argument(message: &str) -> SQLError {
194    SQLError::Internal(format!("malformed call argument: {message}"))
195}
196
197/// Decode SQL call markers carried by expression plans without evaluating arguments.
198pub fn analyze_expression_call_arguments(
199    arguments: &[crate::plan::ExpressionPlan],
200) -> Result<(Vec<ScalarCallArgument<'_>>, bool), SQLError> {
201    let decoded = arguments
202        .iter()
203        .map(|argument| scalar_call_argument(&argument.scalar))
204        .collect::<Result<Vec<_>, _>>()?;
205    let explicit_variadic = validate_scalar_call_arguments(&decoded)?;
206    Ok((decoded, explicit_variadic))
207}