Skip to main content

uqa_sql/catalog/roles/
dependencies.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Role ownership and ACL dependency diagnostics in catalog and requested-role order.
8
9use crate::{
10    catalog::{
11        security::{BoundSchemaSecurity, BoundSequenceSecurity},
12        view::StoredViewKind,
13    },
14    SQLError,
15};
16use std::collections::BTreeMap;
17use uqa_core::RelationIdentity;
18pub mod context;
19pub mod temporary;
20use context::RoleDependencyCatalog;
21
22pub fn ensure_roles_have_no_object_dependencies(
23    catalog: &dyn RoleDependencyCatalog,
24    names: &[String],
25    roles: &BTreeMap<String, super::RoleDefinition>,
26) -> Result<(), SQLError> {
27    let database_security = catalog.database();
28    for name in names {
29        if roles
30            .get(name)
31            .is_some_and(|role| database_security.depends_on(role.identity()))
32        {
33            return Err(SQLError::Routine {
34                sqlstate: "2BP01".into(),
35                message: format!(
36                    "role \"{name}\" cannot be dropped because some objects depend on it: database uqa"
37                ),
38            });
39        }
40    }
41    drop(database_security);
42
43    let schema_security = catalog.schemas();
44    for name in names {
45        if let Some(schema) = roles
46            .get(name)
47            .and_then(|role| dependent_schema_for_role(&schema_security, role.identity()))
48        {
49            return Err(SQLError::Routine {
50                sqlstate: "2BP01".into(),
51                message: format!(
52                    "role \"{name}\" cannot be dropped because some objects depend on it: schema {schema}"
53                ),
54            });
55        }
56    }
57    drop(schema_security);
58
59    ensure_roles_have_no_table_dependencies(catalog, names, roles)?;
60
61    ensure_roles_have_no_system_relation_dependencies(catalog, names, roles)?;
62
63    let views = catalog.views();
64    for name in names {
65        if let Some((relation, view)) = views.iter().find(|(_, view)| {
66            roles
67                .get(name)
68                .is_some_and(|role| view.security.depends_on(role.identity()))
69        }) {
70            let kind = match view.kind {
71                StoredViewKind::View => "view",
72                StoredViewKind::Materialized => "materialized view",
73            };
74            return Err(SQLError::Routine {
75                sqlstate: "2BP01".into(),
76                message: format!(
77                    "role \"{name}\" cannot be dropped because some objects depend on it: {kind} {}",
78                    relation.qualified_name()
79                ),
80            });
81        }
82    }
83    drop(views);
84
85    ensure_roles_have_no_foreign_table_dependencies(catalog, names, roles)?;
86
87    let sequence_security = catalog.sequences();
88    for name in names {
89        if let Some(relation) = roles
90            .get(name)
91            .and_then(|role| dependent_sequence_for_role(&sequence_security, role.identity()))
92        {
93            return Err(SQLError::Routine {
94                sqlstate: "2BP01".into(),
95                message: format!(
96                    "role \"{name}\" cannot be dropped because some objects depend on it: sequence {}",
97                    relation.qualified_name()
98                ),
99            });
100        }
101    }
102    drop(sequence_security);
103
104    ensure_roles_have_no_domain_dependencies(catalog, names, roles)?;
105
106    let routines = catalog.routines();
107    for name in names {
108        if let Some(dependent) = routines.values().flatten().find_map(|function| {
109            let identity = roles.get(name)?.identity();
110            let owns = function.def.owner == Some(identity);
111            let has_acl = function.def.execute_acl.as_ref().is_some_and(|acl| {
112                acl.iter()
113                    .any(|entry| entry.role == Some(identity) || entry.grantor == identity)
114            });
115            (owns || has_acl).then(|| format!("routine {}", function.def.name))
116        }) {
117            return Err(SQLError::Routine {
118                sqlstate: "2BP01".into(),
119                message: format!("role \"{name}\" cannot be dropped because some objects depend on it: {dependent}"),
120            });
121        }
122    }
123    Ok(())
124}
125
126fn ensure_roles_have_no_domain_dependencies(
127    catalog: &dyn RoleDependencyCatalog,
128    names: &[String],
129    roles: &BTreeMap<String, super::RoleDefinition>,
130) -> Result<(), SQLError> {
131    let domains = catalog.domains();
132    for name in names {
133        if let Some(domain) = domains.values().find(|domain| {
134            roles
135                .get(name)
136                .is_some_and(|role| domain.owner == role.identity())
137        }) {
138            return Err(SQLError::Routine {
139                sqlstate: "2BP01".into(),
140                message: format!(
141                    "role \"{name}\" cannot be dropped because some objects depend on it: type {}",
142                    domain.identity.qualified_name()
143                ),
144            });
145        }
146    }
147    drop(domains);
148    Ok(())
149}
150
151fn ensure_roles_have_no_table_dependencies(
152    catalog: &dyn RoleDependencyCatalog,
153    names: &[String],
154    roles: &BTreeMap<String, super::RoleDefinition>,
155) -> Result<(), SQLError> {
156    let tables = catalog.tables();
157    for name in names {
158        if let Some(relation) = tables.iter().find_map(|(relation, table)| {
159            roles
160                .get(name)
161                .is_some_and(|role| table.security().depends_on(role.identity()))
162                .then_some(relation)
163        }) {
164            return Err(SQLError::Routine {
165                sqlstate: "2BP01".into(),
166                message: format!(
167                    "role \"{name}\" cannot be dropped because some objects depend on it: table {}",
168                    relation.qualified_name()
169                ),
170            });
171        }
172    }
173    drop(tables);
174
175    Ok(())
176}
177
178fn ensure_roles_have_no_system_relation_dependencies(
179    catalog: &dyn RoleDependencyCatalog,
180    names: &[String],
181    roles: &BTreeMap<String, super::RoleDefinition>,
182) -> Result<(), SQLError> {
183    let system_relations = catalog.system_relation_securities();
184    for name in names {
185        if let Some((relation, _)) = system_relations.iter().find(|(identity, entry)| {
186            crate::catalog::SystemRelation::at(&identity.schema, &identity.name).is_some_and(
187                |relation| {
188                    roles
189                        .get(name)
190                        .is_some_and(|role| entry.security(relation).depends_on(role.identity()))
191                },
192            )
193        }) {
194            return Err(SQLError::Routine {
195                sqlstate: "2BP01".into(),
196                message: format!(
197                    "role \"{name}\" cannot be dropped because some objects depend on it: table {}",
198                    relation.qualified_name()
199                ),
200            });
201        }
202    }
203    drop(system_relations);
204
205    Ok(())
206}
207
208fn ensure_roles_have_no_foreign_table_dependencies(
209    catalog: &dyn RoleDependencyCatalog,
210    names: &[String],
211    roles: &BTreeMap<String, super::RoleDefinition>,
212) -> Result<(), SQLError> {
213    let foreign_tables = catalog.foreign_tables();
214    for name in names {
215        if let Some((relation, _)) = foreign_tables.iter().find(|(_, security)| {
216            roles
217                .get(name)
218                .is_some_and(|role| security.depends_on(role.identity()))
219        }) {
220            return Err(SQLError::Routine {
221                sqlstate: "2BP01".into(),
222                message: format!(
223                    "role \"{name}\" cannot be dropped because some objects depend on it: foreign table {}",
224                    relation.qualified_name()
225                ),
226            });
227        }
228    }
229    Ok(())
230}
231
232fn dependent_sequence_for_role(
233    sequences: &BTreeMap<RelationIdentity, BoundSequenceSecurity>,
234    role: super::RoleIdentity,
235) -> Option<&RelationIdentity> {
236    sequences
237        .iter()
238        .find_map(|(relation, security)| security.depends_on(role).then_some(relation))
239}
240
241fn dependent_schema_for_role(
242    schemas: &BTreeMap<String, BoundSchemaSecurity>,
243    role: super::RoleIdentity,
244) -> Option<&String> {
245    schemas
246        .iter()
247        .find_map(|(name, security)| security.depends_on(role).then_some(name))
248}
249
250#[cfg(test)]
251mod tests;