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 attributes;
10pub mod body_parameters;
11pub mod body_validation;
12pub mod compilation;
13pub mod configuration;
14pub mod declaration;
15mod defaults;
16pub mod definition_output;
17pub mod dependencies;
18pub mod inlining;
19pub mod lifecycle;
20pub mod merge_columns;
21pub mod privilege_inquiry;
22pub mod regclass;
23pub mod registration;
24pub mod resolution;
25pub mod result_check;
26pub mod security;
27
28use crate::ast::{
29    ColumnType, CreateFunction, FunctionBinding, FunctionReturns, RoutineInvocationBinding,
30};
31use crate::plan::UnifiedPlan;
32use crate::type_resolution::{
33    canonical_routine_type_name, BuiltinFunctionOverload, FunctionTypeResolver,
34    RankedFunctionMatch, ResolvedFunctionOverload,
35};
36use crate::SQLError;
37use std::sync::{Arc, OnceLock};
38
39/// A registered routine: the persistable definition and its body as the catalog keeps it.
40#[derive(Clone)]
41pub struct SQLUserFunction {
42    pub def: CreateFunction,
43    pub body: RoutineBody,
44    version: OnceLock<u64>,
45}
46
47impl SQLUserFunction {
48    #[must_use]
49    pub const fn new(def: CreateFunction, body: RoutineBody) -> Self {
50        Self {
51            def,
52            body,
53            version: OnceLock::new(),
54        }
55    }
56
57    /// The identity of this catalog tuple, preserved through reload and rollback. A legacy definition starts from its immutable object identity; publication assigns a new revision even when it keeps every SQL attribute unchanged.
58    #[must_use]
59    pub fn catalog_revision(&self) -> Option<[u8; 16]> {
60        self.def.catalog_revision.or(self.def.object_id)
61    }
62
63    /// A fingerprint for the compiled-body cache, including the published catalog revision so an identical replacement recompiles its body. Exact catalog dependency checks use `catalog_revision` instead of this hash.
64    pub fn definition_version(&self) -> Result<u64, SQLError> {
65        if let Some(version) = self.version.get() {
66            return Ok(*version);
67        }
68        let encoded = serde_json::to_vec(&self.def).map_err(|error| {
69            SQLError::Internal(format!(
70                "encode routine `{}` definition: {error}",
71                self.def.name
72            ))
73        })?;
74        // FNV-1a over the encoded definition.
75        let version = encoded
76            .iter()
77            .fold(0xcbf2_9ce4_8422_2325_u64, |hash, byte| {
78                (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3)
79            });
80        Ok(*self.version.get_or_init(|| version))
81    }
82}
83
84/// A routine body as the catalog keeps it. A SQL-standard body is bound when the routine is defined, as `PostgreSQL` stores `prosqlbody` as parse trees that name objects by OID. A body given as a string is compiled by each session that uses the routine, as the backend function cache compiles `prosrc`, so its names resolve when that session first needs them.
85/// A routine body as the catalog keeps it.
86#[derive(Clone)]
87pub enum RoutineBody {
88    /// A SQL-standard body, which the statement that defines the routine analyzes and binds.
89    Bound(Arc<CompiledFunctionBody>),
90    /// A body given as a string, which the catalog keeps as text, as `PostgreSQL` keeps `prosrc`: `CREATE FUNCTION` validates it only under `check_function_bodies`, and each session compiles it when it first calls the routine, so a body that no longer compiles reports its error then.
91    Source,
92}
93
94/// The body a static analysis sees, or `None` when the body does not compile in this session. Such a routine cannot run in the session, and running it reports the compilation error, as `PostgreSQL` compiles a routine only when it runs; internal failures still propagate.
95pub fn analyzable_routine_body(
96    resolution: &(impl RoutineResolution + ?Sized),
97    function: &SQLUserFunction,
98) -> Result<Option<Arc<CompiledFunctionBody>>, SQLError> {
99    match resolution.routine_analysis_body(function) {
100        Ok(body) => Ok(Some(body)),
101        Err(SQLError::Internal(message)) => Err(SQLError::Internal(message)),
102        Err(_) => Ok(None),
103    }
104}
105
106/// Executable form of a routine body.
107// The project naming convention spells the acronym as `SQL`.
108#[allow(clippy::upper_case_acronyms)]
109#[derive(Clone)]
110pub enum CompiledFunctionBody {
111    PLpgSQL(crate::plpgsql::PLpgSQLFunction),
112    SQL(Vec<UnifiedPlan>),
113}
114pub fn is_routine_namespace_lookup_error(error: &SQLError) -> bool {
115    matches!(
116        error,
117        SQLError::Routine { sqlstate, message }
118            if sqlstate == "3F000"
119                || (sqlstate == "42501"
120                    && message.starts_with("permission denied for schema "))
121    )
122}
123
124/// 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.
125pub trait RoutineResolution: FunctionTypeResolver {
126    fn has_registered_scalar_function(&self, _name: &str) -> bool {
127        false
128    }
129
130    /// The compiled body of a routine as this session executes it: a bound body as defined, or a source body as this session compiled it. A resolver without a session holds only bound bodies.
131    fn routine_body(
132        &self,
133        function: &SQLUserFunction,
134    ) -> Result<Arc<CompiledFunctionBody>, SQLError> {
135        match &function.body {
136            RoutineBody::Bound(body) => Ok(Arc::clone(body)),
137            RoutineBody::Source => Err(SQLError::Internal(format!(
138                "routine `{}` has no session to compile its body",
139                function.def.name
140            ))),
141        }
142    }
143
144    /// Inspect a body for static effects and portal dependencies. A session may
145    /// compile source independently here without creating its execution cache
146    /// entry or issuing diagnostics for a body the query never reaches.
147    fn routine_analysis_body(
148        &self,
149        function: &SQLUserFunction,
150    ) -> Result<Arc<CompiledFunctionBody>, SQLError> {
151        self.routine_body(function)
152    }
153
154    fn has_registered_table_function(&self, _name: &str) -> bool {
155        false
156    }
157
158    fn has_registered_aggregate_function(&self, _name: &str) -> bool {
159        false
160    }
161
162    fn lookup_visible_sql_functions(
163        &self,
164        _name: &str,
165    ) -> Result<Option<Vec<Arc<SQLUserFunction>>>, SQLError> {
166        Ok(None)
167    }
168
169    /// Resolve candidate metadata without refreshing session-visible function state during analysis.
170    fn lookup_visible_sql_functions_for_analysis(
171        &self,
172        name: &str,
173    ) -> Result<Option<Vec<Arc<SQLUserFunction>>>, SQLError> {
174        self.lookup_visible_sql_functions(name)
175    }
176
177    /// Resolve an exact stored routine name without repeating visible-name lookup.
178    fn lookup_bound_sql_functions(&self, _name: &str) -> Option<Vec<Arc<SQLUserFunction>>> {
179        None
180    }
181
182    fn lookup_bound_sql_functions_by_binding(
183        &self,
184        _binding: &FunctionBinding,
185    ) -> Option<Vec<Arc<SQLUserFunction>>> {
186        None
187    }
188
189    fn resolve_static_sql_function(
190        &self,
191        _name: &str,
192        _binding: Option<&FunctionBinding>,
193        _argument_names: &[Option<String>],
194        _argument_types: &[Option<ColumnType>],
195        _explicit_variadic: bool,
196    ) -> Result<Option<Arc<SQLUserFunction>>, SQLError> {
197        Ok(None)
198    }
199
200    fn resolve_static_sql_function_match(
201        &self,
202        _name: &str,
203        _binding: Option<&FunctionBinding>,
204        _argument_names: &[Option<String>],
205        _argument_types: &[Option<ColumnType>],
206        _explicit_variadic: bool,
207    ) -> Result<Option<StaticFunctionMatch>, SQLError> {
208        Ok(None)
209    }
210
211    fn resolve_table_function_overload_with_builtins(
212        &self,
213        _name: &str,
214        _binding: Option<&FunctionBinding>,
215        _argument_names: &[Option<String>],
216        _argument_types: &[Option<ColumnType>],
217        _explicit_variadic: bool,
218        _builtins: &[BuiltinFunctionOverload],
219    ) -> Result<Option<ResolvedFunctionOverload>, SQLError> {
220        Ok(None)
221    }
222}
223
224pub fn routine_signature_types(def: &CreateFunction) -> Vec<String> {
225    def.identity_params()
226        .iter()
227        .map(|parameter| canonical_routine_type_name(&parameter.type_name))
228        .collect()
229}
230
231pub fn routine_returns_anonymous_record(def: &CreateFunction) -> bool {
232    def.output_params().is_empty()
233        && matches!(
234            &def.returns,
235            FunctionReturns::Scalar { type_name } | FunctionReturns::SetOf { type_name }
236                if canonical_routine_type_name(type_name) == "record"
237        )
238}
239
240pub struct StaticFunctionMatch {
241    pub function: Arc<SQLUserFunction>,
242    pub invocation: Box<RoutineInvocationBinding>,
243    pub argument_types: Vec<String>,
244    pub raw_exact_matches: usize,
245    pub exact_matches: usize,
246    pub preferred_matches: usize,
247    pub variadic_expansion: bool,
248    pub default_error: Option<crate::type_resolution::RoutineSignatureMatchError>,
249}
250
251impl StaticFunctionMatch {
252    pub fn binding(&self) -> FunctionBinding {
253        FunctionBinding {
254            object_id: self.function.def.object_id,
255            name: self.function.def.name.clone(),
256            argument_types: routine_signature_types(&self.function.def),
257            builtin: false,
258            dispatch: None,
259            invocation: Some(self.invocation.clone()),
260            composite_field: None,
261            resolution_error: None,
262        }
263    }
264}
265
266impl RankedFunctionMatch for StaticFunctionMatch {
267    fn argument_types(&self) -> &[String] {
268        &self.argument_types
269    }
270
271    fn raw_exact_matches(&self) -> usize {
272        self.raw_exact_matches
273    }
274
275    fn exact_matches(&self) -> usize {
276        self.exact_matches
277    }
278
279    fn preferred_matches(&self) -> usize {
280        self.preferred_matches
281    }
282
283    fn is_variadic_expansion(&self) -> bool {
284        self.variadic_expansion
285    }
286}
287
288pub fn builtin_routine_support_oid(name: &str) -> Option<i64> {
289    Some(match name.strip_prefix("pg_catalog.").unwrap_or(name) {
290        "textlike_support" => 1023,
291        "texticregexeq_support" => 1024,
292        "texticlike_support" => 1025,
293        "network_subset_support" => 1173,
294        "textregexeq_support" => 1364,
295        "varchar_support" => 3097,
296        "numeric_support" => 3157,
297        _ => return None,
298    })
299}
300
301pub fn function_binding_matches(binding: &FunctionBinding, target: &FunctionBinding) -> bool {
302    if binding.builtin || target.builtin {
303        return false;
304    }
305    match (binding.object_id, target.object_id) {
306        (Some(binding), Some(target)) => binding == target,
307        (None, None) => {
308            binding.name == target.name && binding.argument_types == target.argument_types
309        }
310        _ => false,
311    }
312}
313
314pub fn routine_local_name(name: &str) -> Result<String, SQLError> {
315    uqa_core::RelationIdentity::from_legacy_name(name)
316        .map(|relation| relation.name)
317        .map_err(|error| SQLError::Internal(format!("invalid routine name `{name}`: {error}")))
318}
319
320pub fn routine_kind(def: &CreateFunction) -> &'static str {
321    if def.is_procedure {
322        "procedure"
323    } else {
324        "function"
325    }
326}
327
328pub mod anonymous_block;
329pub mod call;
330pub mod invocation;