Skip to main content

uqa_sql/
routines.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SQL routine definitions and static signature lookup contracts.
8
9pub mod compilation;
10pub mod configuration;
11pub mod declaration;
12pub mod dependencies;
13pub mod lifecycle;
14pub mod merge_columns;
15pub mod regclass;
16pub mod registration;
17pub mod resolution;
18pub mod security;
19
20use crate::ast::{
21    ColumnType, CreateFunction, FunctionBinding, FunctionReturns, RoutineInvocationBinding,
22};
23use crate::plan::UnifiedPlan;
24use crate::type_resolution::{
25    canonical_routine_type_name, BuiltinFunctionOverload, FunctionTypeResolver,
26    RankedFunctionMatch, ResolvedFunctionOverload,
27};
28use crate::SQLError;
29use std::sync::Arc;
30
31/// A registered routine: the persistable definition plus its
32/// pre-compiled body.
33#[derive(Clone)]
34pub struct SQLUserFunction {
35    pub def: CreateFunction,
36    pub compiled: CompiledFunctionBody,
37}
38
39/// Executable form of a routine body.
40// The project naming convention spells the acronym as `SQL`.
41#[allow(clippy::upper_case_acronyms)]
42#[derive(Clone)]
43pub enum CompiledFunctionBody {
44    PLpgSQL(crate::plpgsql::PLpgSQLFunction),
45    SQL(Vec<UnifiedPlan>),
46}
47pub fn is_routine_namespace_lookup_error(error: &SQLError) -> bool {
48    matches!(
49        error,
50        SQLError::Routine { sqlstate, message }
51            if sqlstate == "3F000"
52                || (sqlstate == "42501"
53                    && message.starts_with("permission denied for schema "))
54    )
55}
56
57/// Function-catalog operations required by static query binding. The interface deliberately excludes storage, transaction, locking, and execution services so the binder can run against a deterministic catalog fixture.
58pub trait RoutineResolution: FunctionTypeResolver {
59    fn has_registered_scalar_function(&self, _name: &str) -> bool {
60        false
61    }
62
63    fn has_registered_table_function(&self, _name: &str) -> bool {
64        false
65    }
66
67    fn has_registered_aggregate_function(&self, _name: &str) -> bool {
68        false
69    }
70
71    fn lookup_visible_sql_functions(
72        &self,
73        _name: &str,
74    ) -> Result<Option<Vec<Arc<SQLUserFunction>>>, SQLError> {
75        Ok(None)
76    }
77
78    /// Resolve candidate metadata without refreshing session-visible function state during analysis.
79    fn lookup_visible_sql_functions_for_analysis(
80        &self,
81        name: &str,
82    ) -> Result<Option<Vec<Arc<SQLUserFunction>>>, SQLError> {
83        self.lookup_visible_sql_functions(name)
84    }
85
86    /// Resolve an exact stored routine name without repeating visible-name lookup.
87    fn lookup_bound_sql_functions(&self, _name: &str) -> Option<Vec<Arc<SQLUserFunction>>> {
88        None
89    }
90
91    fn lookup_bound_sql_functions_by_binding(
92        &self,
93        _binding: &FunctionBinding,
94    ) -> Option<Vec<Arc<SQLUserFunction>>> {
95        None
96    }
97
98    fn resolve_static_sql_function(
99        &self,
100        _name: &str,
101        _binding: Option<&FunctionBinding>,
102        _argument_names: &[Option<String>],
103        _argument_types: &[Option<ColumnType>],
104        _explicit_variadic: bool,
105    ) -> Result<Option<Arc<SQLUserFunction>>, SQLError> {
106        Ok(None)
107    }
108
109    fn resolve_static_sql_function_match(
110        &self,
111        _name: &str,
112        _binding: Option<&FunctionBinding>,
113        _argument_names: &[Option<String>],
114        _argument_types: &[Option<ColumnType>],
115        _explicit_variadic: bool,
116    ) -> Result<Option<StaticFunctionMatch>, SQLError> {
117        Ok(None)
118    }
119
120    fn resolve_table_function_overload_with_builtins(
121        &self,
122        _name: &str,
123        _binding: Option<&FunctionBinding>,
124        _argument_names: &[Option<String>],
125        _argument_types: &[Option<ColumnType>],
126        _explicit_variadic: bool,
127        _builtins: &[BuiltinFunctionOverload],
128    ) -> Result<Option<ResolvedFunctionOverload>, SQLError> {
129        Ok(None)
130    }
131}
132
133pub fn routine_signature_types(def: &CreateFunction) -> Vec<String> {
134    def.identity_params()
135        .iter()
136        .map(|parameter| canonical_routine_type_name(&parameter.type_name))
137        .collect()
138}
139
140pub fn routine_returns_anonymous_record(def: &CreateFunction) -> bool {
141    def.output_params().is_empty()
142        && matches!(
143            &def.returns,
144            FunctionReturns::Scalar { type_name } | FunctionReturns::SetOf { type_name }
145                if canonical_routine_type_name(type_name) == "record"
146        )
147}
148
149pub struct StaticFunctionMatch {
150    pub function: Arc<SQLUserFunction>,
151    pub invocation: Box<RoutineInvocationBinding>,
152    pub argument_types: Vec<String>,
153    pub raw_exact_matches: usize,
154    pub exact_matches: usize,
155    pub preferred_matches: usize,
156    pub variadic_expansion: bool,
157}
158
159impl StaticFunctionMatch {
160    pub fn binding(&self) -> FunctionBinding {
161        FunctionBinding {
162            object_id: self.function.def.object_id,
163            name: self.function.def.name.clone(),
164            argument_types: routine_signature_types(&self.function.def),
165            builtin: false,
166            dispatch: None,
167            invocation: Some(self.invocation.clone()),
168            resolution_error: None,
169        }
170    }
171}
172
173impl RankedFunctionMatch for StaticFunctionMatch {
174    fn argument_types(&self) -> &[String] {
175        &self.argument_types
176    }
177
178    fn raw_exact_matches(&self) -> usize {
179        self.raw_exact_matches
180    }
181
182    fn exact_matches(&self) -> usize {
183        self.exact_matches
184    }
185
186    fn preferred_matches(&self) -> usize {
187        self.preferred_matches
188    }
189
190    fn is_variadic_expansion(&self) -> bool {
191        self.variadic_expansion
192    }
193}
194
195pub fn builtin_routine_support_oid(name: &str) -> Option<i64> {
196    Some(match name.strip_prefix("pg_catalog.").unwrap_or(name) {
197        "textlike_support" => 1023,
198        "texticregexeq_support" => 1024,
199        "texticlike_support" => 1025,
200        "network_subset_support" => 1173,
201        "textregexeq_support" => 1364,
202        "varchar_support" => 3097,
203        "numeric_support" => 3157,
204        _ => return None,
205    })
206}
207
208pub fn function_binding_matches(binding: &FunctionBinding, target: &FunctionBinding) -> bool {
209    if binding.builtin || target.builtin {
210        return false;
211    }
212    match (binding.object_id, target.object_id) {
213        (Some(binding), Some(target)) => binding == target,
214        (None, None) => {
215            binding.name == target.name && binding.argument_types == target.argument_types
216        }
217        _ => false,
218    }
219}
220
221pub fn routine_local_name(name: &str) -> Result<String, SQLError> {
222    uqa_core::RelationIdentity::from_legacy_name(name)
223        .map(|relation| relation.name)
224        .map_err(|error| SQLError::Internal(format!("invalid routine name `{name}`: {error}")))
225}
226
227pub fn routine_kind(def: &CreateFunction) -> &'static str {
228    if def.is_procedure {
229        "procedure"
230    } else {
231        "function"
232    }
233}
234
235pub mod anonymous_block;
236pub mod call;
237pub mod invocation;