uqa_sql/schema/
truncate.rs1use crate::SQLError;
10use std::collections::BTreeSet;
11
12pub trait TruncateCatalog {
14 fn try_resolve_visible_relation_kind(
15 &self,
16 name: &str,
17 ) -> Result<Option<(String, &'static str)>, SQLError>;
18 fn is_partitioned(&self, table: &str) -> Result<bool, String>;
19 fn hierarchy_scan_tables(
20 &self,
21 table: &str,
22 descendants: bool,
23 ) -> Result<Vec<String>, SQLError>;
24 fn referrers_to(&self, table: &str) -> Result<Vec<String>, String>;
25 fn partition_ancestor_tables(&self, table: &str) -> Result<Vec<String>, SQLError>;
27}
28
29fn referencing_tables(catalog: &dyn TruncateCatalog, table: &str) -> Result<Vec<String>, SQLError> {
31 let mut referencing = Vec::new();
32 for ancestor in catalog.partition_ancestor_tables(table)? {
33 for referrer in catalog
34 .referrers_to(&ancestor)
35 .map_err(|err| SQLError::Internal(format!("read foreign keys: {err}")))?
36 {
37 if !referencing.contains(&referrer) {
38 referencing.push(referrer);
39 }
40 }
41 }
42 Ok(referencing)
43}
44
45#[cfg(test)]
46mod tests;
47
48#[derive(Debug)]
49pub struct TruncateTargets {
50 pub all: BTreeSet<String>,
51 pub trigger_order: Vec<String>,
52 pub privilege_targets: BTreeSet<String>,
53 pub cascaded: Vec<String>,
55}
56
57pub fn resolve_truncate_targets(
58 catalog: &dyn TruncateCatalog,
59 tables: &[crate::ast::TruncateTarget],
60 cascade: bool,
61) -> Result<TruncateTargets, SQLError> {
62 let mut targets = BTreeSet::new();
63 let mut trigger_targets = Vec::new();
64 let mut privilege_targets = BTreeSet::new();
65 for requested in tables {
66 let table = match catalog.try_resolve_visible_relation_kind(&requested.table)? {
67 Some((table, "table")) => table,
68 Some((_, "foreign table")) => {
69 return Err(SQLError::Unsupported(format!(
70 "TRUNCATE TABLE: foreign table `{}` cannot be truncated",
71 requested.table
72 )))
73 }
74 Some((relation, _)) => {
76 let local = uqa_core::RelationIdentity::from_legacy_name(&relation)
77 .map_or(relation, |relation| relation.name);
78 return Err(SQLError::Routine {
79 sqlstate: "42809".into(),
80 message: format!("\"{local}\" is not a table"),
81 });
82 }
83 None => return Err(SQLError::UnknownTable(requested.table.clone())),
84 };
85 privilege_targets.insert(table.clone());
86 let partitioned = catalog
87 .is_partitioned(&table)
88 .map_err(|err| SQLError::Internal(format!("read table hierarchy: {err}")))?;
89 if !requested.include_descendants && partitioned {
90 return Err(SQLError::Routine {
91 sqlstate: "42809".into(),
92 message: "cannot truncate only a partitioned table".into(),
93 });
94 }
95 for target in catalog.hierarchy_scan_tables(&table, requested.include_descendants)? {
96 if targets.insert(target.clone()) {
97 trigger_targets.push(target);
98 }
99 }
100 }
101 let mut cascaded = Vec::new();
102 if cascade {
103 let mut cursor = 0;
104 while let Some(table) = trigger_targets.get(cursor).cloned() {
105 cursor += 1;
106 for referrer in referencing_tables(catalog, &table)? {
107 if targets.insert(referrer.clone()) {
108 privilege_targets.insert(referrer.clone());
109 cascaded.push(referrer.clone());
110 trigger_targets.push(referrer);
111 }
112 }
113 }
114 }
115 Ok(TruncateTargets {
116 all: targets,
117 trigger_order: trigger_targets,
118 privilege_targets,
119 cascaded,
120 })
121}
122
123pub fn validate_truncate_references(
125 catalog: &dyn TruncateCatalog,
126 targets: &TruncateTargets,
127) -> Result<(), SQLError> {
128 for table in &targets.trigger_order {
129 if let Some(referrer) = referencing_tables(catalog, table)?
130 .into_iter()
131 .find(|referrer| !targets.all.contains(referrer))
132 {
133 let referrer = crate::semantics::foreign_keys::foreign_key_relation_name(&referrer);
134 return Err(SQLError::Diagnostic {
135 sqlstate: "0A000".into(),
136 message: "cannot truncate a table referenced in a foreign key constraint".into(),
137 detail: Some(format!(
138 "Table \"{referrer}\" references \"{}\".",
139 crate::semantics::foreign_keys::foreign_key_relation_name(table)
140 )),
141 hint: Some(format!(
142 "Truncate table \"{referrer}\" at the same time, or use TRUNCATE ... CASCADE."
143 )),
144 });
145 }
146 }
147 Ok(())
148}
149
150pub fn truncate_dependency_order(
152 catalog: &dyn TruncateCatalog,
153 targets: &TruncateTargets,
154) -> Result<Vec<String>, SQLError> {
155 let mut ordered = Vec::with_capacity(targets.all.len());
156 let mut visiting = BTreeSet::new();
157 let mut visited = BTreeSet::new();
158 for table in &targets.trigger_order {
159 visit_truncate_target(
160 catalog,
161 table,
162 &targets.all,
163 &mut visiting,
164 &mut visited,
165 &mut ordered,
166 )?;
167 }
168 Ok(ordered)
169}
170
171fn visit_truncate_target(
172 catalog: &dyn TruncateCatalog,
173 table: &str,
174 targets: &BTreeSet<String>,
175 visiting: &mut BTreeSet<String>,
176 visited: &mut BTreeSet<String>,
177 ordered: &mut Vec<String>,
178) -> Result<(), SQLError> {
179 if visited.contains(table) || !visiting.insert(table.to_string()) {
180 return Ok(());
181 }
182 for referrer in referencing_tables(catalog, table)? {
183 if targets.contains(&referrer) {
184 visit_truncate_target(catalog, &referrer, targets, visiting, visited, ordered)?;
185 }
186 }
187 visiting.remove(table);
188 if visited.insert(table.to_string()) {
189 ordered.push(table.to_string());
190 }
191 Ok(())
192}