Skip to main content

uqa_sql/schema/
removal.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! DROP relation target binding, label protection and declared dependency discovery.
8use crate::{
9    ast::{DropKind, DropStmt},
10    catalog::resolution::RelationResolution,
11    SQLError,
12};
13use std::collections::BTreeSet;
14
15pub trait RelationDropCatalog {
16    fn resolve_relation_kind(&self, name: &str) -> Result<RelationResolution, SQLError>;
17    fn resolve_age_label_relation_name(&self, name: &str) -> Result<Option<String>, SQLError>;
18}
19pub trait ForeignTableDropDependencies {
20    fn views_depending_on_relation(&self, name: &str) -> Result<Vec<String>, SQLError>;
21    fn rules_depending_on_relations(
22        &self,
23        names: &[String],
24    ) -> Result<Vec<(uqa_core::RelationIdentity, String)>, SQLError>;
25    fn sequence_external_dependents_for_owner_drop(
26        &self,
27        name: &str,
28        targets: &BTreeSet<String>,
29    ) -> Result<Vec<String>, SQLError>;
30}
31pub fn validate_drop_table_label_target(
32    catalog: &dyn RelationDropCatalog,
33    name: &str,
34) -> Result<(), SQLError> {
35    if let Some(canonical) = catalog.resolve_age_label_relation_name(name)? {
36        let relation =
37            uqa_core::RelationIdentity::from_legacy_name(&canonical).map_err(|error| {
38                SQLError::Internal(format!(
39                    "resolve AGE label relation `{canonical}` for DROP TABLE: {error}"
40                ))
41            })?;
42        return Err(SQLError::Routine {
43            sqlstate: "2BP01".into(),
44            message: format!(
45                "table \"{}\" is for label \"{}\"",
46                relation.name, relation.name
47            ),
48        });
49    }
50    Ok(())
51}
52
53pub fn drop_relation_kind(kind: DropKind) -> &'static str {
54    match kind {
55        DropKind::Table => "table",
56        DropKind::ForeignTable => "foreign table",
57        DropKind::View => "view",
58        DropKind::MaterializedView => "materialized view",
59        DropKind::Sequence => "sequence",
60        DropKind::Index => "index",
61        DropKind::Schema => "schema",
62        DropKind::Domain => "domain",
63    }
64}
65
66/// Resolve one requested name so execution can repeat the same policy after a lock wait.
67pub fn bind_relation_drop_target(
68    catalog: &dyn RelationDropCatalog,
69    name: &str,
70    kind: DropKind,
71    if_exists: bool,
72    notice: &mut dyn FnMut(&str),
73) -> Result<Option<String>, SQLError> {
74    if kind == DropKind::Table {
75        validate_drop_table_label_target(catalog, name)?;
76    }
77    let expected = drop_relation_kind(kind);
78    let (_, local) =
79        uqa_core::RelationIdentity::parse_reference(name).map_err(SQLError::Internal)?;
80    match catalog.resolve_relation_kind(name)? {
81        RelationResolution::Found(canonical, found) if found == expected => Ok(Some(canonical)),
82        RelationResolution::Found(_, _) => Err(SQLError::Routine {
83            sqlstate: "42809".into(),
84            message: format!("\"{local}\" is not a {expected}"),
85        }),
86        RelationResolution::MissingSchema(schema) if if_exists => {
87            notice(&format!("schema \"{schema}\" does not exist, skipping"));
88            Ok(None)
89        }
90        RelationResolution::MissingRelation if if_exists => {
91            notice(&format!("{expected} \"{local}\" does not exist, skipping"));
92            Ok(None)
93        }
94        RelationResolution::MissingSchema(schema) => Err(SQLError::Routine {
95            sqlstate: "3F000".into(),
96            message: format!("schema \"{schema}\" does not exist"),
97        }),
98        RelationResolution::MissingRelation => Err(SQLError::Routine {
99            sqlstate: if matches!(kind, DropKind::ForeignTable | DropKind::Index) {
100                "42704"
101            } else {
102                "42P01"
103            }
104            .into(),
105            message: format!("{expected} \"{local}\" does not exist"),
106        }),
107    }
108}
109
110pub fn bind_relation_drop_targets(
111    catalog: &dyn RelationDropCatalog,
112    stmt: &DropStmt,
113    notice: &mut dyn FnMut(&str),
114) -> Result<Vec<String>, SQLError> {
115    let mut targets = Vec::new();
116    let mut seen = BTreeSet::new();
117    for name in &stmt.names {
118        if let Some(canonical) =
119            bind_relation_drop_target(catalog, name, stmt.kind, stmt.if_exists, notice)?
120        {
121            if seen.insert(canonical.clone()) {
122                targets.push(canonical);
123            }
124        }
125    }
126    Ok(targets)
127}
128
129pub fn foreign_table_drop_dependents(
130    catalog: &dyn ForeignTableDropDependencies,
131    foreign_tables: &[String],
132    owned_sequences: &BTreeSet<String>,
133    target_names: &BTreeSet<String>,
134) -> Result<BTreeSet<String>, SQLError> {
135    let mut dependents = std::collections::BTreeSet::new();
136    for table in foreign_tables {
137        dependents.extend(
138            catalog
139                .views_depending_on_relation(table)?
140                .into_iter()
141                .map(|view| format!("view {view}")),
142        );
143    }
144    dependents.extend(
145        catalog
146            .rules_depending_on_relations(foreign_tables)?
147            .into_iter()
148            .map(|(table, rule)| format!("rule {rule} on table {}", table.qualified_name())),
149    );
150    for sequence in owned_sequences {
151        dependents
152            .extend(catalog.sequence_external_dependents_for_owner_drop(sequence, target_names)?);
153    }
154    Ok(dependents)
155}
156
157pub mod hierarchy;
158pub mod tables;
159
160#[cfg(test)]
161mod tests;