Skip to main content

uqa_sql/schema/domains/
removal.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! DROP DOMAIN, DROP TYPE and ALTER TYPE name binding, namespace privileges, owner and required-type diagnostics.
8
9use crate::catalog::roles::RoleReference;
10use crate::{
11    catalog::{domain::DomainCatalog, roles::RoleReferenceNames, security::BoundSchemaSecurity},
12    SQLError,
13};
14
15pub trait TypeObjectCatalog: DomainCatalog {
16    fn schema_security(&self, name: &str) -> Option<BoundSchemaSecurity>;
17    fn resolve_drop_type_oid(&self, name: &str) -> Result<Option<i64>, SQLError>;
18    fn format_drop_type(&self, oid: i64) -> Result<Option<String>, String>;
19    /// An enum by its own type OID or by its generated array's OID.
20    fn enum_by_type_oid(&self, oid: u32) -> Option<crate::catalog::enum_type::StoredEnum>;
21    /// A standalone composite type by its own type OID or by its generated array's OID.
22    fn composite_by_type_oid(
23        &self,
24        oid: u32,
25    ) -> Option<crate::catalog::composite_type::StoredComposite>;
26    /// The element type of a generated array of a user-defined type.
27    fn user_array_element(&self, oid: u32) -> Option<u32>;
28    /// The relation whose row type has this OID: its object kind, display name and owner.
29    fn row_type_relation(&self, oid: u32) -> Option<RowTypeRelation>;
30}
31
32/// A relation that owns a composite row type, described for dependency diagnostics.
33#[derive(Debug, Clone)]
34pub struct RowTypeRelation {
35    pub kind: &'static str,
36    /// The relation named as `regclass` output spells it.
37    pub name: String,
38    /// The relation's unqualified catalog name.
39    pub local_name: String,
40    pub owner: uqa_core::catalog_role::RoleIdentity,
41    pub schema: String,
42}
43
44#[derive(Clone, Copy, PartialEq, Eq)]
45enum TypeDropKind {
46    Domain,
47    Type,
48}
49pub trait TypeObjectAuthority {
50    fn schema_usage(&self, schema: &str, role: &RoleReference) -> bool;
51    fn current_user_has_role_privileges(
52        &self,
53        role: &dyn crate::catalog::roles::identity::RoleSubject,
54    ) -> bool;
55}
56pub struct TypeObjectBinding<'a> {
57    pub catalog: &'a dyn TypeObjectCatalog,
58    pub authority: &'a dyn TypeObjectAuthority,
59    pub session: &'a dyn RoleReferenceNames,
60}
61pub enum BoundTypeDrop {
62    Target(u32),
63    Skipped(String),
64}
65
66pub fn resolve_drop_domain(
67    context: &TypeObjectBinding<'_>,
68    name: &str,
69    if_exists: bool,
70) -> Result<BoundTypeDrop, SQLError> {
71    resolve_type_drop(context, name, if_exists, TypeDropKind::Domain)
72}
73
74/// `DROP TYPE` removes domains, enums and composite types. Built-in, generated array and relation row types are required by other objects and fail after the owner check, as in `PostgreSQL`.
75pub fn resolve_drop_type(
76    context: &TypeObjectBinding<'_>,
77    name: &str,
78    if_exists: bool,
79) -> Result<BoundTypeDrop, SQLError> {
80    resolve_type_drop(context, name, if_exists, TypeDropKind::Type)
81}
82
83fn resolve_type_drop(
84    context: &TypeObjectBinding<'_>,
85    name: &str,
86    if_exists: bool,
87    kind: TypeDropKind,
88) -> Result<BoundTypeDrop, SQLError> {
89    let parsed = crate::parse_regtype_name(name)?
90        .ok_or_else(|| SQLError::Internal("DROP has no type name".into()))?;
91    let label = format!(
92        "{}{}",
93        parsed.names.join("."),
94        "[]".repeat(parsed.array_dimensions)
95    );
96    if let [schema, _] = parsed.names.as_slice() {
97        if context.catalog.schema_security(schema).is_none() {
98            if if_exists {
99                return Ok(BoundTypeDrop::Skipped(format!(
100                    "schema \"{schema}\" does not exist, skipping"
101                )));
102            }
103            return Err(SQLError::Routine {
104                sqlstate: "3F000".into(),
105                message: format!("schema \"{schema}\" does not exist"),
106            });
107        }
108        if !context
109            .authority
110            .schema_usage(schema, &context.session.current_role())
111        {
112            return Err(SQLError::Routine {
113                sqlstate: "42501".into(),
114                message: format!("permission denied for schema {schema}"),
115            });
116        }
117    }
118    let oid = context.catalog.resolve_drop_type_oid(name)?;
119    let Some(oid) = oid else {
120        if if_exists {
121            return Ok(BoundTypeDrop::Skipped(format!(
122                "type \"{label}\" does not exist, skipping"
123            )));
124        }
125        return Err(SQLError::Routine {
126            sqlstate: "42704".into(),
127            message: format!("type \"{label}\" does not exist"),
128        });
129    };
130    let type_oid = u32::try_from(oid)
131        .map_err(|_| SQLError::Internal(format!("DROP type OID {oid} is out of range")))?;
132    let target = drop_target(context, type_oid, &label, kind)?;
133    let owns_schema = target.schema.as_ref().is_some_and(|schema| {
134        context
135            .catalog
136            .schema_security(schema)
137            .is_some_and(|security| {
138                context
139                    .authority
140                    .current_user_has_role_privileges(&security.role_owner)
141            })
142    });
143    if !owns_schema
144        && !context
145            .authority
146            .current_user_has_role_privileges(&target.owner)
147    {
148        return Err(SQLError::Routine {
149            sqlstate: "42501".into(),
150            message: format!("must be owner of type {}", format_type(context, oid)?),
151        });
152    }
153    match target.required_by {
154        None => Ok(BoundTypeDrop::Target(type_oid)),
155        Some(Requirement::System) => Err(SQLError::Routine {
156            sqlstate: "2BP01".into(),
157            message: format!(
158                "cannot drop type {} because it is required by the database system",
159                format_type(context, oid)?
160            ),
161        }),
162        Some(Requirement::Object(object)) => Err(SQLError::Diagnostic {
163            sqlstate: "2BP01".into(),
164            message: format!(
165                "cannot drop type {} because {object} requires it",
166                format_type(context, oid)?
167            ),
168            detail: None,
169            hint: Some(format!("You can drop {object} instead.")),
170        }),
171    }
172}
173
174enum Requirement {
175    System,
176    Object(String),
177}
178
179struct DropTarget {
180    schema: Option<String>,
181    owner: uqa_core::catalog_role::RoleIdentity,
182    required_by: Option<Requirement>,
183}
184
185fn drop_target(
186    context: &TypeObjectBinding<'_>,
187    oid: u32,
188    label: &str,
189    kind: TypeDropKind,
190) -> Result<DropTarget, SQLError> {
191    if let Some(domain) = context.catalog.domain_by_oid(oid) {
192        return Ok(DropTarget {
193            schema: Some(domain.identity.schema),
194            owner: domain.owner,
195            required_by: None,
196        });
197    }
198    if kind == TypeDropKind::Domain {
199        return Err(SQLError::Routine {
200            sqlstate: "42809".into(),
201            message: format!("\"{label}\" is not a domain"),
202        });
203    }
204    if let Some(definition) = context.catalog.enum_by_type_oid(oid) {
205        return Ok(DropTarget {
206            schema: Some(definition.identity.schema.clone()),
207            owner: definition.owner,
208            required_by: (definition.array_oid == oid)
209                .then(|| {
210                    format_type(context, i64::from(definition.oid))
211                        .map(|element| Requirement::Object(format!("type {element}")))
212                })
213                .transpose()?,
214        });
215    }
216    if let Some(definition) = context.catalog.composite_by_type_oid(oid) {
217        return Ok(DropTarget {
218            schema: Some(definition.identity.schema.clone()),
219            owner: definition.owner,
220            required_by: (definition.array_oid == oid)
221                .then(|| {
222                    format_type(context, i64::from(definition.oid))
223                        .map(|element| Requirement::Object(format!("type {element}")))
224                })
225                .transpose()?,
226        });
227    }
228    if let Some(element) = context.catalog.user_array_element(oid) {
229        let domain = context
230            .catalog
231            .domain_by_oid(element)
232            .ok_or_else(|| SQLError::Internal(format!("array type {oid} has no element type")))?;
233        return Ok(DropTarget {
234            schema: Some(domain.identity.schema),
235            owner: domain.owner,
236            required_by: Some(Requirement::Object(format!(
237                "type {}",
238                format_type(context, i64::from(element))?
239            ))),
240        });
241    }
242    if let Some(relation) = context.catalog.row_type_relation(oid) {
243        return Ok(DropTarget {
244            schema: Some(relation.schema),
245            owner: relation.owner,
246            required_by: Some(Requirement::Object(format!(
247                "{} {}",
248                relation.kind, relation.name
249            ))),
250        });
251    }
252    Ok(DropTarget {
253        schema: None,
254        owner: uqa_core::catalog_role::RoleIdentity::BOOTSTRAP,
255        required_by: Some(Requirement::System),
256    })
257}
258
259fn format_type(context: &TypeObjectBinding<'_>, oid: i64) -> Result<String, SQLError> {
260    context
261        .catalog
262        .format_drop_type(oid)
263        .map_err(SQLError::Internal)?
264        .ok_or_else(|| SQLError::Internal("DROP type target disappeared".into()))
265}
266
267/// Resolve the enum named by `ALTER TYPE ... ADD VALUE | RENAME VALUE` with `PostgreSQL`'s `checkEnumOwner` order: missing type, then a non-enum type, then ownership.
268pub fn resolve_alter_enum(
269    context: &TypeObjectBinding<'_>,
270    name: &str,
271) -> Result<crate::catalog::enum_type::StoredEnum, SQLError> {
272    let parsed = crate::parse_regtype_name(name)?
273        .ok_or_else(|| SQLError::Internal("ALTER TYPE has no type name".into()))?;
274    let label = format!(
275        "{}{}",
276        parsed.names.join("."),
277        "[]".repeat(parsed.array_dimensions)
278    );
279    let oid = context
280        .catalog
281        .resolve_drop_type_oid(name)?
282        .ok_or_else(|| SQLError::Routine {
283            sqlstate: "42704".into(),
284            message: format!("type \"{label}\" does not exist"),
285        })?;
286    let definition = u32::try_from(oid)
287        .ok()
288        .and_then(|oid| context.catalog.enum_by_type_oid(oid))
289        .filter(|definition| i64::from(definition.oid) == oid)
290        .ok_or_else(|| {
291            format_type(context, oid).map_or_else(
292                |error| error,
293                |name| SQLError::Routine {
294                    sqlstate: "42809".into(),
295                    message: format!("{name} is not an enum"),
296                },
297            )
298        })?;
299    // Unlike DROP, altering an enum requires ownership of the type itself.
300    if !context
301        .authority
302        .current_user_has_role_privileges(&definition.owner)
303    {
304        return Err(SQLError::Routine {
305            sqlstate: "42501".into(),
306            message: format!("must be owner of type {}", format_type(context, oid)?),
307        });
308    }
309    Ok(definition)
310}