Skip to main content

uqa_sql/ast/
function_binding.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7use serde::{Deserialize, Serialize};
8
9use super::RangeSubtype;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct FunctionBinding {
13    /// Stable identity of a bound user routine. Built-ins and unbound calls leave this unset.
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub object_id: Option<[u8; 16]>,
16    pub name: String,
17    pub argument_types: Vec<String>,
18    #[serde(default)]
19    pub builtin: bool,
20    /// Executor operation selected structurally during parsing or overload binding. SQL-visible routine lookup never consults display-name conventions for these operations.
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub dispatch: Option<FunctionDispatch>,
23    /// Concrete invocation contract selected during routine overload resolution.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub invocation: Option<Box<RoutineInvocationBinding>>,
26    /// A typed overload-resolution failure retained until the expression reaches a fallible planning or execution boundary. This never reuses the SQL function-name namespace as an error channel.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub resolution_error: Option<FunctionResolutionError>,
29}
30
31/// Static function-call failure discovered while binding declared argument types.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub enum FunctionResolutionError {
34    UndefinedFunction { signature: String },
35}
36
37/// Structural identity for parser-owned expressions and overload-specific built-in implementations. These variants occupy no SQL function-name namespace.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39pub enum FunctionDispatch {
40    NamedArgument,
41    VariadicArgument,
42    ArraySubscripts,
43    ArraySlices,
44    Subscript,
45    Slice,
46    AnyOperator,
47    AllOperator,
48    IsDistinct,
49    BetweenSymmetric,
50    ToBinInt4,
51    ToBinInt8,
52    ToHexInt4,
53    ToHexInt8,
54    ToOctInt4,
55    ToOctInt8,
56    RandomInt4Range,
57    RandomInt8Range,
58    RandomNumericRange,
59    ArraySortJson,
60    Range {
61        operation: RangeFunctionOperation,
62        subtype: RangeSubtype,
63        multirange: bool,
64    },
65}
66
67/// Operation selected for one typed range or multirange call.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69pub enum RangeFunctionOperation {
70    Lower,
71    Upper,
72    IsEmpty,
73    LowerInclusive,
74    UpperInclusive,
75    LowerInfinite,
76    UpperInfinite,
77    Merge,
78    Multirange,
79    Overlap,
80    Contains,
81    ContainedBy,
82    Adjacent,
83}
84
85impl FunctionDispatch {
86    /// Human-readable expression label used only in diagnostics and serialized plans; dispatch is always selected by the enum variant.
87    #[must_use]
88    pub const fn label(self) -> &'static str {
89        match self {
90            Self::NamedArgument => "named argument",
91            Self::VariadicArgument => "VARIADIC argument",
92            Self::ArraySubscripts | Self::Subscript => "subscript",
93            Self::ArraySlices | Self::Slice => "slice",
94            Self::AnyOperator => "ANY operator",
95            Self::AllOperator => "ALL operator",
96            Self::IsDistinct => "IS DISTINCT FROM",
97            Self::BetweenSymmetric => "BETWEEN SYMMETRIC",
98            Self::ToBinInt4 | Self::ToBinInt8 => "pg_catalog.to_bin",
99            Self::ToHexInt4 | Self::ToHexInt8 => "pg_catalog.to_hex",
100            Self::ToOctInt4 | Self::ToOctInt8 => "pg_catalog.to_oct",
101            Self::RandomInt4Range | Self::RandomInt8Range | Self::RandomNumericRange => {
102                "pg_catalog.random"
103            }
104            Self::ArraySortJson => "pg_catalog.array_sort",
105            Self::Range { operation, .. } => operation.label(),
106        }
107    }
108
109    #[must_use]
110    pub const fn is_call_argument_marker(self) -> bool {
111        matches!(self, Self::NamedArgument | Self::VariadicArgument)
112    }
113
114    /// Decode the compiler-private function spellings written into durable expressions by releases through 0.1.6. This is a catalog migration primitive, never a SQL routine lookup path.
115    #[doc(hidden)]
116    #[must_use]
117    pub fn from_legacy_serialized_name(name: &str) -> Option<Self> {
118        let fixed = match name {
119            "__named_arg" => Self::NamedArgument,
120            "__variadic_arg" => Self::VariadicArgument,
121            "__array_subscripts" => Self::ArraySubscripts,
122            "__array_slices" => Self::ArraySlices,
123            "__subscript" => Self::Subscript,
124            "__slice" => Self::Slice,
125            "__any_op" => Self::AnyOperator,
126            "__all_op" => Self::AllOperator,
127            "__is_distinct" => Self::IsDistinct,
128            "__between_symmetric" => Self::BetweenSymmetric,
129            "__to_bin_int4" => Self::ToBinInt4,
130            "__to_bin_int8" => Self::ToBinInt8,
131            "__to_hex_int4" => Self::ToHexInt4,
132            "__to_hex_int8" => Self::ToHexInt8,
133            "__to_oct_int4" => Self::ToOctInt4,
134            "__to_oct_int8" => Self::ToOctInt8,
135            "__random_int4_range" => Self::RandomInt4Range,
136            "__random_int8_range" => Self::RandomInt8Range,
137            "__random_numeric_range" => Self::RandomNumericRange,
138            "__array_sort_json" => Self::ArraySortJson,
139            _ => return Self::legacy_range_dispatch(name),
140        };
141        Some(fixed)
142    }
143
144    fn legacy_range_dispatch(name: &str) -> Option<Self> {
145        let encoded = name.strip_prefix("__range_")?;
146        let subtypes = [
147            RangeSubtype::Integer,
148            RangeSubtype::BigInteger,
149            RangeSubtype::Numeric,
150            RangeSubtype::Date,
151            RangeSubtype::Timestamp,
152            RangeSubtype::TimestampTz,
153        ];
154        for subtype in subtypes {
155            for (type_name, multirange) in [
156                (subtype.multirange_name(), true),
157                (subtype.range_name(), false),
158            ] {
159                let Some(operation) = encoded.strip_suffix(type_name) else {
160                    continue;
161                };
162                let operation = match operation.trim_end_matches('_') {
163                    "lower" => RangeFunctionOperation::Lower,
164                    "upper" => RangeFunctionOperation::Upper,
165                    "isempty" => RangeFunctionOperation::IsEmpty,
166                    "lower_inc" => RangeFunctionOperation::LowerInclusive,
167                    "upper_inc" => RangeFunctionOperation::UpperInclusive,
168                    "lower_inf" => RangeFunctionOperation::LowerInfinite,
169                    "upper_inf" => RangeFunctionOperation::UpperInfinite,
170                    "merge" => RangeFunctionOperation::Merge,
171                    "multirange" => RangeFunctionOperation::Multirange,
172                    "overlap" => RangeFunctionOperation::Overlap,
173                    "contains" => RangeFunctionOperation::Contains,
174                    "contained_by" => RangeFunctionOperation::ContainedBy,
175                    "adjacent" => RangeFunctionOperation::Adjacent,
176                    _ => continue,
177                };
178                return Some(Self::Range {
179                    operation,
180                    subtype,
181                    multirange,
182                });
183            }
184        }
185        None
186    }
187}
188
189impl RangeFunctionOperation {
190    #[must_use]
191    pub const fn label(self) -> &'static str {
192        match self {
193            Self::Lower => "pg_catalog.lower",
194            Self::Upper => "pg_catalog.upper",
195            Self::IsEmpty => "pg_catalog.isempty",
196            Self::LowerInclusive => "pg_catalog.lower_inc",
197            Self::UpperInclusive => "pg_catalog.upper_inc",
198            Self::LowerInfinite => "pg_catalog.lower_inf",
199            Self::UpperInfinite => "pg_catalog.upper_inf",
200            Self::Merge => "pg_catalog.range_merge",
201            Self::Multirange => "pg_catalog.multirange",
202            Self::Overlap => "range overlap operator",
203            Self::Contains => "range contains operator",
204            Self::ContainedBy => "range contained-by operator",
205            Self::Adjacent => "range adjacent operator",
206        }
207    }
208}
209
210/// Concrete parameter and result types selected for one routine invocation.
211#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
212pub struct RoutineInvocationBinding {
213    /// Zero-based declared parameter index for each call argument, aligned with the call argument list.
214    pub argument_positions: Vec<usize>,
215    /// Concrete coercion target for each call argument, aligned with the call argument list.
216    pub argument_targets: Vec<String>,
217    /// Declared source types before argument coercion; absent only in legacy bindings.
218    #[serde(default, skip_serializing_if = "Vec::is_empty")]
219    pub argument_sources: Vec<Option<String>>,
220    /// Concrete type for each declared parameter, aligned with [`crate::ast::CreateFunction::params`].
221    pub parameter_types: Vec<String>,
222    /// Concrete invocation result type after polymorphic substitution.
223    pub return_type: Option<String>,
224    /// Whether and where the declared variadic parameter participates in this invocation.
225    pub variadic_mode: RoutineVariadicMode,
226}
227
228/// Call syntax selected for a routine's variadic parameter.
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
230pub enum RoutineVariadicMode {
231    /// The invocation does not use a variadic parameter.
232    #[default]
233    None,
234    /// Trailing call arguments are expanded into the declared variadic array parameter.
235    Expanded {
236        /// Zero-based index in [`crate::ast::CreateFunction::params`].
237        parameter_index: usize,
238    },
239    /// An explicit `VARIADIC` array argument supplies the declared variadic parameter.
240    Explicit {
241        /// Zero-based index in [`crate::ast::CreateFunction::params`].
242        parameter_index: usize,
243    },
244}
245
246impl FunctionBinding {
247    /// Construct the identity marker used when `PostgreSQL` parses a polymorphic syntax expression instead of an ordinary function call.
248    #[must_use]
249    pub fn polymorphic_builtin_syntax(name: &str) -> Self {
250        assert!(Self::is_polymorphic_builtin_syntax_name(name));
251        Self {
252            object_id: None,
253            name: name.into(),
254            argument_types: Vec::new(),
255            builtin: true,
256            dispatch: None,
257            invocation: None,
258            resolution_error: None,
259        }
260    }
261
262    /// Construct a parser- or binder-owned expression with an identity that cannot collide with a SQL routine name.
263    #[must_use]
264    pub fn dispatched(dispatch: FunctionDispatch) -> Self {
265        Self {
266            object_id: None,
267            name: dispatch.label().into(),
268            argument_types: Vec::new(),
269            builtin: true,
270            dispatch: Some(dispatch),
271            invocation: None,
272            resolution_error: None,
273        }
274    }
275
276    /// Preserve an undefined-overload error structurally without fabricating a dispatch name.
277    #[must_use]
278    pub fn undefined_function(name: impl Into<String>, signature: impl Into<String>) -> Self {
279        Self {
280            object_id: None,
281            name: name.into(),
282            argument_types: Vec::new(),
283            builtin: false,
284            dispatch: None,
285            invocation: None,
286            resolution_error: Some(FunctionResolutionError::UndefinedFunction {
287                signature: signature.into(),
288            }),
289        }
290    }
291
292    /// Upgrade one function node deserialized from the catalog format written by releases through 0.1.6. Bound user routines are deliberately left untouched even when their SQL names resemble an old compiler marker.
293    #[doc(hidden)]
294    pub fn upgrade_legacy_serialized_dispatch(
295        display_name: &mut String,
296        binding: &mut Option<Self>,
297    ) -> bool {
298        if binding
299            .as_ref()
300            .is_some_and(|binding| binding.dispatch.is_some() || !binding.builtin)
301        {
302            return false;
303        }
304        let Some(dispatch) = FunctionDispatch::from_legacy_serialized_name(display_name) else {
305            return false;
306        };
307        if let Some(binding) = binding {
308            binding.dispatch = Some(dispatch);
309            display_name.clone_from(&binding.name);
310        } else {
311            let upgraded = Self::dispatched(dispatch);
312            display_name.clone_from(&upgraded.name);
313            *binding = Some(upgraded);
314        }
315        true
316    }
317
318    /// Return whether this binding marks a polymorphic syntax expression whose argument types must be inferred from its operands.
319    #[must_use]
320    pub fn is_polymorphic_builtin_syntax(&self) -> bool {
321        self.builtin
322            && self.argument_types.is_empty()
323            && Self::is_polymorphic_builtin_syntax_name(&self.name)
324    }
325
326    /// Return whether an unqualified local name belongs to `PostgreSQL`'s polymorphic function-like syntax expressions.
327    #[must_use]
328    pub fn is_polymorphic_builtin_syntax_name(name: &str) -> bool {
329        matches!(name, "coalesce" | "greatest" | "least" | "nullif")
330    }
331}
332
333pub type GeneratedFunctionDependency = FunctionBinding;