Skip to main content

uqa_sql/catalog/node_tree/expressions/
functions.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Keep a routine's declared identity separate from its supplied call arguments.
8
9use super::{
10    ColumnType, Expr, ExpressionContext, Field, FunctionBinding, Node, RoutineIdentity, SQLError,
11    TypedNode,
12};
13use crate::ast::{
14    FunctionCallSyntax, FunctionDispatch, RoutineInvocationBinding, RoutineVariadicMode,
15};
16use crate::catalog::type_metadata::{pg_type_collation_oid, pg_type_oid};
17use crate::expr::{call_argument_name, call_argument_value, variadic_argument_value};
18use crate::type_resolution::RoutineCallDescriptor;
19
20impl RoutineIdentity {
21    /// A fixed positional signature with no defaults or variadic expansion.
22    #[must_use]
23    pub fn positional(oid: i64, argument_types: Vec<ColumnType>, result_type: ColumnType) -> Self {
24        let names = argument_types
25            .iter()
26            .map(ColumnType::catalog_name)
27            .collect::<Vec<_>>();
28        Self {
29            oid,
30            invocation: RoutineInvocationBinding {
31                argument_positions: (0..names.len()).collect(),
32                argument_targets: names.clone(),
33                argument_sources: Vec::new(),
34                parameter_types: names,
35                return_type: Some(result_type.catalog_name()),
36                variadic_mode: RoutineVariadicMode::None,
37            },
38            result_type,
39        }
40    }
41}
42
43impl ExpressionContext<'_> {
44    pub(super) fn function(
45        &self,
46        name: &str,
47        binding: Option<&FunctionBinding>,
48        arguments: &[Expr],
49        syntax: FunctionCallSyntax,
50    ) -> Result<TypedNode, SQLError> {
51        let types = arguments
52            .iter()
53            .map(|arg| self.expression_type(call_argument_value(arg)))
54            .collect::<Result<Vec<_>, _>>()?;
55        let names = arguments
56            .iter()
57            .map(|arg| call_argument_name(arg).map(|name| name.map(str::to_owned)))
58            .collect::<Result<Vec<_>, _>>()?;
59        let helper = if let Some(dispatch @ FunctionDispatch::PatternEscape { similar }) =
60            binding.and_then(|binding| binding.dispatch)
61        {
62            if !(1..=2).contains(&types.len()) {
63                return Err(SQLError::Internal("invalid pattern escape operands".into()));
64            }
65            let inputs = [
66                types.first().and_then(Option::as_ref),
67                types.get(1).and_then(Option::as_ref),
68            ];
69            let ty = crate::type_resolution::pattern::select_escape(
70                similar,
71                &inputs[..types.len()],
72                &uqa_core::memory::ProductionControl::uncontrolled(),
73            )?;
74            let mut selected = FunctionBinding::dispatched(dispatch);
75            selected.argument_types = vec![ty.sql_name(); types.len()];
76            Some(selected)
77        } else {
78            None
79        };
80        let name = helper
81            .as_ref()
82            .map_or(name, |binding| binding.name.as_str());
83        let routine = self.routines.resolve(
84            name,
85            helper.as_ref().or(binding),
86            RoutineCallDescriptor {
87                argument_names: &names,
88                argument_types: &types,
89                explicit_variadic: arguments
90                    .last()
91                    .is_some_and(|arg| variadic_argument_value(arg).is_some()),
92            },
93        )?;
94        let (arguments, variadic) =
95            self.routine_arguments(arguments, &names, &routine.invocation)?;
96        Ok(TypedNode {
97            node: Node::new(
98                "FUNCEXPR",
99                [
100                    ("funcid", routine.oid.into()),
101                    ("funcresulttype", pg_type_oid(&routine.result_type).into()),
102                    ("funcretset", false.into()),
103                    ("funcvariadic", variadic.into()),
104                    (
105                        "funcformat",
106                        if syntax == FunctionCallSyntax::Extract {
107                            3.into()
108                        } else {
109                            0.into()
110                        },
111                    ),
112                    (
113                        "funccollid",
114                        pg_type_collation_oid(&routine.result_type).into(),
115                    ),
116                    ("inputcollid", 0.into()),
117                    ("args", Field::List(arguments)),
118                    ("location", (-1).into()),
119                ],
120            ),
121            ty: Some(routine.result_type),
122        })
123    }
124
125    fn routine_arguments(
126        &self,
127        arguments: &[Expr],
128        names: &[Option<String>],
129        invocation: &RoutineInvocationBinding,
130    ) -> Result<(Vec<Field>, bool), SQLError> {
131        if invocation.argument_targets.len() != arguments.len()
132            || invocation.argument_positions.len() != arguments.len()
133        {
134            return Err(SQLError::Internal(
135                "catalog invocation differs from supplied arguments".into(),
136            ));
137        }
138        let packed = match invocation.variadic_mode {
139            RoutineVariadicMode::Expanded { parameter_index }
140                if invocation
141                    .parameter_types
142                    .get(parameter_index)
143                    .is_some_and(|ty| ty != "any") =>
144            {
145                Some(parameter_index)
146            }
147            _ => None,
148        };
149        let mut fields = Vec::with_capacity(arguments.len());
150        let mut elements = Vec::new();
151        for (index, argument) in arguments.iter().enumerate() {
152            let position = invocation.argument_positions[index];
153            if position >= invocation.parameter_types.len() {
154                return Err(SQLError::Internal(
155                    "invalid catalog invocation parameter".into(),
156                ));
157            }
158            let value = call_argument_value(argument);
159            let target = &invocation.argument_targets[index];
160            let node = if target == "unknown" {
161                let Expr::Literal(value) = value else {
162                    return Err(SQLError::Internal(
163                        "unknown routine argument is not a literal".into(),
164                    ));
165                };
166                super::values::unknown_constant(value)?
167            } else {
168                self.encode(value, Some(&self.resolve_type(target)?))?.node
169            };
170            if packed == Some(position) {
171                if names[index].is_some() {
172                    return Err(SQLError::Internal(
173                        "expanded variadic argument has a name".into(),
174                    ));
175                }
176                elements.push(node.into());
177            } else if let Some(name) = &names[index] {
178                fields.push(named_argument(node, name, position)?.into());
179            } else {
180                fields.push(node.into());
181            }
182        }
183        if let Some(position) = packed {
184            let ty = self.resolve_type(&invocation.parameter_types[position])?;
185            fields.push(variadic_array(&ty, elements)?.into());
186        }
187        Ok((
188            fields,
189            packed.is_some()
190                || matches!(
191                    invocation.variadic_mode,
192                    RoutineVariadicMode::Explicit { .. }
193                ),
194        ))
195    }
196}
197
198fn named_argument(node: Node, name: &str, position: usize) -> Result<Node, SQLError> {
199    let position = i64::try_from(position)
200        .map_err(|_| super::super::invalid("routine argument position exceeds catalog integer"))?;
201    Ok(Node::new(
202        "NAMEDARGEXPR",
203        [
204            ("arg", node.into()),
205            ("name", Field::Atom(name.into())),
206            ("argnumber", position.into()),
207            ("location", (-1).into()),
208        ],
209    ))
210}
211
212fn variadic_array(ty: &ColumnType, elements: Vec<Field>) -> Result<Node, SQLError> {
213    let ColumnType::Array(element) = ty else {
214        return Err(SQLError::Internal(
215            "expanded routine parameter is not an array".into(),
216        ));
217    };
218    Ok(Node::new(
219        "ARRAYEXPR",
220        [
221            ("array_typeid", pg_type_oid(ty).into()),
222            ("array_collid", pg_type_collation_oid(ty).into()),
223            ("element_typeid", pg_type_oid(element).into()),
224            ("elements", Field::List(elements)),
225            ("multidims", false.into()),
226            ("list_start", (-1).into()),
227            ("list_end", (-1).into()),
228            ("location", (-1).into()),
229        ],
230    ))
231}