Skip to main content

uqa_sql/catalog/dependencies/
shared.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! One `pg_shdepend` row, and the detail `DROP ROLE` reports from the rows that name a role, as `checkSharedDependencies` builds it.
8
9use super::ObjectAddress;
10use std::cmp::Ordering;
11use std::fmt::Write as _;
12
13/// `pg_shdepend.deptype`.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum SharedDependencyKind {
16    /// `o`: the referenced role owns the object.
17    Owner,
18    /// `a`: the object's privileges name the referenced role as a grantee or grantor.
19    Acl,
20    /// `i`: the object's initial privileges name the referenced role.
21    InitialAcl,
22    /// `r`: a row-level security policy applies to the referenced role.
23    Policy,
24    /// `t`: the object's storage is in the referenced tablespace.
25    Tablespace,
26}
27
28impl SharedDependencyKind {
29    /// The `pg_shdepend.deptype` letter.
30    #[must_use]
31    pub const fn code(self) -> char {
32        match self {
33            Self::Owner => 'o',
34            Self::Acl => 'a',
35            Self::InitialAcl => 'i',
36            Self::Policy => 'r',
37            Self::Tablespace => 't',
38        }
39    }
40
41    fn describe(self, object: &str) -> String {
42        match self {
43            Self::Owner => format!("owner of {object}"),
44            Self::Acl => format!("privileges for {object}"),
45            Self::InitialAcl => format!("initial privileges for {object}"),
46            Self::Policy => format!("target of {object}"),
47            Self::Tablespace => format!("tablespace for {object}"),
48        }
49    }
50}
51
52/// A dependency of an object on a role or tablespace, which every database shares.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54pub struct SharedDependency {
55    /// `dbid`: the database holding the dependent object, or zero for an object that every database shares.
56    pub database: u32,
57    pub dependent: ObjectAddress,
58    pub referenced: ObjectAddress,
59    pub kind: SharedDependencyKind,
60}
61
62/// `MAX_REPORTED_DEPS`: the detail lists at most this many objects.
63const MAX_REPORTED_DEPENDENTS: usize = 100;
64
65/// `checkSharedDependencies`: the detail listing what depends on `referenced`, by ascending OID, catalog, column and dependency type; `None` when nothing that can be described depends on it. Every row must belong to the current database or be shared.
66pub fn shared_dependency_detail<'a>(
67    dependencies: impl IntoIterator<Item = &'a SharedDependency>,
68    referenced: ObjectAddress,
69    describe: super::Describe<'_>,
70) -> Result<Option<String>, crate::SQLError> {
71    let mut dependents = dependencies
72        .into_iter()
73        .filter(|dependency| dependency.referenced == referenced)
74        .map(|dependency| (dependency.dependent, dependency.kind))
75        .collect::<Vec<_>>();
76    dependents.sort_by(|(left, left_kind), (right, right_kind)| {
77        shared_dependency_order(left, right).then(left_kind.code().cmp(&right_kind.code()))
78    });
79    let mut lines = Vec::new();
80    let mut unreported = 0_usize;
81    for (index, (object, kind)) in dependents.into_iter().enumerate() {
82        if index >= MAX_REPORTED_DEPENDENTS {
83            unreported += 1;
84            continue;
85        }
86        // An object that cannot be described is skipped but still counts toward the reported limit.
87        if let Some(description) = describe(object)? {
88            lines.push(kind.describe(&description));
89        }
90    }
91    if lines.is_empty() {
92        return Ok(None);
93    }
94    let mut detail = lines.join("\n");
95    if unreported > 0 {
96        write!(
97            detail,
98            "\nand {unreported} other object{} (see server log for list)",
99            if unreported == 1 { "" } else { "s" }
100        )
101        .expect("writing to a String cannot fail");
102    }
103    Ok(Some(detail))
104}
105
106/// `shared_dependency_comparator`: ascending OID, then catalog, then column with the whole object first.
107fn shared_dependency_order(left: &ObjectAddress, right: &ObjectAddress) -> Ordering {
108    left.object_id
109        .cmp(&right.object_id)
110        .then(left.class_id.cmp(&right.class_id))
111        .then(
112            left.sub_id
113                .cast_unsigned()
114                .cmp(&right.sub_id.cast_unsigned()),
115        )
116}
117
118#[cfg(test)]
119mod tests;