Skip to main content

uqa_sql/catalog/dependencies/
deletion.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! The objects a `DROP` deletes, found and reported as `findDependentObjects` and `reportDependentObjects` do.
8
9use super::{DependencyGraph, DependencyKind, ObjectAddress};
10use crate::SQLError;
11use std::fmt::Write as _;
12
13/// `DEPFLAG_*`: how the search reached an object.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub struct DeletionFlags(u16);
16
17impl DeletionFlags {
18    /// An object the command names.
19    pub const ORIGINAL: Self = Self(0x0001);
20    pub const NORMAL: Self = Self(0x0002);
21    pub const AUTO: Self = Self(0x0004);
22    pub const INTERNAL: Self = Self(0x0008);
23    pub const PARTITION: Self = Self(0x0010);
24    pub const EXTENSION: Self = Self(0x0020);
25    /// Reached from an object that is part of it, which redirected the search to its owner.
26    pub const REVERSE: Self = Self(0x0040);
27    /// The object has a partition dependency.
28    pub const IS_PART: Self = Self(0x0080);
29    /// A column of a relation that is deleted as a whole.
30    pub const SUBOBJECT: Self = Self(0x0100);
31
32    #[must_use]
33    pub const fn contains(self, other: Self) -> bool {
34        self.0 & other.0 != 0
35    }
36
37    #[must_use]
38    pub const fn union(self, other: Self) -> Self {
39        Self(self.0 | other.0)
40    }
41
42    const fn reached_by(kind: DependencyKind) -> Self {
43        match kind {
44            DependencyKind::Normal => Self::NORMAL,
45            DependencyKind::Auto | DependencyKind::AutoExtension => Self::AUTO,
46            DependencyKind::Internal => Self::INTERNAL,
47            DependencyKind::PartitionPrimary | DependencyKind::PartitionSecondary => {
48                Self::PARTITION
49            }
50            DependencyKind::Extension => Self::EXTENSION,
51        }
52    }
53
54    /// Objects reached through these dependencies are deleted without `CASCADE` and not reported.
55    const fn deleted_silently(self) -> bool {
56        self.contains(Self::AUTO)
57            || self.contains(Self::INTERNAL)
58            || self.contains(Self::PARTITION)
59            || self.contains(Self::EXTENSION)
60    }
61}
62
63/// One object to delete, with how it was reached and the object whose deletion requires it.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct DeletionTarget {
66    pub object: ObjectAddress,
67    pub flags: DeletionFlags,
68    pub dependee: Option<ObjectAddress>,
69}
70
71/// The `NOTICE` a cascading `DROP` reports: one line for a single object, or a count with the list in the detail.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct CascadeNotice {
74    pub message: String,
75    pub detail: Option<String>,
76}
77
78struct StackEntry {
79    object: ObjectAddress,
80    flags: DeletionFlags,
81}
82
83/// The objects to delete in deletion order: every dependent before the object it depends on.
84#[derive(Debug, Clone, Default)]
85pub struct DeletionTargets {
86    targets: Vec<DeletionTarget>,
87}
88
89/// `MAX_REPORTED_DEPS`: the detail lists at most this many objects.
90const MAX_REPORTED_DEPENDENTS: usize = 100;
91
92impl DeletionTargets {
93    /// `performMultipleDeletions`' search: the named objects and everything that depends on them. An object that is internal to another named object is deleted with its owner; one that is internal to an unnamed object reports `2BP01` as `findDependentObjects` does.
94    pub fn collect(
95        graph: &DependencyGraph,
96        originals: &[ObjectAddress],
97        describe: super::Describe<'_>,
98    ) -> Result<Self, SQLError> {
99        let mut search = Self::default();
100        for original in originals {
101            search.find(
102                graph,
103                *original,
104                DeletionFlags::ORIGINAL,
105                &mut Vec::new(),
106                originals,
107                describe,
108            )?;
109        }
110        Ok(search)
111    }
112
113    pub fn targets(&self) -> &[DeletionTarget] {
114        &self.targets
115    }
116
117    fn find(
118        &mut self,
119        graph: &DependencyGraph,
120        object: ObjectAddress,
121        mut flags: DeletionFlags,
122        stack: &mut Vec<StackEntry>,
123        pending: &[ObjectAddress],
124        describe: super::Describe<'_>,
125    ) -> Result<(), SQLError> {
126        if stack_present_add_flags(object, flags, stack) || self.present_add_flags(object, flags) {
127            return Ok(());
128        }
129        let mut owner = None;
130        let mut partition = None;
131        let references = graph.references_of(object).copied().collect::<Vec<_>>();
132        for dependency in references {
133            let other = dependency.referenced;
134            // A column's dependency on its own relation is implicit.
135            if other.same_object(object) && object.sub_id == 0 {
136                continue;
137            }
138            match dependency.kind {
139                DependencyKind::Normal | DependencyKind::Auto | DependencyKind::AutoExtension => {}
140                DependencyKind::Extension | DependencyKind::Internal => {
141                    if stack.is_empty() {
142                        if pending.iter().any(|named| {
143                            named.same_object(other)
144                                && (named.sub_id == other.sub_id || named.sub_id == 0)
145                        }) {
146                            // The owner is named too; deleting it deletes this object.
147                            return Ok(());
148                        }
149                        if owner.is_none() || dependency.kind == DependencyKind::Extension {
150                            owner = Some(other);
151                        }
152                        continue;
153                    }
154                    if stack_present_add_flags(other, DeletionFlags::default(), stack) {
155                        continue;
156                    }
157                    // Delete the owner instead; this object is deleted when the search returns to it from the owner.
158                    self.find(
159                        graph,
160                        other,
161                        DeletionFlags::REVERSE,
162                        stack,
163                        pending,
164                        describe,
165                    )?;
166                    if !self.present_add_flags(object, flags) {
167                        return Err(SQLError::Internal(format!(
168                            "deletion of owning object {} failed to delete {}",
169                            describe_or_address(describe, other)?,
170                            describe_or_address(describe, object)?
171                        )));
172                    }
173                    return Ok(());
174                }
175                DependencyKind::PartitionPrimary => {
176                    flags = flags.union(DeletionFlags::IS_PART);
177                    partition = Some(other);
178                }
179                DependencyKind::PartitionSecondary => {
180                    if !flags.contains(DeletionFlags::IS_PART) {
181                        partition = Some(other);
182                    }
183                    flags = flags.union(DeletionFlags::IS_PART);
184                }
185            }
186        }
187        if let Some(owner) = owner {
188            let required_by = describe_or_address(describe, partition.unwrap_or(owner))?;
189            return Err(SQLError::Diagnostic {
190                sqlstate: "2BP01".into(),
191                message: format!(
192                    "cannot drop {} because {required_by} requires it",
193                    describe_or_address(describe, object)?
194                ),
195                detail: None,
196                hint: Some(format!("You can drop {required_by} instead.")),
197            });
198        }
199        let mut dependents = graph
200            .dependents_of(object)
201            .filter(|dependency| !(dependency.dependent.same_object(object) && object.sub_id == 0))
202            .map(|dependency| {
203                (
204                    dependency.dependent,
205                    DeletionFlags::reached_by(dependency.kind),
206                )
207            })
208            .collect::<Vec<_>>();
209        dependents.sort_by(|left, right| left.0.deletion_order(&right.0));
210        stack.push(StackEntry { object, flags });
211        for (dependent, reached) in dependents {
212            self.find(graph, dependent, reached, stack, pending, describe)?;
213        }
214        let entry = stack.pop().expect("the pushed stack entry");
215        let dependee = if entry.flags.contains(DeletionFlags::IS_PART) {
216            partition
217        } else {
218            stack.last().map(|parent| parent.object)
219        };
220        self.targets.push(DeletionTarget {
221            object,
222            flags: entry.flags,
223            dependee,
224        });
225        Ok(())
226    }
227
228    /// `object_address_present_add_flags`: whether the object, or the whole relation of a column, is already a target, merging the new flags into it. A whole relation marks its columns already found as subobjects, which are not reported.
229    fn present_add_flags(&mut self, object: ObjectAddress, flags: DeletionFlags) -> bool {
230        let mut present = false;
231        for target in self.targets.iter_mut().rev() {
232            if !target.object.same_object(object) {
233                continue;
234            }
235            if target.object.sub_id == object.sub_id {
236                target.flags = target.flags.union(flags);
237                present = true;
238            } else if target.object.sub_id == 0 {
239                present = true;
240            } else if object.sub_id == 0 && flags != DeletionFlags::default() {
241                target.flags = target.flags.union(flags).union(DeletionFlags::SUBOBJECT);
242            }
243        }
244        present
245    }
246
247    /// `reportDependentObjects`: reject a `RESTRICT` drop that would delete an object reached through a normal dependency, or describe what a cascading drop deletes. `original` names the single object the command drops.
248    pub fn report(
249        &self,
250        cascade: bool,
251        original: Option<ObjectAddress>,
252        describe: super::Describe<'_>,
253    ) -> Result<Option<CascadeNotice>, SQLError> {
254        for target in &self.targets {
255            if target.flags.contains(DeletionFlags::IS_PART)
256                && !target.flags.contains(DeletionFlags::PARTITION)
257            {
258                let required_by = target
259                    .dependee
260                    .map(|dependee| describe_or_address(describe, dependee))
261                    .transpose()?
262                    .unwrap_or_default();
263                return Err(SQLError::Diagnostic {
264                    sqlstate: "2BP01".into(),
265                    message: format!(
266                        "cannot drop {} because {required_by} requires it",
267                        describe_or_address(describe, target.object)?
268                    ),
269                    detail: None,
270                    hint: Some(format!("You can drop {required_by} instead.")),
271                });
272            }
273        }
274        let mut lines = Vec::new();
275        let mut unreported = 0_usize;
276        let mut allowed = true;
277        for target in self.targets.iter().rev() {
278            if target.flags.contains(DeletionFlags::ORIGINAL)
279                || target.flags.contains(DeletionFlags::SUBOBJECT)
280            {
281                continue;
282            }
283            let Some(description) = describe(target.object)? else {
284                continue;
285            };
286            if target.flags.deleted_silently() {
287                continue;
288            }
289            let line = if cascade {
290                Some(format!("drop cascades to {description}"))
291            } else {
292                allowed = false;
293                match target.dependee {
294                    Some(dependee) => describe(dependee)?
295                        .map(|dependee| format!("{description} depends on {dependee}")),
296                    None => None,
297                }
298            };
299            match line {
300                Some(line) if lines.len() < MAX_REPORTED_DEPENDENTS => lines.push(line),
301                _ => unreported += 1,
302            }
303        }
304        let reported = lines.len();
305        let mut detail = lines.join("\n");
306        if unreported > 0 {
307            write!(
308                detail,
309                "\nand {unreported} other object{} (see server log for list)",
310                if unreported == 1 { "" } else { "s" }
311            )
312            .expect("writing to a String cannot fail");
313        }
314        if !allowed {
315            return Err(SQLError::Diagnostic {
316                sqlstate: "2BP01".into(),
317                message: match original {
318                    Some(original) => format!(
319                        "cannot drop {} because other objects depend on it",
320                        describe_or_address(describe, original)?
321                    ),
322                    None => {
323                        "cannot drop desired object(s) because other objects depend on them".into()
324                    }
325                },
326                detail: Some(detail),
327                hint: Some("Use DROP ... CASCADE to drop the dependent objects too.".into()),
328            });
329        }
330        Ok(match reported {
331            0 => None,
332            1 => Some(CascadeNotice {
333                message: detail,
334                detail: None,
335            }),
336            _ => {
337                let total = reported + unreported;
338                Some(CascadeNotice {
339                    message: format!(
340                        "drop cascades to {total} other object{}",
341                        if total == 1 { "" } else { "s" }
342                    ),
343                    detail: Some(detail),
344                })
345            }
346        })
347    }
348}
349
350/// `stack_address_present_add_flags`: whether the object, or the whole relation of a column, is being visited by an outer level of the search. Merging flags into the entry lets an inner level mark the object it redirected from.
351fn stack_present_add_flags(
352    object: ObjectAddress,
353    flags: DeletionFlags,
354    stack: &mut [StackEntry],
355) -> bool {
356    let mut present = false;
357    for entry in stack.iter_mut() {
358        if !entry.object.same_object(object) {
359            continue;
360        }
361        if entry.object.sub_id == object.sub_id {
362            entry.flags = entry.flags.union(flags);
363            present = true;
364        } else if entry.object.sub_id == 0 {
365            present = true;
366        } else if object.sub_id == 0 && flags != DeletionFlags::default() {
367            entry.flags = entry.flags.union(flags).union(DeletionFlags::SUBOBJECT);
368        }
369    }
370    present
371}
372
373fn describe_or_address(
374    describe: super::Describe<'_>,
375    object: ObjectAddress,
376) -> Result<String, SQLError> {
377    Ok(describe(object)?.unwrap_or_else(|| {
378        format!(
379            "object {} of class {} column {}",
380            object.object_id, object.class_id, object.sub_id
381        )
382    }))
383}
384
385#[cfg(test)]
386mod tests;