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 Some((table, "table")) = catalog.try_resolve_visible_relation_kind(&requested.table)?
67 else {
68 return Err(SQLError::Unsupported(format!(
69 "TRUNCATE TABLE: relation `{}` does not exist",
70 requested.table
71 )));
72 };
73 privilege_targets.insert(table.clone());
74 let partitioned = catalog
75 .is_partitioned(&table)
76 .map_err(|err| SQLError::Internal(format!("read table hierarchy: {err}")))?;
77 if !requested.include_descendants && partitioned {
78 return Err(SQLError::Routine {
79 sqlstate: "42809".into(),
80 message: "cannot truncate only a partitioned table".into(),
81 });
82 }
83 for target in catalog.hierarchy_scan_tables(&table, requested.include_descendants)? {
84 if targets.insert(target.clone()) {
85 trigger_targets.push(target);
86 }
87 }
88 }
89 let mut cascaded = Vec::new();
90 if cascade {
91 let mut cursor = 0;
92 while let Some(table) = trigger_targets.get(cursor).cloned() {
93 cursor += 1;
94 for referrer in referencing_tables(catalog, &table)? {
95 if targets.insert(referrer.clone()) {
96 privilege_targets.insert(referrer.clone());
97 cascaded.push(referrer.clone());
98 trigger_targets.push(referrer);
99 }
100 }
101 }
102 }
103 Ok(TruncateTargets {
104 all: targets,
105 trigger_order: trigger_targets,
106 privilege_targets,
107 cascaded,
108 })
109}
110
111pub fn validate_truncate_references(
113 catalog: &dyn TruncateCatalog,
114 targets: &TruncateTargets,
115) -> Result<(), SQLError> {
116 for table in &targets.trigger_order {
117 if let Some(referrer) = referencing_tables(catalog, table)?
118 .into_iter()
119 .find(|referrer| !targets.all.contains(referrer))
120 {
121 let referrer = crate::semantics::foreign_keys::foreign_key_relation_name(&referrer);
122 return Err(SQLError::Diagnostic {
123 sqlstate: "0A000".into(),
124 message: "cannot truncate a table referenced in a foreign key constraint".into(),
125 detail: Some(format!(
126 "Table \"{referrer}\" references \"{}\".",
127 crate::semantics::foreign_keys::foreign_key_relation_name(table)
128 )),
129 hint: Some(format!(
130 "Truncate table \"{referrer}\" at the same time, or use TRUNCATE ... CASCADE."
131 )),
132 });
133 }
134 }
135 Ok(())
136}
137
138pub fn truncate_dependency_order(
140 catalog: &dyn TruncateCatalog,
141 targets: &TruncateTargets,
142) -> Result<Vec<String>, SQLError> {
143 let mut ordered = Vec::with_capacity(targets.all.len());
144 let mut visiting = BTreeSet::new();
145 let mut visited = BTreeSet::new();
146 for table in &targets.trigger_order {
147 visit_truncate_target(
148 catalog,
149 table,
150 &targets.all,
151 &mut visiting,
152 &mut visited,
153 &mut ordered,
154 )?;
155 }
156 Ok(ordered)
157}
158
159fn visit_truncate_target(
160 catalog: &dyn TruncateCatalog,
161 table: &str,
162 targets: &BTreeSet<String>,
163 visiting: &mut BTreeSet<String>,
164 visited: &mut BTreeSet<String>,
165 ordered: &mut Vec<String>,
166) -> Result<(), SQLError> {
167 if visited.contains(table) || !visiting.insert(table.to_string()) {
168 return Ok(());
169 }
170 for referrer in referencing_tables(catalog, table)? {
171 if targets.contains(&referrer) {
172 visit_truncate_target(catalog, &referrer, targets, visiting, visited, ordered)?;
173 }
174 }
175 visiting.remove(table);
176 if visited.insert(table.to_string()) {
177 ordered.push(table.to_string());
178 }
179 Ok(())
180}