use crate::SQLError;
use std::collections::BTreeSet;
pub trait TruncateCatalog {
fn try_resolve_visible_relation_kind(
&self,
name: &str,
) -> Result<Option<(String, &'static str)>, SQLError>;
fn is_partitioned(&self, table: &str) -> Result<bool, String>;
fn hierarchy_scan_tables(
&self,
table: &str,
descendants: bool,
) -> Result<Vec<String>, SQLError>;
fn referrers_to(&self, table: &str) -> Result<Vec<String>, String>;
fn partition_ancestor_tables(&self, table: &str) -> Result<Vec<String>, SQLError>;
}
fn referencing_tables(catalog: &dyn TruncateCatalog, table: &str) -> Result<Vec<String>, SQLError> {
let mut referencing = Vec::new();
for ancestor in catalog.partition_ancestor_tables(table)? {
for referrer in catalog
.referrers_to(&ancestor)
.map_err(|err| SQLError::Internal(format!("read foreign keys: {err}")))?
{
if !referencing.contains(&referrer) {
referencing.push(referrer);
}
}
}
Ok(referencing)
}
#[cfg(test)]
mod tests;
#[derive(Debug)]
pub struct TruncateTargets {
pub all: BTreeSet<String>,
pub trigger_order: Vec<String>,
pub privilege_targets: BTreeSet<String>,
pub cascaded: Vec<String>,
}
pub fn resolve_truncate_targets(
catalog: &dyn TruncateCatalog,
tables: &[crate::ast::TruncateTarget],
cascade: bool,
) -> Result<TruncateTargets, SQLError> {
let mut targets = BTreeSet::new();
let mut trigger_targets = Vec::new();
let mut privilege_targets = BTreeSet::new();
for requested in tables {
let table = match catalog.try_resolve_visible_relation_kind(&requested.table)? {
Some((table, "table")) => table,
Some((_, "foreign table")) => {
return Err(SQLError::Unsupported(format!(
"TRUNCATE TABLE: foreign table `{}` cannot be truncated",
requested.table
)))
}
Some((relation, _)) => {
let local = uqa_core::RelationIdentity::from_legacy_name(&relation)
.map_or(relation, |relation| relation.name);
return Err(SQLError::Routine {
sqlstate: "42809".into(),
message: format!("\"{local}\" is not a table"),
});
}
None => return Err(SQLError::UnknownTable(requested.table.clone())),
};
privilege_targets.insert(table.clone());
let partitioned = catalog
.is_partitioned(&table)
.map_err(|err| SQLError::Internal(format!("read table hierarchy: {err}")))?;
if !requested.include_descendants && partitioned {
return Err(SQLError::Routine {
sqlstate: "42809".into(),
message: "cannot truncate only a partitioned table".into(),
});
}
for target in catalog.hierarchy_scan_tables(&table, requested.include_descendants)? {
if targets.insert(target.clone()) {
trigger_targets.push(target);
}
}
}
let mut cascaded = Vec::new();
if cascade {
let mut cursor = 0;
while let Some(table) = trigger_targets.get(cursor).cloned() {
cursor += 1;
for referrer in referencing_tables(catalog, &table)? {
if targets.insert(referrer.clone()) {
privilege_targets.insert(referrer.clone());
cascaded.push(referrer.clone());
trigger_targets.push(referrer);
}
}
}
}
Ok(TruncateTargets {
all: targets,
trigger_order: trigger_targets,
privilege_targets,
cascaded,
})
}
pub fn validate_truncate_references(
catalog: &dyn TruncateCatalog,
targets: &TruncateTargets,
) -> Result<(), SQLError> {
for table in &targets.trigger_order {
if let Some(referrer) = referencing_tables(catalog, table)?
.into_iter()
.find(|referrer| !targets.all.contains(referrer))
{
let referrer = crate::semantics::foreign_keys::foreign_key_relation_name(&referrer);
return Err(SQLError::Diagnostic {
sqlstate: "0A000".into(),
message: "cannot truncate a table referenced in a foreign key constraint".into(),
detail: Some(format!(
"Table \"{referrer}\" references \"{}\".",
crate::semantics::foreign_keys::foreign_key_relation_name(table)
)),
hint: Some(format!(
"Truncate table \"{referrer}\" at the same time, or use TRUNCATE ... CASCADE."
)),
});
}
}
Ok(())
}
pub fn truncate_dependency_order(
catalog: &dyn TruncateCatalog,
targets: &TruncateTargets,
) -> Result<Vec<String>, SQLError> {
let mut ordered = Vec::with_capacity(targets.all.len());
let mut visiting = BTreeSet::new();
let mut visited = BTreeSet::new();
for table in &targets.trigger_order {
visit_truncate_target(
catalog,
table,
&targets.all,
&mut visiting,
&mut visited,
&mut ordered,
)?;
}
Ok(ordered)
}
fn visit_truncate_target(
catalog: &dyn TruncateCatalog,
table: &str,
targets: &BTreeSet<String>,
visiting: &mut BTreeSet<String>,
visited: &mut BTreeSet<String>,
ordered: &mut Vec<String>,
) -> Result<(), SQLError> {
if visited.contains(table) || !visiting.insert(table.to_string()) {
return Ok(());
}
for referrer in referencing_tables(catalog, table)? {
if targets.contains(&referrer) {
visit_truncate_target(catalog, &referrer, targets, visiting, visited, ordered)?;
}
}
visiting.remove(table);
if visited.insert(table.to_string()) {
ordered.push(table.to_string());
}
Ok(())
}