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 name binding, namespace privileges, and owner diagnostics.
8
9use crate::catalog::roles::RoleReference;
10use crate::{
11    catalog::{domain::DomainCatalog, roles::RoleReferenceNames, security::BoundSchemaSecurity},
12    SQLError,
13};
14
15pub trait DomainDropCatalog: DomainCatalog {
16    fn schema_security(&self, name: &str) -> Option<BoundSchemaSecurity>;
17    fn resolve_domain_drop_type(&self, name: &str) -> Result<Option<i64>, SQLError>;
18    fn format_domain_drop_type(&self, oid: i64) -> Result<Option<String>, String>;
19}
20pub trait DomainDropAuthority {
21    fn schema_usage(&self, schema: &str, role: &RoleReference) -> bool;
22    fn current_user_has_role_privileges(
23        &self,
24        role: &dyn crate::catalog::roles::identity::RoleSubject,
25    ) -> bool;
26}
27pub struct DomainDropBinding<'a> {
28    pub catalog: &'a dyn DomainDropCatalog,
29    pub authority: &'a dyn DomainDropAuthority,
30    pub session: &'a dyn RoleReferenceNames,
31}
32pub enum BoundDomainDrop {
33    Target(u32),
34    Skipped(String),
35}
36
37pub fn resolve_drop_domain(
38    context: &DomainDropBinding<'_>,
39    name: &str,
40    if_exists: bool,
41) -> Result<BoundDomainDrop, SQLError> {
42    let parsed = crate::parse_regtype_name(name)?
43        .ok_or_else(|| SQLError::Internal("DROP DOMAIN has no type name".into()))?;
44    let label = format!(
45        "{}{}",
46        parsed.names.join("."),
47        "[]".repeat(parsed.array_dimensions)
48    );
49    if let [schema, _] = parsed.names.as_slice() {
50        if context.catalog.schema_security(schema).is_none() {
51            if if_exists {
52                return Ok(BoundDomainDrop::Skipped(format!(
53                    "schema \"{schema}\" does not exist, skipping"
54                )));
55            }
56            return Err(SQLError::Routine {
57                sqlstate: "3F000".into(),
58                message: format!("schema \"{schema}\" does not exist"),
59            });
60        }
61        if !context
62            .authority
63            .schema_usage(schema, &context.session.current_role())
64        {
65            return Err(SQLError::Routine {
66                sqlstate: "42501".into(),
67                message: format!("permission denied for schema {schema}"),
68            });
69        }
70    }
71    let oid = context.catalog.resolve_domain_drop_type(name)?;
72    let Some(oid) = oid else {
73        if if_exists {
74            return Ok(BoundDomainDrop::Skipped(format!(
75                "type \"{label}\" does not exist, skipping"
76            )));
77        }
78        return Err(SQLError::Routine {
79            sqlstate: "42704".into(),
80            message: format!("type \"{label}\" does not exist"),
81        });
82    };
83    let domain = u32::try_from(oid)
84        .ok()
85        .and_then(|oid| context.catalog.domain_by_oid(oid))
86        .ok_or_else(|| SQLError::Routine {
87            sqlstate: "42809".into(),
88            message: format!("\"{label}\" is not a domain"),
89        })?;
90    let owns_schema = context
91        .catalog
92        .schema_security(&domain.identity.schema)
93        .is_some_and(|security| {
94            context
95                .authority
96                .current_user_has_role_privileges(&security.role_owner)
97        });
98    if !owns_schema
99        && !context
100            .authority
101            .current_user_has_role_privileges(&domain.owner)
102    {
103        let label = context
104            .catalog
105            .format_domain_drop_type(oid)
106            .map_err(SQLError::Internal)?
107            .ok_or_else(|| SQLError::Internal("DROP DOMAIN target disappeared".into()))?;
108        return Err(SQLError::Routine {
109            sqlstate: "42501".into(),
110            message: format!("must be owner of type {label}"),
111        });
112    }
113    Ok(BoundDomainDrop::Target(domain.oid))
114}