Skip to main content

uqa_sql/routines/lifecycle/
binding.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! DROP and ALTER routine target binding and ownership validation.
8
9use super::{
10    alter_routine_kind_matches, alter_routine_kind_name, ambiguous_routine_error,
11    names::{routine_lookup_keys, RoutineNameCatalog},
12    require_routine_ownership, routine_signature_display, wrong_routine_kind_error,
13    RoutineDropResolution, RoutineDropTarget,
14};
15use crate::catalog::roles::identity::RoleSubject;
16use crate::{
17    ast::{AlterRoutineKind, DropFunctionItem, DropFunctionStmt},
18    catalog::roles::{role_inherits, RoleDefinition, RoleMembership, RoleMembershipKey},
19    routines::{routine_signature_types, SQLUserFunction},
20    SQLError,
21};
22use std::{
23    collections::{BTreeMap, BTreeSet},
24    sync::Arc,
25};
26
27/// Find the routines a DROP names, item by item as `RemoveObjects` does: each item's routine is looked up and then passed to `ensure_owner`, before the next item is looked up.
28pub fn resolve_sql_function_drop_targets(
29    catalog: &dyn RoutineNameCatalog,
30    types: &dyn crate::routines::declaration::RoutineTypeCatalog,
31    stmt: &DropFunctionStmt,
32    registry: &BTreeMap<String, Vec<Arc<SQLUserFunction>>>,
33    kind: &'static str,
34    mut ensure_owner: impl FnMut(&SQLUserFunction, &DropFunctionItem) -> Result<(), SQLError>,
35) -> Result<RoutineDropResolution, SQLError> {
36    let mut resolution = RoutineDropResolution {
37        targets: Vec::new(),
38        seen_targets: BTreeSet::new(),
39        notices: Vec::new(),
40    };
41    for item in &stmt.items {
42        // Argument types name catalog types, as `LookupFuncWithArgs` resolves them; a missing type skips the item under IF EXISTS.
43        let requested_types = match crate::routines::declaration::resolve_routine_identity_types(
44            types,
45            item.arg_types.as_deref(),
46            &[],
47            "DROP routine",
48        ) {
49            Ok(types) => types,
50            Err(error) if stmt.if_exists && error.sqlstate() == Some("42704") => {
51                resolution
52                    .notices
53                    .push(crate::SQLNotice::notice(format!("{error}, skipping")));
54                continue;
55            }
56            Err(error) => return Err(error),
57        };
58        let target = resolve_sql_function_drop_target(
59            catalog,
60            registry,
61            item,
62            requested_types.as_deref(),
63            stmt.is_procedure,
64            kind,
65        )?;
66        if let Some((key, position)) = target {
67            let function = &registry[&key][position];
68            ensure_owner(function, item)?;
69            let target = RoutineDropTarget {
70                object_id: function.def.object_id,
71                name: key,
72                argument_types: routine_signature_types(&function.def),
73                is_procedure: function.def.is_procedure,
74            };
75            if resolution.seen_targets.insert(target.clone()) {
76                resolution.targets.push(target);
77            }
78        } else {
79            let spelled = match &item.arg_types {
80                Some(types) => format!("{}({})", item.name, types.join(", ")),
81                None => format!("{}()", item.name),
82            };
83            if stmt.if_exists {
84                resolution.notices.push(crate::SQLNotice::notice(format!(
85                    "{kind} {spelled} does not exist, skipping"
86                )));
87                continue;
88            }
89            // The notice echoes the argument types as written; the error spells the resolved types as `format_type_be` does.
90            let described = match requested_types.as_deref() {
91                Some(types) => format!(
92                    "{kind} {} does not exist",
93                    routine_signature_display(catalog, &item.name, types)
94                ),
95                None => format!("could not find a {kind} named \"{}\"", item.name),
96            };
97            return Err(SQLError::Routine {
98                sqlstate: "42883".into(),
99                message: described,
100            });
101        }
102    }
103    Ok(resolution)
104}
105
106/// Find the routine a DROP item names; `requested_types` are its argument types resolved to catalog type names.
107pub fn resolve_sql_function_drop_target(
108    catalog: &dyn RoutineNameCatalog,
109    registry: &BTreeMap<String, Vec<Arc<SQLUserFunction>>>,
110    item: &DropFunctionItem,
111    requested_types: Option<&[String]>,
112    is_procedure: bool,
113    expected_kind: &str,
114) -> Result<Option<(String, usize)>, SQLError> {
115    let keys = routine_lookup_keys(catalog, &item.name)?;
116    // An argument list selects a routine of any kind, which then must be of the command's kind.
117    if let Some(types) = requested_types {
118        for key in keys {
119            let Some(overloads) = registry.get(&key) else {
120                continue;
121            };
122            let Some((position, function)) = overloads
123                .iter()
124                .enumerate()
125                .find(|(_, function)| routine_signature_types(&function.def) == *types)
126            else {
127                continue;
128            };
129            if function.def.is_procedure != is_procedure {
130                return Err(wrong_routine_kind_error(
131                    &routine_signature_display(catalog, &item.name, types),
132                    expected_kind,
133                ));
134            }
135            return Ok(Some((key, position)));
136        }
137        return Ok(None);
138    }
139    // A bare name considers only routines of the command's kind, after search-path shadowing by declared identity.
140    let mut visible_signatures = BTreeSet::new();
141    let mut candidates = Vec::new();
142    for key in keys {
143        let Some(overloads) = registry.get(&key) else {
144            continue;
145        };
146        for (position, function) in overloads.iter().enumerate() {
147            if visible_signatures.insert(routine_signature_types(&function.def))
148                && function.def.is_procedure == is_procedure
149            {
150                candidates.push((key.clone(), position));
151            }
152        }
153    }
154    match candidates.as_slice() {
155        [] => Ok(None),
156        [(key, position)] => Ok(Some((key.clone(), *position))),
157        _ => Err(ambiguous_routine_error(expected_kind, &item.name)),
158    }
159}
160
161pub fn resolve_sql_routine_alter_target(
162    catalog: &dyn RoutineNameCatalog,
163    registry: &BTreeMap<String, Vec<Arc<SQLUserFunction>>>,
164    requested_name: &str,
165    requested_types: Option<&[String]>,
166    kind: AlterRoutineKind,
167) -> Result<(String, usize), SQLError> {
168    let kind_name = alter_routine_kind_name(kind);
169    let keys = routine_lookup_keys(catalog, requested_name)?;
170    if let Some(types) = requested_types {
171        for key in keys {
172            let Some(overloads) = registry.get(&key) else {
173                continue;
174            };
175            let Some((position, function)) = overloads
176                .iter()
177                .enumerate()
178                .find(|(_, function)| routine_signature_types(&function.def) == types)
179            else {
180                continue;
181            };
182            if !alter_routine_kind_matches(kind, &function.def) {
183                return Err(wrong_routine_kind_error(
184                    &routine_signature_display(catalog, requested_name, types),
185                    kind_name,
186                ));
187            }
188            return Ok((key, position));
189        }
190        return Err(SQLError::Routine {
191            sqlstate: "42883".into(),
192            message: format!(
193                "{} {} does not exist",
194                missing_routine_kind(kind),
195                routine_signature_display(catalog, requested_name, types)
196            ),
197        });
198    }
199
200    // PostgreSQL applies search-path shadowing by declared identity before filtering FUNCTION versus PROCEDURE. Thus an earlier procedure can hide a same-signature function in a later schema, while a distinct later function remains visible.
201    let mut visible_signatures = std::collections::BTreeSet::new();
202    let mut candidates = Vec::new();
203    for key in keys {
204        let Some(overloads) = registry.get(&key) else {
205            continue;
206        };
207        for (position, function) in overloads.iter().enumerate() {
208            let signature = routine_signature_types(&function.def);
209            if visible_signatures.insert(signature)
210                && alter_routine_kind_matches(kind, &function.def)
211            {
212                candidates.push((key.clone(), position));
213            }
214        }
215    }
216    match candidates.as_slice() {
217        [(name, position)] => Ok((name.clone(), *position)),
218        [] => Err(SQLError::Routine {
219            sqlstate: "42883".into(),
220            message: format!(
221                "could not find a {} named \"{requested_name}\"",
222                missing_routine_kind(kind)
223            ),
224        }),
225        _ => Err(ambiguous_routine_error(kind_name, requested_name)),
226    }
227}
228
229/// `LookupFuncWithArgs` reports a missing `ROUTINE` as a missing function.
230const fn missing_routine_kind(kind: AlterRoutineKind) -> &'static str {
231    match kind {
232        AlterRoutineKind::Procedure => "procedure",
233        AlterRoutineKind::Function | AlterRoutineKind::Routine => "function",
234    }
235}
236
237/// `RemoveObjects`' ownership check of a routine a DROP names: the owner of the routine's schema may drop it, and otherwise only its owner, reported by the command's kind and the name as written.
238pub fn ensure_routine_drop_owner(
239    catalog: &dyn RoutineNameCatalog,
240    function: &SQLUserFunction,
241    written: &str,
242    kind: &str,
243    current_user: &(impl RoleSubject + ?Sized),
244    roles: &BTreeMap<String, RoleDefinition>,
245    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
246) -> Result<(), SQLError> {
247    let schema = uqa_core::RelationIdentity::from_legacy_name(&function.def.name)
248        .map_err(|error| SQLError::Internal(format!("resolve dropped routine schema: {error}")))?
249        .schema;
250    if catalog.schema_security(&schema).is_some_and(|security| {
251        role_inherits(roles, memberships, current_user, &security.role_owner)
252    }) {
253        return Ok(());
254    }
255    require_routine_ownership(
256        kind,
257        written,
258        role_inherits(
259            roles,
260            memberships,
261            current_user,
262            &crate::routines::security::bound_routine_owner(&function.def)?,
263        ),
264    )
265}