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    /// Concrete type for each declared parameter, aligned with [`crate::ast::CreateFunction::params`].
218    pub parameter_types: Vec<String>,
219    /// Concrete invocation result type after polymorphic substitution.
220    pub return_type: Option<String>,
221    /// Whether and where the declared variadic parameter participates in this invocation.
222    pub variadic_mode: RoutineVariadicMode,
223}
224
225/// Call syntax selected for a routine's variadic parameter.
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
227pub enum RoutineVariadicMode {
228    /// The invocation does not use a variadic parameter.
229    #[default]
230    None,
231    /// Trailing call arguments are expanded into the declared variadic array parameter.
232    Expanded {
233        /// Zero-based index in [`crate::ast::CreateFunction::params`].
234        parameter_index: usize,
235    },
236    /// An explicit `VARIADIC` array argument supplies the declared variadic parameter.
237    Explicit {
238        /// Zero-based index in [`crate::ast::CreateFunction::params`].
239        parameter_index: usize,
240    },
241}
242
243impl FunctionBinding {
244    /// Construct the identity marker used when `PostgreSQL` parses a polymorphic syntax expression instead of an ordinary function call.
245    #[must_use]
246    pub fn polymorphic_builtin_syntax(name: &str) -> Self {
247        assert!(Self::is_polymorphic_builtin_syntax_name(name));
248        Self {
249            object_id: None,
250            name: name.into(),
251            argument_types: Vec::new(),
252            builtin: true,
253            dispatch: None,
254            invocation: None,
255            resolution_error: None,
256        }
257    }
258
259    /// Construct a parser- or binder-owned expression with an identity that cannot collide with a SQL routine name.
260    #[must_use]
261    pub fn dispatched(dispatch: FunctionDispatch) -> Self {
262        Self {
263            object_id: None,
264            name: dispatch.label().into(),
265            argument_types: Vec::new(),
266            builtin: true,
267            dispatch: Some(dispatch),
268            invocation: None,
269            resolution_error: None,
270        }
271    }
272
273    /// Preserve an undefined-overload error structurally without fabricating a dispatch name.
274    #[must_use]
275    pub fn undefined_function(name: impl Into<String>, signature: impl Into<String>) -> Self {
276        Self {
277            object_id: None,
278            name: name.into(),
279            argument_types: Vec::new(),
280            builtin: false,
281            dispatch: None,
282            invocation: None,
283            resolution_error: Some(FunctionResolutionError::UndefinedFunction {
284                signature: signature.into(),
285            }),
286        }
287    }
288
289    /// 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.
290    #[doc(hidden)]
291    pub fn upgrade_legacy_serialized_dispatch(
292        display_name: &mut String,
293        binding: &mut Option<Self>,
294    ) -> bool {
295        if binding
296            .as_ref()
297            .is_some_and(|binding| binding.dispatch.is_some() || !binding.builtin)
298        {
299            return false;
300        }
301        let Some(dispatch) = FunctionDispatch::from_legacy_serialized_name(display_name) else {
302            return false;
303        };
304        if let Some(binding) = binding {
305            binding.dispatch = Some(dispatch);
306            display_name.clone_from(&binding.name);
307        } else {
308            let upgraded = Self::dispatched(dispatch);
309            display_name.clone_from(&upgraded.name);
310            *binding = Some(upgraded);
311        }
312        true
313    }
314
315    /// Return whether this binding marks a polymorphic syntax expression whose argument types must be inferred from its operands.
316    #[must_use]
317    pub fn is_polymorphic_builtin_syntax(&self) -> bool {
318        self.builtin
319            && self.argument_types.is_empty()
320            && Self::is_polymorphic_builtin_syntax_name(&self.name)
321    }
322
323    /// Return whether an unqualified local name belongs to `PostgreSQL`'s polymorphic function-like syntax expressions.
324    #[must_use]
325    pub fn is_polymorphic_builtin_syntax_name(name: &str) -> bool {
326        matches!(name, "coalesce" | "greatest" | "least" | "nullif")
327    }
328}
329
330pub type GeneratedFunctionDependency = FunctionBinding;