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