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