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_targets(
32    catalog: &dyn RelationDropCatalog,
33    stmt: &DropStmt,
34) -> Result<(), SQLError> {
35    if stmt.kind == DropKind::Table {
36        for name in &stmt.names {
37            if let Some(canonical) = catalog.resolve_age_label_relation_name(name)? {
38                let relation =
39                    uqa_core::RelationIdentity::from_legacy_name(&canonical).map_err(|error| {
40                        SQLError::Internal(format!(
41                            "resolve AGE label relation `{canonical}` for DROP TABLE: {error}"
42                        ))
43                    })?;
44                return Err(SQLError::Routine {
45                    sqlstate: "2BP01".into(),
46                    message: format!(
47                        "table \"{}\" is for label \"{}\"",
48                        relation.name, relation.name
49                    ),
50                });
51            }
52        }
53    }
54    Ok(())
55}
56pub fn bind_table_drop_targets(
57    catalog: &dyn RelationDropCatalog,
58    stmt: &DropStmt,
59    notice: &mut dyn FnMut(&str),
60) -> Result<Vec<String>, SQLError> {
61    let mut tables = Vec::new();
62    for name in &stmt.names {
63        let (_, local) =
64            uqa_core::RelationIdentity::parse_reference(name).map_err(SQLError::Internal)?;
65        match catalog.resolve_relation_kind(name)? {
66            RelationResolution::Found(canonical, "table") => tables.push(canonical),
67            RelationResolution::Found(_, _) => {
68                return Err(SQLError::Routine {
69                    sqlstate: "42809".into(),
70                    message: format!("\"{local}\" is not a table"),
71                });
72            }
73            RelationResolution::MissingSchema(schema) if stmt.if_exists => {
74                notice(&format!("schema \"{schema}\" does not exist, skipping"));
75            }
76            RelationResolution::MissingRelation if stmt.if_exists => {
77                notice(&format!("table \"{local}\" does not exist, skipping"));
78            }
79            RelationResolution::MissingSchema(schema) => {
80                return Err(SQLError::Routine {
81                    sqlstate: "3F000".into(),
82                    message: format!("schema \"{schema}\" does not exist"),
83                });
84            }
85            RelationResolution::MissingRelation => {
86                return Err(SQLError::Routine {
87                    sqlstate: "42P01".into(),
88                    message: format!("table \"{local}\" does not exist"),
89                });
90            }
91        }
92    }
93    Ok(tables)
94}
95
96pub fn bind_foreign_table_drop_targets(
97    catalog: &dyn RelationDropCatalog,
98    stmt: &DropStmt,
99    notice: &mut dyn FnMut(&str),
100) -> Result<Vec<String>, SQLError> {
101    let mut foreign_tables = Vec::new();
102    let mut seen = std::collections::BTreeSet::new();
103    for name in &stmt.names {
104        match catalog.resolve_relation_kind(name)? {
105            RelationResolution::Found(canonical, "foreign table") => {
106                if seen.insert(canonical.clone()) {
107                    foreign_tables.push(canonical);
108                }
109            }
110            RelationResolution::Found(_, _) => {
111                return Err(SQLError::Routine {
112                    sqlstate: "42809".into(),
113                    message: format!("\"{name}\" is not a foreign table"),
114                });
115            }
116            RelationResolution::MissingSchema(schema) if stmt.if_exists => {
117                notice(&format!("schema \"{schema}\" does not exist, skipping"));
118            }
119            RelationResolution::MissingRelation if stmt.if_exists => {
120                notice(&format!(
121                    "foreign table \"{name}\" does not exist, skipping"
122                ));
123            }
124            RelationResolution::MissingSchema(schema) => {
125                return Err(SQLError::Routine {
126                    sqlstate: "3F000".into(),
127                    message: format!("schema \"{schema}\" does not exist"),
128                });
129            }
130            RelationResolution::MissingRelation => {
131                return Err(SQLError::Routine {
132                    sqlstate: "42P01".into(),
133                    message: format!("foreign table \"{name}\" does not exist"),
134                });
135            }
136        }
137    }
138    Ok(foreign_tables)
139}
140
141pub fn bind_view_drop_targets(
142    catalog: &dyn RelationDropCatalog,
143    stmt: &DropStmt,
144) -> Result<(Vec<String>, &'static str), SQLError> {
145    let expected_kind = if stmt.kind == DropKind::View {
146        "view"
147    } else {
148        "materialized view"
149    };
150    let command = if stmt.kind == DropKind::View {
151        "DROP VIEW"
152    } else {
153        "DROP MATERIALIZED VIEW"
154    };
155    let mut views = Vec::new();
156    for name in &stmt.names {
157        match catalog.resolve_relation_kind(name)?.into_found() {
158            Some((canonical, kind)) if kind == expected_kind => views.push(canonical),
159            Some((canonical, kind)) => {
160                return Err(SQLError::Routine {
161                    sqlstate: "42809".into(),
162                    message: format!(
163                        "{command}: relation `{canonical}` is a {kind}, not a {expected_kind}"
164                    ),
165                });
166            }
167            None if stmt.if_exists => {}
168            None => {
169                return Err(SQLError::Routine {
170                    sqlstate: "42P01".into(),
171                    message: format!("{command}: relation `{name}` does not exist"),
172                });
173            }
174        }
175    }
176    Ok((views, expected_kind))
177}
178
179pub fn bind_sequence_drop_targets(
180    catalog: &dyn RelationDropCatalog,
181    stmt: &DropStmt,
182    notice: &mut dyn FnMut(&str),
183) -> Result<Vec<String>, SQLError> {
184    let mut sequences = Vec::new();
185    let mut seen = std::collections::BTreeSet::new();
186    for name in &stmt.names {
187        match catalog.resolve_relation_kind(name)? {
188            RelationResolution::Found(canonical, "sequence") => {
189                if seen.insert(canonical.clone()) {
190                    sequences.push(canonical);
191                }
192            }
193            RelationResolution::Found(_canonical, _kind) => {
194                return Err(SQLError::Routine {
195                    sqlstate: "42809".into(),
196                    message: format!("\"{name}\" is not a sequence"),
197                });
198            }
199            RelationResolution::MissingRelation | RelationResolution::MissingSchema(_)
200                if stmt.if_exists =>
201            {
202                notice(&format!("sequence \"{name}\" does not exist, skipping"));
203            }
204            RelationResolution::MissingSchema(schema) => {
205                return Err(SQLError::Routine {
206                    sqlstate: "3F000".into(),
207                    message: format!("schema \"{schema}\" does not exist"),
208                });
209            }
210            RelationResolution::MissingRelation => {
211                return Err(SQLError::Routine {
212                    sqlstate: "42P01".into(),
213                    message: format!("sequence \"{name}\" does not exist"),
214                });
215            }
216        }
217    }
218    Ok(sequences)
219}
220pub fn foreign_table_drop_dependents(
221    catalog: &dyn ForeignTableDropDependencies,
222    foreign_tables: &[String],
223    owned_sequences: &BTreeSet<String>,
224    target_names: &BTreeSet<String>,
225) -> Result<BTreeSet<String>, SQLError> {
226    let mut dependents = std::collections::BTreeSet::new();
227    for table in foreign_tables {
228        dependents.extend(
229            catalog
230                .views_depending_on_relation(table)?
231                .into_iter()
232                .map(|view| format!("view {view}")),
233        );
234    }
235    dependents.extend(
236        catalog
237            .rules_depending_on_relations(foreign_tables)?
238            .into_iter()
239            .map(|(table, rule)| format!("rule {rule} on table {}", table.qualified_name())),
240    );
241    for sequence in owned_sequences {
242        dependents
243            .extend(catalog.sequence_external_dependents_for_owner_drop(sequence, target_names)?);
244    }
245    Ok(dependents)
246}
247
248pub mod hierarchy;
249pub mod tables;