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