Skip to main content

uqa_sql/schema/namespaces/
removal.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Schema DROP target authority, namespace protection, and empty-schema validation.
8
9use crate::{
10    catalog::{is_virtual_system_schema, security::BoundSchemaSecurity},
11    SQLError,
12};
13use std::collections::BTreeSet;
14
15pub trait EmptySchemaCatalog {
16    fn schema_registered(&self, name: &str) -> bool;
17    fn schema_is_empty(&self, name: &str) -> bool;
18}
19pub trait SchemaDropCatalog: EmptySchemaCatalog {
20    fn schema_security(&self, name: &str) -> Option<BoundSchemaSecurity>;
21    fn current_user_has_role_privileges(&self, role: &uqa_core::catalog_role::RoleIdentity)
22        -> bool;
23    fn schema_is_graph(&self, name: &str) -> Result<bool, String>;
24}
25pub enum BoundSchemaDrop {
26    Schema,
27    Graph,
28    Skipped(String),
29}
30
31pub fn bind_schema_drop_target(
32    catalog: &dyn SchemaDropCatalog,
33    name: &str,
34    if_exists: bool,
35) -> Result<BoundSchemaDrop, SQLError> {
36    let Some(security) = catalog.schema_security(name) else {
37        if if_exists {
38            return Ok(BoundSchemaDrop::Skipped(format!(
39                "schema \"{name}\" does not exist, skipping"
40            )));
41        }
42        return Err(SQLError::Routine {
43            sqlstate: "3F000".into(),
44            message: format!("schema \"{name}\" does not exist"),
45        });
46    };
47    if !catalog.current_user_has_role_privileges(&security.role_owner) {
48        return Err(SQLError::Routine {
49            sqlstate: "42501".into(),
50            message: format!("must be owner of schema {name}"),
51        });
52    }
53    if is_virtual_system_schema(name) {
54        return Err(SQLError::Routine {
55            sqlstate: "2BP01".into(),
56            message: format!("schema `{name}` cannot be dropped"),
57        });
58    }
59    if catalog
60        .schema_is_graph(name)
61        .map_err(|error| SQLError::Internal(format!("DROP SCHEMA: {error}")))?
62    {
63        Ok(BoundSchemaDrop::Graph)
64    } else {
65        Ok(BoundSchemaDrop::Schema)
66    }
67}
68
69pub fn validate_schema_drop_restrict(
70    catalog: &dyn SchemaDropCatalog,
71    schemas: &BTreeSet<String>,
72    graphs: &BTreeSet<String>,
73) -> Result<(), SQLError> {
74    let occupied = schemas
75        .iter()
76        .find(|name| !catalog.schema_is_empty(name))
77        .or_else(|| graphs.first());
78    if let Some(name) = occupied {
79        let single = schemas.len() + graphs.len() == 1;
80        let object = if single {
81            format!("schema {name}")
82        } else {
83            "desired object(s)".into()
84        };
85        return Err(SQLError::Routine {
86            sqlstate: "2BP01".into(),
87            message: format!(
88                "cannot drop {object} because other objects depend on {}",
89                if single { "it" } else { "them" }
90            ),
91        });
92    }
93    Ok(())
94}
95
96pub fn validate_empty_schema_drop(
97    catalog: &dyn EmptySchemaCatalog,
98    name: &str,
99) -> Result<bool, String> {
100    if is_virtual_system_schema(name) {
101        return Err(format!("schema `{name}` cannot be dropped"));
102    }
103    if !catalog.schema_registered(name) {
104        return Ok(false);
105    }
106    if !catalog.schema_is_empty(name) {
107        return Err(format!("schema `{name}` is not empty"));
108    }
109    Ok(true)
110}
111
112pub fn routine_name_occupies_schema(name: &str, schema: &str) -> bool {
113    uqa_core::RelationIdentity::from_legacy_name(name)
114        .map_or(true, |relation| relation.schema == schema)
115}