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, ensure_routine_owner_as,
11    names::{routine_lookup_keys, RoutineNameCatalog},
12    routine_signature_label, wrong_routine_kind_error, RoutineDropResolution, RoutineDropTarget,
13};
14use crate::catalog::roles::identity::RoleSubject;
15use crate::{
16    ast::{AlterRoutineKind, DropFunctionItem, DropFunctionStmt},
17    catalog::roles::{role_inherits, RoleDefinition, RoleMembership, RoleMembershipKey},
18    routines::{routine_signature_types, SQLUserFunction},
19    type_resolution::canonical_routine_type_name,
20    SQLError,
21};
22use std::{
23    collections::{BTreeMap, BTreeSet},
24    sync::Arc,
25};
26
27pub fn resolve_sql_function_drop_targets(
28    catalog: &dyn RoutineNameCatalog,
29    stmt: &DropFunctionStmt,
30    registry: &BTreeMap<String, Vec<Arc<SQLUserFunction>>>,
31    kind: &'static str,
32) -> Result<RoutineDropResolution, SQLError> {
33    let mut resolution = RoutineDropResolution {
34        targets: Vec::new(),
35        seen_targets: BTreeSet::new(),
36        notices: Vec::new(),
37    };
38    for item in &stmt.items {
39        let target =
40            resolve_sql_function_drop_target(catalog, registry, item, stmt.is_procedure, kind)?;
41        if let Some((key, position)) = target {
42            let function = &registry[&key][position];
43            let target = RoutineDropTarget {
44                object_id: function.def.object_id,
45                name: key,
46                argument_types: routine_signature_types(&function.def),
47                is_procedure: function.def.is_procedure,
48            };
49            if resolution.seen_targets.insert(target.clone()) {
50                resolution.targets.push(target);
51            }
52        } else {
53            let spelled = match &item.arg_types {
54                Some(types) => format!("{}({})", item.name, types.join(", ")),
55                None => format!("{}()", item.name),
56            };
57            if stmt.if_exists {
58                resolution.notices.push((
59                    "NOTICE",
60                    format!("{kind} {spelled} does not exist, skipping"),
61                ));
62                continue;
63            }
64            let described = match &item.arg_types {
65                Some(_) => format!("{kind} {spelled} does not exist"),
66                None => format!("could not find a {kind} named \"{}\"", item.name),
67            };
68            return Err(SQLError::Routine {
69                sqlstate: "42883".into(),
70                message: described,
71            });
72        }
73    }
74    Ok(resolution)
75}
76
77pub fn resolve_sql_function_drop_target(
78    catalog: &dyn RoutineNameCatalog,
79    registry: &BTreeMap<String, Vec<Arc<SQLUserFunction>>>,
80    item: &DropFunctionItem,
81    is_procedure: bool,
82    expected_kind: &str,
83) -> Result<Option<(String, usize)>, SQLError> {
84    let requested_types = item.arg_types.as_ref().map(|types| {
85        types
86            .iter()
87            .map(|type_name| canonical_routine_type_name(type_name))
88            .collect::<Vec<_>>()
89    });
90    for key in routine_lookup_keys(catalog, &item.name)? {
91        let Some(overloads) = registry.get(&key) else {
92            continue;
93        };
94        if let Some(types) = requested_types.as_ref() {
95            let Some((position, function)) = overloads
96                .iter()
97                .enumerate()
98                .find(|(_, function)| routine_signature_types(&function.def) == *types)
99            else {
100                continue;
101            };
102            if function.def.is_procedure != is_procedure {
103                return Err(wrong_routine_kind_error(
104                    &function.def.name,
105                    types,
106                    function.def.is_procedure,
107                    expected_kind,
108                ));
109            }
110            return Ok(Some((key, position)));
111        }
112
113        let positions = overloads
114            .iter()
115            .enumerate()
116            .filter(|(_, function)| function.def.is_procedure == is_procedure)
117            .map(|(position, _)| position)
118            .collect::<Vec<_>>();
119        match positions.as_slice() {
120            [] => {
121                if let Some(function) = overloads.first() {
122                    return Err(wrong_routine_kind_error(
123                        &function.def.name,
124                        &routine_signature_types(&function.def),
125                        function.def.is_procedure,
126                        expected_kind,
127                    ));
128                }
129            }
130            [position] => return Ok(Some((key, *position))),
131            _ => {
132                return Err(SQLError::Routine {
133                    sqlstate: "42725".into(),
134                    message: format!("{expected_kind} name \"{}\" is not unique", item.name),
135                });
136            }
137        }
138    }
139    Ok(None)
140}
141
142pub fn resolve_sql_routine_alter_target(
143    catalog: &dyn RoutineNameCatalog,
144    registry: &BTreeMap<String, Vec<Arc<SQLUserFunction>>>,
145    requested_name: &str,
146    requested_types: Option<&[String]>,
147    kind: AlterRoutineKind,
148) -> Result<(String, usize), SQLError> {
149    let kind_name = alter_routine_kind_name(kind);
150    let keys = routine_lookup_keys(catalog, requested_name)?;
151    if let Some(types) = requested_types {
152        for key in keys {
153            let Some(overloads) = registry.get(&key) else {
154                continue;
155            };
156            let Some((position, function)) = overloads
157                .iter()
158                .enumerate()
159                .find(|(_, function)| routine_signature_types(&function.def) == types)
160            else {
161                continue;
162            };
163            if !alter_routine_kind_matches(kind, &function.def) {
164                return Err(wrong_routine_kind_error(
165                    &function.def.name,
166                    types,
167                    function.def.is_procedure,
168                    kind_name,
169                ));
170            }
171            return Ok((key, position));
172        }
173        return Err(SQLError::Routine {
174            sqlstate: "42883".into(),
175            message: format!(
176                "{kind_name} {} does not exist",
177                routine_signature_label(requested_name, types)
178            ),
179        });
180    }
181
182    // 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.
183    let mut visible_signatures = std::collections::BTreeSet::new();
184    let mut candidates = Vec::new();
185    for key in keys {
186        let Some(overloads) = registry.get(&key) else {
187            continue;
188        };
189        for (position, function) in overloads.iter().enumerate() {
190            let signature = routine_signature_types(&function.def);
191            if visible_signatures.insert(signature)
192                && alter_routine_kind_matches(kind, &function.def)
193            {
194                candidates.push((key.clone(), position));
195            }
196        }
197    }
198    match candidates.as_slice() {
199        [(name, position)] => Ok((name.clone(), *position)),
200        [] => Err(SQLError::Routine {
201            sqlstate: "42883".into(),
202            message: format!("could not find a {kind_name} named \"{requested_name}\""),
203        }),
204        _ => Err(SQLError::Routine {
205            sqlstate: "42725".into(),
206            message: format!("{kind_name} name \"{requested_name}\" is not unique"),
207        }),
208    }
209}
210
211pub fn ensure_routine_drop_owners(
212    registry: &BTreeMap<String, Vec<Arc<SQLUserFunction>>>,
213    targets: &[RoutineDropTarget],
214    current_user: &(impl RoleSubject + ?Sized),
215    roles: &BTreeMap<String, RoleDefinition>,
216    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
217) -> Result<(), SQLError> {
218    for target in targets {
219        let definition = registry
220            .get(&target.name)
221            .and_then(|overloads| {
222                overloads.iter().find(|function| {
223                    function.def.is_procedure == target.is_procedure
224                        && routine_signature_types(&function.def) == target.argument_types
225                })
226            })
227            .map(|function| &function.def)
228            .ok_or_else(|| {
229                SQLError::Internal(format!(
230                    "resolved {} {} disappeared before ownership validation",
231                    target.kind(),
232                    target.label()
233                ))
234            })?;
235        ensure_routine_owner_as(
236            definition,
237            role_inherits(
238                roles,
239                memberships,
240                current_user,
241                &crate::routines::security::bound_routine_owner(definition)?,
242            ),
243        )?;
244    }
245    Ok(())
246}