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