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