use rupg_catalog::SequenceData;
use rupg_common::{Error, Result, SqlState};
use rupg_sql::nodes::{DropBehavior, Node, TruncateStmt};
use super::comment::not_owner;
use super::{Definer, Found, not_yet, relname};
impl Definer<'_, '_> {
pub(super) fn truncate(&mut self, stmt: &TruncateStmt) -> Result<()> {
let mut tables: Vec<u32> = Vec::new();
for node in stmt.relations.iter().flatten() {
let Node::RangeVar(rv) = node else { continue };
let found = self.lookup_relation(rv)?;
let table = self.truncate_check(found, relname(rv))?;
if tables.contains(&table) {
continue;
}
tables.push(table);
if rv.inh {
let mut at = tables.len() - 1;
while at < tables.len() {
for child in self.catalog.children(tables[at]) {
if !tables.contains(&child.oid) {
self.truncate_check_rel(Found::User(child.oid), &child.name)?;
tables.push(child.oid);
}
}
at += 1;
}
} else if self.kind_of(found) == b'p' {
return Err(Error::new(
SqlState::WRONG_OBJECT_TYPE,
"cannot truncate only a partitioned table",
)
.with_hint(
"Do not specify the ONLY keyword, or use TRUNCATE ONLY on the partitions directly.",
));
}
}
if stmt.behavior == DropBehavior::DROP_CASCADE {
loop {
let added: Vec<u32> =
self.referencing(&tables).into_iter().filter(|t| !tables.contains(t)).collect();
if added.is_empty() {
break;
}
for table in added {
let name = self.catalog.relation(table).map(|r| r.name.clone());
let name = name.unwrap_or_default();
self.notice(
SqlState::SUCCESSFUL_COMPLETION,
format!("truncate cascades to table \"{name}\""),
);
self.truncate_check(Found::User(table), &name)?;
tables.push(table);
}
}
} else {
self.check_truncate_keys(&tables)?;
}
let fires = |t: &rupg_catalog::Trigger| {
tables.contains(&t.relation)
&& t.kind & rupg_catalog::TRIGGER_TRUNCATE != 0
&& t.fires()
};
if self.catalog.triggers().any(fires) {
return Err(not_yet("TRUNCATE of a table with TRUNCATE triggers", None));
}
let mut sequences = Vec::new();
if stmt.restart_seqs {
for &table in &tables {
for sequence in self.catalog.owned_sequences(table) {
let rel = self.catalog.relation(sequence);
let owner = rel.map_or(0, |r| r.owner);
if !self.is_superuser() && owner != self.user {
let name = rel.map(|r| r.name.clone()).unwrap_or_default();
return Err(not_owner("sequence", &name));
}
sequences.push(sequence);
}
}
}
for sequence in sequences {
self.reset_sequence(sequence)?;
}
self.truncated = tables;
Ok(())
}
fn truncate_check_rel(&self, found: Found, name: &str) -> Result<()> {
if !matches!(self.kind_of(found), b'r' | b'p') {
return Err(Error::new(
SqlState::WRONG_OBJECT_TYPE,
format!("\"{name}\" is not a table"),
));
}
if self.is_system(found) && !self.an.env.allow_system_table_mods() {
return Err(Error::new(
SqlState::INSUFFICIENT_PRIVILEGE,
format!("permission denied: \"{name}\" is a system catalog"),
));
}
Ok(())
}
fn truncate_check(&self, found: Found, name: &str) -> Result<u32> {
self.truncate_check_rel(found, name)?;
let owner = match found {
Found::User(oid) => self.catalog.relation(oid).map_or(0, |r| r.owner),
Found::Builtin(row) => row.owner,
};
if !self.is_superuser() && owner != self.user {
return Err(Error::new(
SqlState::INSUFFICIENT_PRIVILEGE,
format!("permission denied for table {name}"),
));
}
match found {
Found::User(oid) => Ok(oid),
Found::Builtin(_) => Err(not_yet("TRUNCATE of a built-in table", None)),
}
}
fn referencing(&self, tables: &[u32]) -> Vec<u32> {
let mut found: Vec<u32> = self
.catalog
.constraints()
.filter(|c| c.foreign.as_ref().is_some_and(|f| tables.contains(&f.table)))
.map(|c| c.relation)
.collect();
found.sort_unstable();
found.dedup();
found
}
fn check_truncate_keys(&self, tables: &[u32]) -> Result<()> {
let with_triggers: Vec<u32> = tables
.iter()
.copied()
.filter(|&t| self.catalog.relation(t).is_some_and(|r| r.has_triggers))
.collect();
for &table in &with_triggers {
let Some(other) =
self.referencing(&[table]).into_iter().find(|t| !with_triggers.contains(t))
else {
continue;
};
let name =
|oid: u32| self.catalog.relation(oid).map(|r| r.name.clone()).unwrap_or_default();
let (table, other) = (name(table), name(other));
return Err(Error::new(
SqlState::FEATURE_NOT_SUPPORTED,
"cannot truncate a table referenced in a foreign key constraint",
)
.with_detail(format!("Table \"{other}\" references \"{table}\"."))
.with_hint(format!(
"Truncate table \"{other}\" at the same time, or use TRUNCATE ... CASCADE."
)));
}
Ok(())
}
fn reset_sequence(&mut self, sequence: u32) -> Result<()> {
let Some(info) = self.catalog.relation(sequence).and_then(|r| r.sequence.clone()) else {
return Err(Error::internal(format!("cache lookup failed for sequence {sequence}")));
};
let start = info.start;
self.catalog.alter_sequence(sequence, info, Some(SequenceData::new(start)))
}
}