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    Operator(Box<OperatorResolutionError>),
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct OperatorResolutionError {
40    pub sqlstate: String,
41    pub message: String,
42}
43
44impl FunctionResolutionError {
45    #[must_use]
46    pub fn sql_error(&self) -> crate::SQLError {
47        let (sqlstate, message) = match self {
48            Self::UndefinedFunction { signature } => (
49                "42883".to_string(),
50                format!("function {signature} does not exist"),
51            ),
52            Self::Operator(error) => (error.sqlstate.clone(), error.message.clone()),
53        };
54        crate::SQLError::Routine { sqlstate, message }
55    }
56}
57
58/// Structural identity for parser-owned expressions and overload-specific built-in implementations. These variants occupy no SQL function-name namespace.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60pub enum FunctionDispatch {
61    NumericOperator(NumericOperator),
62    NamedArgument,
63    VariadicArgument,
64    ArraySubscripts,
65    ArraySlices,
66    Subscript,
67    Slice,
68    AnyOperator,
69    AllOperator,
70    IsDistinct,
71    BetweenSymmetric,
72    ToBinInt4,
73    ToBinInt8,
74    ToHexInt4,
75    ToHexInt8,
76    ToOctInt4,
77    ToOctInt8,
78    RandomInt4Range,
79    RandomInt8Range,
80    RandomNumericRange,
81    ArraySortJson,
82    JsonExtract {
83        as_text: bool,
84        path: bool,
85    },
86    Range {
87        operation: RangeFunctionOperation,
88        subtype: RangeSubtype,
89        multirange: bool,
90    },
91}
92
93/// Operation selected for one typed range or multirange call.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95pub enum RangeFunctionOperation {
96    Lower,
97    Upper,
98    IsEmpty,
99    LowerInclusive,
100    UpperInclusive,
101    LowerInfinite,
102    UpperInfinite,
103    Merge,
104    Multirange,
105    Overlap,
106    Contains,
107    ContainedBy,
108    Adjacent,
109}
110
111impl FunctionDispatch {
112    /// Human-readable expression label used only in diagnostics and serialized plans; dispatch is always selected by the enum variant.
113    #[must_use]
114    pub const fn label(self) -> &'static str {
115        match self {
116            Self::NumericOperator(operator) => operator.symbol(),
117            Self::NamedArgument => "named argument",
118            Self::VariadicArgument => "VARIADIC argument",
119            Self::ArraySubscripts | Self::Subscript => "subscript",
120            Self::ArraySlices | Self::Slice => "slice",
121            Self::AnyOperator => "ANY operator",
122            Self::AllOperator => "ALL operator",
123            Self::IsDistinct => "IS DISTINCT FROM",
124            Self::BetweenSymmetric => "BETWEEN SYMMETRIC",
125            Self::ToBinInt4 | Self::ToBinInt8 => "pg_catalog.to_bin",
126            Self::ToHexInt4 | Self::ToHexInt8 => "pg_catalog.to_hex",
127            Self::ToOctInt4 | Self::ToOctInt8 => "pg_catalog.to_oct",
128            Self::RandomInt4Range | Self::RandomInt8Range | Self::RandomNumericRange => {
129                "pg_catalog.random"
130            }
131            Self::ArraySortJson => "pg_catalog.array_sort",
132            Self::JsonExtract { as_text: false, .. } => "JSON extraction operator",
133            Self::JsonExtract { as_text: true, .. } => "JSON text extraction operator",
134            Self::Range { operation, .. } => operation.label(),
135        }
136    }
137
138    #[must_use]
139    pub const fn is_call_argument_marker(self) -> bool {
140        matches!(self, Self::NamedArgument | Self::VariadicArgument)
141    }
142
143    /// 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.
144    #[doc(hidden)]
145    #[must_use]
146    pub fn from_legacy_serialized_name(name: &str) -> Option<Self> {
147        let fixed = match name {
148            "__named_arg" => Self::NamedArgument,
149            "__variadic_arg" => Self::VariadicArgument,
150            "__array_subscripts" => Self::ArraySubscripts,
151            "__array_slices" => Self::ArraySlices,
152            "__subscript" => Self::Subscript,
153            "__slice" => Self::Slice,
154            "__any_op" => Self::AnyOperator,
155            "__all_op" => Self::AllOperator,
156            "__is_distinct" => Self::IsDistinct,
157            "__between_symmetric" => Self::BetweenSymmetric,
158            "__to_bin_int4" => Self::ToBinInt4,
159            "__to_bin_int8" => Self::ToBinInt8,
160            "__to_hex_int4" => Self::ToHexInt4,
161            "__to_hex_int8" => Self::ToHexInt8,
162            "__to_oct_int4" => Self::ToOctInt4,
163            "__to_oct_int8" => Self::ToOctInt8,
164            "__random_int4_range" => Self::RandomInt4Range,
165            "__random_int8_range" => Self::RandomInt8Range,
166            "__random_numeric_range" => Self::RandomNumericRange,
167            "__array_sort_json" => Self::ArraySortJson,
168            _ => return Self::legacy_range_dispatch(name),
169        };
170        Some(fixed)
171    }
172
173    fn legacy_range_dispatch(name: &str) -> Option<Self> {
174        let encoded = name.strip_prefix("__range_")?;
175        let subtypes = [
176            RangeSubtype::Integer,
177            RangeSubtype::BigInteger,
178            RangeSubtype::Numeric,
179            RangeSubtype::Date,
180            RangeSubtype::Timestamp,
181            RangeSubtype::TimestampTz,
182        ];
183        for subtype in subtypes {
184            for (type_name, multirange) in [
185                (subtype.multirange_name(), true),
186                (subtype.range_name(), false),
187            ] {
188                let Some(operation) = encoded.strip_suffix(type_name) else {
189                    continue;
190                };
191                let operation = match operation.trim_end_matches('_') {
192                    "lower" => RangeFunctionOperation::Lower,
193                    "upper" => RangeFunctionOperation::Upper,
194                    "isempty" => RangeFunctionOperation::IsEmpty,
195                    "lower_inc" => RangeFunctionOperation::LowerInclusive,
196                    "upper_inc" => RangeFunctionOperation::UpperInclusive,
197                    "lower_inf" => RangeFunctionOperation::LowerInfinite,
198                    "upper_inf" => RangeFunctionOperation::UpperInfinite,
199                    "merge" => RangeFunctionOperation::Merge,
200                    "multirange" => RangeFunctionOperation::Multirange,
201                    "overlap" => RangeFunctionOperation::Overlap,
202                    "contains" => RangeFunctionOperation::Contains,
203                    "contained_by" => RangeFunctionOperation::ContainedBy,
204                    "adjacent" => RangeFunctionOperation::Adjacent,
205                    _ => continue,
206                };
207                return Some(Self::Range {
208                    operation,
209                    subtype,
210                    multirange,
211                });
212            }
213        }
214        None
215    }
216}
217
218/// Numeric operator syntax, kept separate from ordinary calls such as `mod` or `abs`.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
220pub enum NumericOperator {
221    Modulo,
222    Power,
223    Plus,
224    SquareRoot,
225    CubeRoot,
226    Absolute,
227}
228
229impl NumericOperator {
230    #[must_use]
231    pub const fn symbol(self) -> &'static str {
232        match self {
233            Self::Modulo => "%",
234            Self::Power => "^",
235            Self::Plus => "+",
236            Self::SquareRoot => "|/",
237            Self::CubeRoot => "||/",
238            Self::Absolute => "@",
239        }
240    }
241
242    #[must_use]
243    pub const fn arity(self) -> usize {
244        match self {
245            Self::Modulo | Self::Power => 2,
246            Self::Plus | Self::SquareRoot | Self::CubeRoot | Self::Absolute => 1,
247        }
248    }
249}
250
251impl RangeFunctionOperation {
252    #[must_use]
253    pub const fn label(self) -> &'static str {
254        match self {
255            Self::Lower => "pg_catalog.lower",
256            Self::Upper => "pg_catalog.upper",
257            Self::IsEmpty => "pg_catalog.isempty",
258            Self::LowerInclusive => "pg_catalog.lower_inc",
259            Self::UpperInclusive => "pg_catalog.upper_inc",
260            Self::LowerInfinite => "pg_catalog.lower_inf",
261            Self::UpperInfinite => "pg_catalog.upper_inf",
262            Self::Merge => "pg_catalog.range_merge",
263            Self::Multirange => "pg_catalog.multirange",
264            Self::Overlap => "range overlap operator",
265            Self::Contains => "range contains operator",
266            Self::ContainedBy => "range contained-by operator",
267            Self::Adjacent => "range adjacent operator",
268        }
269    }
270}
271
272/// Concrete parameter and result types selected for one routine invocation.
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
274pub struct RoutineInvocationBinding {
275    /// Zero-based declared parameter index for each call argument, aligned with the call argument list.
276    pub argument_positions: Vec<usize>,
277    /// Concrete coercion target for each call argument, aligned with the call argument list.
278    pub argument_targets: Vec<String>,
279    /// Declared source types before argument coercion; absent only in legacy bindings.
280    #[serde(default, skip_serializing_if = "Vec::is_empty")]
281    pub argument_sources: Vec<Option<String>>,
282    /// Concrete type for each declared parameter, aligned with [`crate::ast::CreateFunction::params`].
283    pub parameter_types: Vec<String>,
284    /// Concrete invocation result type after polymorphic substitution.
285    pub return_type: Option<String>,
286    /// Whether and where the declared variadic parameter participates in this invocation.
287    pub variadic_mode: RoutineVariadicMode,
288}
289
290/// Call syntax selected for a routine's variadic parameter.
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
292pub enum RoutineVariadicMode {
293    /// The invocation does not use a variadic parameter.
294    #[default]
295    None,
296    /// Trailing call arguments are expanded into the declared variadic array parameter.
297    Expanded {
298        /// Zero-based index in [`crate::ast::CreateFunction::params`].
299        parameter_index: usize,
300    },
301    /// An explicit `VARIADIC` array argument supplies the declared variadic parameter.
302    Explicit {
303        /// Zero-based index in [`crate::ast::CreateFunction::params`].
304        parameter_index: usize,
305    },
306}
307
308impl FunctionBinding {
309    /// Construct the identity marker used when `PostgreSQL` parses a polymorphic syntax expression instead of an ordinary function call.
310    #[must_use]
311    pub fn polymorphic_builtin_syntax(name: &str) -> Self {
312        assert!(Self::is_polymorphic_builtin_syntax_name(name));
313        Self {
314            object_id: None,
315            name: name.into(),
316            argument_types: Vec::new(),
317            builtin: true,
318            dispatch: None,
319            invocation: None,
320            resolution_error: None,
321        }
322    }
323
324    /// Construct a parser- or binder-owned expression with an identity that cannot collide with a SQL routine name.
325    #[must_use]
326    pub fn dispatched(dispatch: FunctionDispatch) -> Self {
327        Self::dispatched_with_control(
328            dispatch,
329            &uqa_core::memory::ProductionControl::uncontrolled(),
330        )
331        .expect("ordinary dispatch constructor cannot be cancelled or limited")
332        .into_uncontrolled()
333        .expect("ordinary dispatch owner")
334    }
335
336    pub fn dispatched_with_control(
337        dispatch: FunctionDispatch,
338        control: &uqa_core::memory::ProductionControl<'_>,
339    ) -> Result<uqa_core::memory::Produced<Self>, uqa_core::ValueRetentionError> {
340        let (name, memory) = control.copy_text(dispatch.label())?.into_parts();
341        control.finish(
342            Self {
343                object_id: None,
344                name,
345                argument_types: Vec::new(),
346                builtin: true,
347                dispatch: Some(dispatch),
348                invocation: None,
349                resolution_error: None,
350            },
351            memory,
352        )
353    }
354
355    /// Preserve an undefined-overload error structurally without fabricating a dispatch name.
356    #[must_use]
357    pub fn undefined_function(name: impl Into<String>, signature: impl Into<String>) -> Self {
358        Self {
359            object_id: None,
360            name: name.into(),
361            argument_types: Vec::new(),
362            builtin: false,
363            dispatch: None,
364            invocation: None,
365            resolution_error: Some(FunctionResolutionError::UndefinedFunction {
366                signature: signature.into(),
367            }),
368        }
369    }
370
371    /// 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.
372    #[doc(hidden)]
373    pub fn upgrade_legacy_serialized_dispatch(
374        display_name: &mut String,
375        binding: &mut Option<Self>,
376    ) -> bool {
377        if binding
378            .as_ref()
379            .is_some_and(|binding| binding.dispatch.is_some() || !binding.builtin)
380        {
381            return false;
382        }
383        let Some(dispatch) = FunctionDispatch::from_legacy_serialized_name(display_name) else {
384            return false;
385        };
386        if let Some(binding) = binding {
387            binding.dispatch = Some(dispatch);
388            display_name.clone_from(&binding.name);
389        } else {
390            let upgraded = Self::dispatched(dispatch);
391            display_name.clone_from(&upgraded.name);
392            *binding = Some(upgraded);
393        }
394        true
395    }
396
397    /// Return whether this binding marks a polymorphic syntax expression whose argument types must be inferred from its operands.
398    #[must_use]
399    pub fn is_polymorphic_builtin_syntax(&self) -> bool {
400        self.builtin
401            && self.argument_types.is_empty()
402            && Self::is_polymorphic_builtin_syntax_name(&self.name)
403    }
404
405    /// Return whether an unqualified local name belongs to `PostgreSQL`'s polymorphic function-like syntax expressions.
406    #[must_use]
407    pub fn is_polymorphic_builtin_syntax_name(name: &str) -> bool {
408        matches!(name, "coalesce" | "greatest" | "least" | "nullif")
409    }
410}
411
412pub type GeneratedFunctionDependency = FunctionBinding;