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::{EnumFunctionOperation, 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    /// Prepared field identity and its original result type; executable copies refresh its live descriptor without changing the result contract.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub composite_field: Option<Box<CompositeFieldBinding>>,
29    /// 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.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub resolution_error: Option<Box<FunctionResolutionError>>,
32}
33
34/// A field selected during preparation. Attribute numbers are never reused, even when a later attribute has the same name.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct CompositeFieldBinding {
37    pub type_oid: u32,
38    pub number: i16,
39    pub result_type: super::ColumnType,
40    /// The field's declaration at preparation, independently of the argument's collation.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub collation_oid: Option<u32>,
43    pub dropped: bool,
44    /// The live attribute type in an executable copy, when it differs from the original type OID. Type modifiers do not change `FieldSelect`'s result contract.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub changed_type: Option<super::ColumnType>,
47}
48
49/// Static function-call failure discovered while binding declared argument types.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub enum FunctionResolutionError {
52    UndefinedFunction { signature: String },
53    Operator(Box<OperatorResolutionError>),
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct OperatorResolutionError {
58    pub sqlstate: String,
59    pub message: String,
60}
61
62impl FunctionResolutionError {
63    #[must_use]
64    pub fn sql_error(&self) -> crate::SQLError {
65        match self {
66            Self::UndefinedFunction { signature } => {
67                crate::SQLError::undefined_function_call(signature)
68            }
69            Self::Operator(error) => crate::SQLError::Routine {
70                sqlstate: error.sqlstate.clone(),
71                message: error.message.clone(),
72            },
73        }
74    }
75}
76
77/// Structural identity for parser-owned expressions and overload-specific built-in implementations. These variants occupy no SQL function-name namespace.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79pub enum FunctionDispatch {
80    NumericOperator(NumericOperator),
81    /// SQL pattern syntax, retaining its selected binary operator after escape processing.
82    Pattern {
83        operator: super::PatternOperator,
84        oid: Option<std::num::NonZeroU32>,
85    },
86    /// A parser-owned escape helper, evaluated and folded independently of the strict match operator.
87    PatternEscape {
88        similar: bool,
89    },
90    /// A non-strict SQL Boolean test, distinct from routine lookup and equality.
91    BooleanTest(BooleanTest),
92    /// A collation clause, retaining its selected identity independently of its written name.
93    Collate {
94        oid: Option<u32>,
95    },
96    /// The selected argument metadata of `pg_collation_for`; the argument still executes before the observation.
97    CollationFor {
98        collation_oid: u32,
99        noncollatable_type_oid: Option<u32>,
100    },
101    NamedArgument,
102    VariadicArgument,
103    ArraySubscripts,
104    ArraySlices,
105    /// Slice syntax with independently retained omitted endpoints.
106    ArraySlicesWithBounds(super::ArraySliceBounds),
107    Subscript,
108    Slice,
109    /// `(expression).field`: the field of a composite value, or the key of a document map.
110    FieldSelect,
111    AnyOperator,
112    AllOperator,
113    IsDistinct,
114    /// One selected DISTINCT comparison of complete field values; nested ROW constructors remain one record-valued operand.
115    ValueIsDistinct,
116    /// The outer NULL test selected by DISTINCT against a bare NULL, without testing individual record fields or requiring equality.
117    ValueNullTest {
118        negated: bool,
119    },
120    BetweenSymmetric,
121    ToBinInt4,
122    ToBinInt8,
123    ToHexInt4,
124    ToHexInt8,
125    ToOctInt4,
126    ToOctInt8,
127    RandomInt4Range,
128    RandomInt8Range,
129    RandomNumericRange,
130    ArraySortJson,
131    /// SQL concatenation syntax, with its selected catalog overload after operand analysis.
132    Concat {
133        oid: Option<u32>,
134    },
135    /// The selected array `||` overload, retained even when its arguments evaluate to NULL.
136    ArrayConcat,
137    ArrayAppend,
138    ArrayPrepend,
139    /// A resolved JSON path function whose final argument is the declared text array.
140    JsonPath {
141        as_text: bool,
142        jsonb: bool,
143    },
144    JsonExtract {
145        as_text: bool,
146        path: bool,
147    },
148    Range {
149        operation: RangeFunctionOperation,
150        subtype: RangeSubtype,
151        multirange: bool,
152    },
153    /// An `anyenum` support function bound to one concrete enum type.
154    Enum {
155        operation: EnumFunctionOperation,
156        type_oid: u32,
157    },
158    /// The current MERGE action, read from the executor-owned row attribute.
159    MergeAction,
160}
161
162/// The six predicates over SQL's true, false and NULL Boolean inputs.
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
164pub enum BooleanTest {
165    IsTrue,
166    IsNotTrue,
167    IsFalse,
168    IsNotFalse,
169    IsUnknown,
170    IsNotUnknown,
171}
172
173impl BooleanTest {
174    #[must_use]
175    pub const fn label(self) -> &'static str {
176        match self {
177            Self::IsTrue => "IS TRUE",
178            Self::IsNotTrue => "IS NOT TRUE",
179            Self::IsFalse => "IS FALSE",
180            Self::IsNotFalse => "IS NOT FALSE",
181            Self::IsUnknown => "IS UNKNOWN",
182            Self::IsNotUnknown => "IS NOT UNKNOWN",
183        }
184    }
185
186    /// Stable `PostgreSQL` catalog node identity, independent of parser protobuf tags.
187    #[must_use]
188    pub const fn catalog_code(self) -> i64 {
189        match self {
190            Self::IsTrue => 0,
191            Self::IsNotTrue => 1,
192            Self::IsFalse => 2,
193            Self::IsNotFalse => 3,
194            Self::IsUnknown => 4,
195            Self::IsNotUnknown => 5,
196        }
197    }
198
199    #[must_use]
200    pub const fn from_catalog_code(code: i64) -> Option<Self> {
201        match code {
202            0 => Some(Self::IsTrue),
203            1 => Some(Self::IsNotTrue),
204            2 => Some(Self::IsFalse),
205            3 => Some(Self::IsNotFalse),
206            4 => Some(Self::IsUnknown),
207            5 => Some(Self::IsNotUnknown),
208            _ => None,
209        }
210    }
211}
212
213/// Operation selected for one typed range or multirange call.
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
215pub enum RangeFunctionOperation {
216    Lower,
217    Upper,
218    IsEmpty,
219    LowerInclusive,
220    UpperInclusive,
221    LowerInfinite,
222    UpperInfinite,
223    Merge,
224    Multirange,
225    Overlap,
226    Contains,
227    ContainedBy,
228    /// The range operand is first; the second operand has its declared subtype.
229    ContainsElement,
230    /// The range operand is second; the first operand has its declared subtype.
231    ElementContainedBy,
232    Adjacent,
233}
234
235impl FunctionDispatch {
236    /// Whether this expression retains SQL concatenation syntax and operand roles.
237    #[must_use]
238    pub const fn is_concatenation(self) -> bool {
239        matches!(
240            self,
241            Self::Concat { .. } | Self::ArrayConcat | Self::ArrayAppend | Self::ArrayPrepend
242        )
243    }
244
245    /// Human-readable expression label used only in diagnostics and serialized plans; dispatch is always selected by the enum variant.
246    #[must_use]
247    pub const fn label(self) -> &'static str {
248        match self {
249            Self::MergeAction => "merge_action",
250            Self::NumericOperator(operator) => operator.symbol(),
251            Self::Pattern { operator, .. } => operator.syntax(),
252            Self::PatternEscape { similar: false } => "pg_catalog.like_escape",
253            Self::PatternEscape { similar: true } => "pg_catalog.similar_to_escape",
254            Self::BooleanTest(test) => test.label(),
255            Self::Collate { .. } => "COLLATE",
256            Self::CollationFor { .. } => "pg_catalog.pg_collation_for",
257            Self::NamedArgument => "named argument",
258            Self::VariadicArgument => "VARIADIC argument",
259            Self::ArraySubscripts | Self::Subscript => "subscript",
260            Self::ArraySlices | Self::ArraySlicesWithBounds(_) | Self::Slice => "slice",
261            Self::FieldSelect => "field selection",
262            Self::AnyOperator => "ANY operator",
263            Self::AllOperator => "ALL operator",
264            Self::IsDistinct | Self::ValueIsDistinct => "IS DISTINCT FROM",
265            Self::ValueNullTest { negated: false } => "IS NULL",
266            Self::ValueNullTest { negated: true } => "IS NOT NULL",
267            Self::BetweenSymmetric => "BETWEEN SYMMETRIC",
268            Self::ToBinInt4 | Self::ToBinInt8 => "pg_catalog.to_bin",
269            Self::ToHexInt4 | Self::ToHexInt8 => "pg_catalog.to_hex",
270            Self::ToOctInt4 | Self::ToOctInt8 => "pg_catalog.to_oct",
271            Self::RandomInt4Range | Self::RandomInt8Range | Self::RandomNumericRange => {
272                "pg_catalog.random"
273            }
274            Self::ArraySortJson => "pg_catalog.array_sort",
275            Self::Concat { .. } => "||",
276            Self::ArrayConcat => "pg_catalog.array_cat",
277            Self::ArrayAppend => "pg_catalog.array_append",
278            Self::ArrayPrepend => "pg_catalog.array_prepend",
279            Self::JsonPath {
280                as_text: false,
281                jsonb: false,
282            } => "pg_catalog.json_extract_path",
283            Self::JsonPath {
284                as_text: true,
285                jsonb: false,
286            } => "pg_catalog.json_extract_path_text",
287            Self::JsonPath {
288                as_text: false,
289                jsonb: true,
290            } => "pg_catalog.jsonb_extract_path",
291            Self::JsonPath {
292                as_text: true,
293                jsonb: true,
294            } => "pg_catalog.jsonb_extract_path_text",
295            Self::JsonExtract { as_text: false, .. } => "JSON extraction operator",
296            Self::JsonExtract { as_text: true, .. } => "JSON text extraction operator",
297            Self::Range { operation, .. } => operation.label(),
298            Self::Enum { operation, .. } => operation.label(),
299        }
300    }
301
302    #[must_use]
303    pub const fn is_call_argument_marker(self) -> bool {
304        matches!(self, Self::NamedArgument | Self::VariadicArgument)
305    }
306
307    /// The SQL spelling of a retained JSON extraction operator, independent of its diagnostic label.
308    #[must_use]
309    pub const fn json_extraction_symbol(self) -> Option<&'static str> {
310        match self {
311            Self::JsonExtract {
312                as_text: false,
313                path: false,
314            } => Some("->"),
315            Self::JsonExtract {
316                as_text: true,
317                path: false,
318            } => Some("->>"),
319            Self::JsonExtract {
320                as_text: false,
321                path: true,
322            } => Some("#>"),
323            Self::JsonExtract {
324                as_text: true,
325                path: true,
326            } => Some("#>>"),
327            _ => None,
328        }
329    }
330
331    /// Decode function spellings in older durable expressions. This is a catalog migration primitive, never a SQL routine lookup path.
332    #[doc(hidden)]
333    #[must_use]
334    pub fn from_legacy_serialized_name(name: &str) -> Option<Self> {
335        let fixed = match name {
336            "merge_action" => Self::MergeAction,
337            "__named_arg" => Self::NamedArgument,
338            "__variadic_arg" => Self::VariadicArgument,
339            "__array_subscripts" => Self::ArraySubscripts,
340            "__array_slices" => Self::ArraySlices,
341            "__subscript" => Self::Subscript,
342            "__slice" => Self::Slice,
343            "__any_op" => Self::AnyOperator,
344            "__all_op" => Self::AllOperator,
345            "__is_distinct" => Self::IsDistinct,
346            "__between_symmetric" => Self::BetweenSymmetric,
347            "__to_bin_int4" => Self::ToBinInt4,
348            "__to_bin_int8" => Self::ToBinInt8,
349            "__to_hex_int4" => Self::ToHexInt4,
350            "__to_hex_int8" => Self::ToHexInt8,
351            "__to_oct_int4" => Self::ToOctInt4,
352            "__to_oct_int8" => Self::ToOctInt8,
353            "__random_int4_range" => Self::RandomInt4Range,
354            "__random_int8_range" => Self::RandomInt8Range,
355            "__random_numeric_range" => Self::RandomNumericRange,
356            "__array_sort_json" => Self::ArraySortJson,
357            _ => return Self::legacy_range_dispatch(name),
358        };
359        Some(fixed)
360    }
361
362    fn legacy_range_dispatch(name: &str) -> Option<Self> {
363        let encoded = name.strip_prefix("__range_")?;
364        let subtypes = [
365            RangeSubtype::Integer,
366            RangeSubtype::BigInteger,
367            RangeSubtype::Numeric,
368            RangeSubtype::Date,
369            RangeSubtype::Timestamp,
370            RangeSubtype::TimestampTz,
371        ];
372        for subtype in subtypes {
373            for (type_name, multirange) in [
374                (subtype.multirange_name(), true),
375                (subtype.range_name(), false),
376            ] {
377                let Some(operation) = encoded.strip_suffix(type_name) else {
378                    continue;
379                };
380                let operation = match operation.trim_end_matches('_') {
381                    "lower" => RangeFunctionOperation::Lower,
382                    "upper" => RangeFunctionOperation::Upper,
383                    "isempty" => RangeFunctionOperation::IsEmpty,
384                    "lower_inc" => RangeFunctionOperation::LowerInclusive,
385                    "upper_inc" => RangeFunctionOperation::UpperInclusive,
386                    "lower_inf" => RangeFunctionOperation::LowerInfinite,
387                    "upper_inf" => RangeFunctionOperation::UpperInfinite,
388                    "merge" => RangeFunctionOperation::Merge,
389                    "multirange" => RangeFunctionOperation::Multirange,
390                    "overlap" => RangeFunctionOperation::Overlap,
391                    "contains" => RangeFunctionOperation::Contains,
392                    "contained_by" => RangeFunctionOperation::ContainedBy,
393                    "adjacent" => RangeFunctionOperation::Adjacent,
394                    _ => continue,
395                };
396                return Some(Self::Range {
397                    operation,
398                    subtype,
399                    multirange,
400                });
401            }
402        }
403        None
404    }
405}
406
407/// Numeric operator syntax, kept separate from ordinary calls such as `mod` or `abs`.
408#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
409pub enum NumericOperator {
410    Modulo,
411    Power,
412    Plus,
413    SquareRoot,
414    CubeRoot,
415    Absolute,
416}
417
418impl NumericOperator {
419    #[must_use]
420    pub const fn symbol(self) -> &'static str {
421        match self {
422            Self::Modulo => "%",
423            Self::Power => "^",
424            Self::Plus => "+",
425            Self::SquareRoot => "|/",
426            Self::CubeRoot => "||/",
427            Self::Absolute => "@",
428        }
429    }
430
431    #[must_use]
432    pub const fn arity(self) -> usize {
433        match self {
434            Self::Modulo | Self::Power => 2,
435            Self::Plus | Self::SquareRoot | Self::CubeRoot | Self::Absolute => 1,
436        }
437    }
438}
439
440impl RangeFunctionOperation {
441    #[must_use]
442    pub const fn label(self) -> &'static str {
443        match self {
444            Self::Lower => "pg_catalog.lower",
445            Self::Upper => "pg_catalog.upper",
446            Self::IsEmpty => "pg_catalog.isempty",
447            Self::LowerInclusive => "pg_catalog.lower_inc",
448            Self::UpperInclusive => "pg_catalog.upper_inc",
449            Self::LowerInfinite => "pg_catalog.lower_inf",
450            Self::UpperInfinite => "pg_catalog.upper_inf",
451            Self::Merge => "pg_catalog.range_merge",
452            Self::Multirange => "pg_catalog.multirange",
453            Self::Overlap => "range overlap operator",
454            Self::Contains => "range contains operator",
455            Self::ContainedBy => "range contained-by operator",
456            Self::ContainsElement => "range contains element operator",
457            Self::ElementContainedBy => "element contained-by range operator",
458            Self::Adjacent => "range adjacent operator",
459        }
460    }
461}
462
463/// Concrete parameter and result types selected for one routine invocation.
464#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465pub struct RoutineInvocationBinding {
466    /// Zero-based declared parameter index for each call argument, aligned with the call argument list.
467    pub argument_positions: Vec<usize>,
468    /// Concrete coercion target for each call argument, aligned with the call argument list.
469    pub argument_targets: Vec<String>,
470    /// Declared source types before argument coercion; absent only in legacy bindings.
471    #[serde(default, skip_serializing_if = "Vec::is_empty")]
472    pub argument_sources: Vec<Option<String>>,
473    /// Concrete type for each declared parameter, aligned with [`crate::ast::CreateFunction::params`].
474    pub parameter_types: Vec<String>,
475    /// Concrete invocation result type after polymorphic substitution.
476    pub return_type: Option<String>,
477    /// Whether and where the declared variadic parameter participates in this invocation.
478    pub variadic_mode: RoutineVariadicMode,
479}
480
481/// Call syntax selected for a routine's variadic parameter.
482#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
483pub enum RoutineVariadicMode {
484    /// The invocation does not use a variadic parameter.
485    #[default]
486    None,
487    /// Trailing call arguments are expanded into the declared variadic array parameter.
488    Expanded {
489        /// Zero-based index in [`crate::ast::CreateFunction::params`].
490        parameter_index: usize,
491    },
492    /// An explicit `VARIADIC` array argument supplies the declared variadic parameter.
493    Explicit {
494        /// Zero-based index in [`crate::ast::CreateFunction::params`].
495        parameter_index: usize,
496    },
497}
498
499impl FunctionBinding {
500    /// Preserve the predecessor format's encoded omission before an optimizer can fold supplied expressions to NULL.
501    #[doc(hidden)]
502    pub fn upgrade_legacy_array_slices(
503        binding: &mut Option<Self>,
504        arguments: usize,
505        is_bare_null: impl Fn(usize) -> bool,
506    ) -> bool {
507        let Some(binding) = binding
508            .as_mut()
509            .filter(|binding| binding.dispatch == Some(FunctionDispatch::ArraySlices))
510        else {
511            return false;
512        };
513        let Some(Ok(access)) = super::ArrayAccess::from_dispatch(
514            FunctionDispatch::ArraySlices,
515            arguments,
516            is_bare_null,
517        ) else {
518            return false;
519        };
520        binding.dispatch = Some(FunctionDispatch::ArraySlicesWithBounds(
521            access.slice_bounds().expect("slice bounds"),
522        ));
523        true
524    }
525
526    /// Construct the identity marker used when `PostgreSQL` parses a polymorphic syntax expression instead of an ordinary function call.
527    #[must_use]
528    pub fn polymorphic_builtin_syntax(name: &str) -> Self {
529        assert!(Self::is_polymorphic_builtin_syntax_name(name));
530        Self {
531            object_id: None,
532            name: name.into(),
533            argument_types: Vec::new(),
534            builtin: true,
535            dispatch: None,
536            invocation: None,
537            composite_field: None,
538            resolution_error: None,
539        }
540    }
541
542    /// Construct a parser- or binder-owned expression with an identity that cannot collide with a SQL routine name.
543    #[must_use]
544    pub fn dispatched(dispatch: FunctionDispatch) -> Self {
545        Self::dispatched_with_control(
546            dispatch,
547            &uqa_core::memory::ProductionControl::uncontrolled(),
548        )
549        .expect("ordinary dispatch constructor cannot be cancelled or limited")
550        .into_uncontrolled()
551        .expect("ordinary dispatch owner")
552    }
553
554    pub fn dispatched_with_control(
555        dispatch: FunctionDispatch,
556        control: &uqa_core::memory::ProductionControl<'_>,
557    ) -> Result<uqa_core::memory::Produced<Self>, uqa_core::ValueRetentionError> {
558        let (name, memory) = control.copy_text(dispatch.label())?.into_parts();
559        control.finish(
560            Self {
561                object_id: None,
562                name,
563                argument_types: Vec::new(),
564                builtin: true,
565                dispatch: Some(dispatch),
566                invocation: None,
567                composite_field: None,
568                resolution_error: None,
569            },
570            memory,
571        )
572    }
573
574    /// Preserve an undefined-overload error structurally without fabricating a dispatch name.
575    #[must_use]
576    pub fn undefined_function(name: impl Into<String>, signature: impl Into<String>) -> Self {
577        Self {
578            object_id: None,
579            name: name.into(),
580            argument_types: Vec::new(),
581            builtin: false,
582            dispatch: None,
583            invocation: None,
584            composite_field: None,
585            resolution_error: Some(Box::new(FunctionResolutionError::UndefinedFunction {
586                signature: signature.into(),
587            })),
588        }
589    }
590
591    /// Upgrade one function node from older serialized catalog syntax. Bound user routines are left untouched even when their SQL names resemble an old compiler marker.
592    #[doc(hidden)]
593    pub fn upgrade_legacy_serialized_dispatch(
594        display_name: &mut String,
595        binding: &mut Option<Self>,
596    ) -> bool {
597        if binding
598            .as_ref()
599            .is_some_and(|binding| binding.dispatch.is_some() || !binding.builtin)
600        {
601            return false;
602        }
603        let Some(dispatch) = FunctionDispatch::from_legacy_serialized_name(display_name) else {
604            return false;
605        };
606        if let Some(binding) = binding {
607            binding.dispatch = Some(dispatch);
608            display_name.clone_from(&binding.name);
609        } else {
610            let upgraded = Self::dispatched(dispatch);
611            display_name.clone_from(&upgraded.name);
612            *binding = Some(upgraded);
613        }
614        true
615    }
616
617    /// Return whether this binding marks a polymorphic syntax expression whose argument types must be inferred from its operands.
618    #[must_use]
619    pub fn is_polymorphic_builtin_syntax(&self) -> bool {
620        self.builtin
621            && self.argument_types.is_empty()
622            && Self::is_polymorphic_builtin_syntax_name(&self.name)
623    }
624
625    /// Return whether an unqualified local name belongs to `PostgreSQL`'s polymorphic function-like syntax expressions.
626    #[must_use]
627    pub fn is_polymorphic_builtin_syntax_name(name: &str) -> bool {
628        matches!(name, "coalesce" | "greatest" | "least" | "nullif")
629    }
630}
631
632pub type GeneratedFunctionDependency = FunctionBinding;