use super::{DependencyGraph, DependencyKind, ObjectAddress};
use crate::SQLError;
use std::fmt::Write as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DeletionFlags(u16);
impl DeletionFlags {
pub const ORIGINAL: Self = Self(0x0001);
pub const NORMAL: Self = Self(0x0002);
pub const AUTO: Self = Self(0x0004);
pub const INTERNAL: Self = Self(0x0008);
pub const PARTITION: Self = Self(0x0010);
pub const EXTENSION: Self = Self(0x0020);
pub const REVERSE: Self = Self(0x0040);
pub const IS_PART: Self = Self(0x0080);
pub const SUBOBJECT: Self = Self(0x0100);
#[must_use]
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 != 0
}
#[must_use]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
const fn reached_by(kind: DependencyKind) -> Self {
match kind {
DependencyKind::Normal => Self::NORMAL,
DependencyKind::Auto | DependencyKind::AutoExtension => Self::AUTO,
DependencyKind::Internal => Self::INTERNAL,
DependencyKind::PartitionPrimary | DependencyKind::PartitionSecondary => {
Self::PARTITION
}
DependencyKind::Extension => Self::EXTENSION,
}
}
const fn deleted_silently(self) -> bool {
self.contains(Self::AUTO)
|| self.contains(Self::INTERNAL)
|| self.contains(Self::PARTITION)
|| self.contains(Self::EXTENSION)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeletionTarget {
pub object: ObjectAddress,
pub flags: DeletionFlags,
pub dependee: Option<ObjectAddress>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CascadeNotice {
pub message: String,
pub detail: Option<String>,
}
struct StackEntry {
object: ObjectAddress,
flags: DeletionFlags,
}
#[derive(Debug, Clone, Default)]
pub struct DeletionTargets {
targets: Vec<DeletionTarget>,
}
const MAX_REPORTED_DEPENDENTS: usize = 100;
impl DeletionTargets {
pub fn collect(
graph: &DependencyGraph,
originals: &[ObjectAddress],
describe: super::Describe<'_>,
) -> Result<Self, SQLError> {
let mut search = Self::default();
for original in originals {
search.find(
graph,
*original,
DeletionFlags::ORIGINAL,
&mut Vec::new(),
originals,
describe,
)?;
}
Ok(search)
}
pub fn targets(&self) -> &[DeletionTarget] {
&self.targets
}
fn find(
&mut self,
graph: &DependencyGraph,
object: ObjectAddress,
mut flags: DeletionFlags,
stack: &mut Vec<StackEntry>,
pending: &[ObjectAddress],
describe: super::Describe<'_>,
) -> Result<(), SQLError> {
if stack_present_add_flags(object, flags, stack) || self.present_add_flags(object, flags) {
return Ok(());
}
let mut owner = None;
let mut partition = None;
let references = graph.references_of(object).copied().collect::<Vec<_>>();
for dependency in references {
let other = dependency.referenced;
if other.same_object(object) && object.sub_id == 0 {
continue;
}
match dependency.kind {
DependencyKind::Normal | DependencyKind::Auto | DependencyKind::AutoExtension => {}
DependencyKind::Extension | DependencyKind::Internal => {
if stack.is_empty() {
if pending.iter().any(|named| {
named.same_object(other)
&& (named.sub_id == other.sub_id || named.sub_id == 0)
}) {
return Ok(());
}
if owner.is_none() || dependency.kind == DependencyKind::Extension {
owner = Some(other);
}
continue;
}
if stack_present_add_flags(other, DeletionFlags::default(), stack) {
continue;
}
self.find(
graph,
other,
DeletionFlags::REVERSE,
stack,
pending,
describe,
)?;
if !self.present_add_flags(object, flags) {
return Err(SQLError::Internal(format!(
"deletion of owning object {} failed to delete {}",
describe_or_address(describe, other)?,
describe_or_address(describe, object)?
)));
}
return Ok(());
}
DependencyKind::PartitionPrimary => {
flags = flags.union(DeletionFlags::IS_PART);
partition = Some(other);
}
DependencyKind::PartitionSecondary => {
if !flags.contains(DeletionFlags::IS_PART) {
partition = Some(other);
}
flags = flags.union(DeletionFlags::IS_PART);
}
}
}
if let Some(owner) = owner {
let required_by = describe_or_address(describe, partition.unwrap_or(owner))?;
return Err(SQLError::Diagnostic {
sqlstate: "2BP01".into(),
message: format!(
"cannot drop {} because {required_by} requires it",
describe_or_address(describe, object)?
),
detail: None,
hint: Some(format!("You can drop {required_by} instead.")),
});
}
let mut dependents = graph
.dependents_of(object)
.filter(|dependency| !(dependency.dependent.same_object(object) && object.sub_id == 0))
.map(|dependency| {
(
dependency.dependent,
DeletionFlags::reached_by(dependency.kind),
)
})
.collect::<Vec<_>>();
dependents.sort_by(|left, right| left.0.deletion_order(&right.0));
stack.push(StackEntry { object, flags });
for (dependent, reached) in dependents {
self.find(graph, dependent, reached, stack, pending, describe)?;
}
let entry = stack.pop().expect("the pushed stack entry");
let dependee = if entry.flags.contains(DeletionFlags::IS_PART) {
partition
} else {
stack.last().map(|parent| parent.object)
};
self.targets.push(DeletionTarget {
object,
flags: entry.flags,
dependee,
});
Ok(())
}
fn present_add_flags(&mut self, object: ObjectAddress, flags: DeletionFlags) -> bool {
let mut present = false;
for target in self.targets.iter_mut().rev() {
if !target.object.same_object(object) {
continue;
}
if target.object.sub_id == object.sub_id {
target.flags = target.flags.union(flags);
present = true;
} else if target.object.sub_id == 0 {
present = true;
} else if object.sub_id == 0 && flags != DeletionFlags::default() {
target.flags = target.flags.union(flags).union(DeletionFlags::SUBOBJECT);
}
}
present
}
pub fn report(
&self,
cascade: bool,
original: Option<ObjectAddress>,
describe: super::Describe<'_>,
) -> Result<Option<CascadeNotice>, SQLError> {
for target in &self.targets {
if target.flags.contains(DeletionFlags::IS_PART)
&& !target.flags.contains(DeletionFlags::PARTITION)
{
let required_by = target
.dependee
.map(|dependee| describe_or_address(describe, dependee))
.transpose()?
.unwrap_or_default();
return Err(SQLError::Diagnostic {
sqlstate: "2BP01".into(),
message: format!(
"cannot drop {} because {required_by} requires it",
describe_or_address(describe, target.object)?
),
detail: None,
hint: Some(format!("You can drop {required_by} instead.")),
});
}
}
let mut lines = Vec::new();
let mut unreported = 0_usize;
let mut allowed = true;
for target in self.targets.iter().rev() {
if target.flags.contains(DeletionFlags::ORIGINAL)
|| target.flags.contains(DeletionFlags::SUBOBJECT)
{
continue;
}
let Some(description) = describe(target.object)? else {
continue;
};
if target.flags.deleted_silently() {
continue;
}
let line = if cascade {
Some(format!("drop cascades to {description}"))
} else {
allowed = false;
match target.dependee {
Some(dependee) => describe(dependee)?
.map(|dependee| format!("{description} depends on {dependee}")),
None => None,
}
};
match line {
Some(line) if lines.len() < MAX_REPORTED_DEPENDENTS => lines.push(line),
_ => unreported += 1,
}
}
let reported = lines.len();
let mut detail = lines.join("\n");
if unreported > 0 {
write!(
detail,
"\nand {unreported} other object{} (see server log for list)",
if unreported == 1 { "" } else { "s" }
)
.expect("writing to a String cannot fail");
}
if !allowed {
return Err(SQLError::Diagnostic {
sqlstate: "2BP01".into(),
message: match original {
Some(original) => format!(
"cannot drop {} because other objects depend on it",
describe_or_address(describe, original)?
),
None => {
"cannot drop desired object(s) because other objects depend on them".into()
}
},
detail: Some(detail),
hint: Some("Use DROP ... CASCADE to drop the dependent objects too.".into()),
});
}
Ok(match reported {
0 => None,
1 => Some(CascadeNotice {
message: detail,
detail: None,
}),
_ => {
let total = reported + unreported;
Some(CascadeNotice {
message: format!(
"drop cascades to {total} other object{}",
if total == 1 { "" } else { "s" }
),
detail: Some(detail),
})
}
})
}
}
fn stack_present_add_flags(
object: ObjectAddress,
flags: DeletionFlags,
stack: &mut [StackEntry],
) -> bool {
let mut present = false;
for entry in stack.iter_mut() {
if !entry.object.same_object(object) {
continue;
}
if entry.object.sub_id == object.sub_id {
entry.flags = entry.flags.union(flags);
present = true;
} else if entry.object.sub_id == 0 {
present = true;
} else if object.sub_id == 0 && flags != DeletionFlags::default() {
entry.flags = entry.flags.union(flags).union(DeletionFlags::SUBOBJECT);
}
}
present
}
fn describe_or_address(
describe: super::Describe<'_>,
object: ObjectAddress,
) -> Result<String, SQLError> {
Ok(describe(object)?.unwrap_or_else(|| {
format!(
"object {} of class {} column {}",
object.object_id, object.class_id, object.sub_id
)
}))
}
#[cfg(test)]
mod tests;