Skip to main content

uqa_sql/routines/
registration.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Routine replacement compatibility, security attributes, and ALTER definition analysis.
8
9use super::{builtin_routine_support_oid, lifecycle::ensure_routine_owner_as, routine_kind};
10use crate::catalog::roles::identity::RoleSubject;
11use crate::{
12    ast::{AlterRoutineStmt, CreateFunction},
13    catalog::roles::{role_inherits, RoleDefinition, RoleMembership, RoleMembershipKey},
14    type_resolution::canonical_routine_type_name,
15    SQLError,
16};
17use std::collections::BTreeMap;
18
19pub trait RoutineSupportAuthority {
20    fn current_user_is_superuser(&self) -> bool;
21}
22pub fn validate_routine_support(
23    authority: &dyn RoutineSupportAuthority,
24    support: &str,
25) -> Result<(), SQLError> {
26    if builtin_routine_support_oid(support).is_none() {
27        return Err(SQLError::Routine {
28            sqlstate: "42883".into(),
29            message: format!("function {support}(internal) does not exist"),
30        });
31    }
32    if !authority.current_user_is_superuser() {
33        return Err(SQLError::Routine {
34            sqlstate: "42501".into(),
35            message: "must be superuser to specify a support function".into(),
36        });
37    }
38    Ok(())
39}
40
41pub fn validate_routine_security_attributes(
42    def: &CreateFunction,
43    current_user_is_superuser: bool,
44) -> Result<(), SQLError> {
45    if (def.security.leakproof || def.support.is_some()) && !current_user_is_superuser {
46        return Err(SQLError::Routine {
47            sqlstate: "42501".into(),
48            message: if def.security.leakproof {
49                "only superuser can define a leakproof function".into()
50            } else {
51                "must be superuser to specify a support function".into()
52            },
53        });
54    }
55    Ok(())
56}
57
58pub fn prepare_routine_replacement(
59    existing: &CreateFunction,
60    def: &mut CreateFunction,
61    requested_name: &str,
62    current_user: &(impl RoleSubject + ?Sized),
63    roles: &BTreeMap<String, RoleDefinition>,
64    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
65) -> Result<(), SQLError> {
66    if !def.or_replace {
67        let kind = routine_kind(def);
68        return Err(SQLError::Routine {
69            sqlstate: "42723".into(),
70            message: format!("{kind} \"{requested_name}\" already exists with same argument types"),
71        });
72    }
73    ensure_routine_owner_as(
74        existing,
75        role_inherits(
76            roles,
77            memberships,
78            current_user,
79            &crate::routines::security::bound_routine_owner(existing)?,
80        ),
81    )?;
82    if existing.is_procedure != def.is_procedure {
83        return Err(SQLError::Routine {
84            sqlstate: "42809".into(),
85            message: "cannot change routine kind".into(),
86        });
87    }
88    if !same_return_shape(existing, def) {
89        return Err(SQLError::Routine {
90            sqlstate: "42P13".into(),
91            message: "cannot change return type of existing function".into(),
92        });
93    }
94    // CREATE OR REPLACE changes the definition but not object ownership or privileges.
95    def.object_id = Some(existing.object_id.ok_or_else(|| {
96        SQLError::Internal(format!(
97            "existing routine `{}` has no catalog object identity",
98            existing.name,
99        ))
100    })?);
101    def.owner = existing.owner;
102    def.execute_acl.clone_from(&existing.execute_acl);
103    Ok(())
104}
105
106pub fn alter_routine_attributes(
107    existing: &CreateFunction,
108    stmt: &AlterRoutineStmt,
109    current_user_is_superuser: bool,
110    authority: &dyn RoutineSupportAuthority,
111) -> Result<CreateFunction, SQLError> {
112    if existing.is_procedure
113        && (stmt.volatility.is_some()
114            || stmt.strict.is_some()
115            || stmt.leakproof.is_some()
116            || stmt.parallel.is_some()
117            || stmt.support.is_some())
118    {
119        return Err(SQLError::Routine {
120            sqlstate: "42P13".into(),
121            message: "invalid attribute in procedure definition".into(),
122        });
123    }
124
125    let mut def = existing.clone();
126    if let Some(volatility) = stmt.volatility {
127        def.volatility = volatility;
128    }
129    if let Some(strict) = stmt.strict {
130        def.strict = strict;
131    }
132    if let Some(security_definer) = stmt.security_definer {
133        def.security.security_definer = security_definer;
134    }
135    if let Some(leakproof) = stmt.leakproof {
136        if leakproof && !current_user_is_superuser {
137            return Err(SQLError::Routine {
138                sqlstate: "42501".into(),
139                message: "only superuser can define a leakproof function".into(),
140            });
141        }
142        def.security.leakproof = leakproof;
143    }
144    if let Some(parallel) = stmt.parallel {
145        def.parallel = parallel;
146    }
147    if let Some(support) = &stmt.support {
148        validate_routine_support(authority, support)?;
149        def.support = Some(support.clone());
150    }
151    def.config_actions.clone_from(&stmt.config_actions);
152    Ok(def)
153}
154
155fn same_return_shape(a: &CreateFunction, b: &CreateFunction) -> bool {
156    use crate::ast::FunctionReturns;
157    let same_outputs = {
158        let a_outs = a.output_params();
159        let b_outs = b.output_params();
160        a_outs.len() == b_outs.len()
161            && a_outs.iter().zip(&b_outs).all(|(x, y)| {
162                x.name == y.name
163                    && canonical_routine_type_name(&x.type_name)
164                        == canonical_routine_type_name(&y.type_name)
165                    && x.mode == y.mode
166            })
167    };
168    let same_kind = match (&a.returns, &b.returns) {
169        (FunctionReturns::None, FunctionReturns::None)
170        | (FunctionReturns::Table, FunctionReturns::Table) => true,
171        (FunctionReturns::Scalar { type_name: x }, FunctionReturns::Scalar { type_name: y })
172        | (FunctionReturns::SetOf { type_name: x }, FunctionReturns::SetOf { type_name: y }) => {
173            canonical_routine_type_name(x) == canonical_routine_type_name(y)
174        }
175        _ => false,
176    };
177    same_kind && same_outputs
178}