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