Skip to main content

pgmt/catalog/raw/
shared.rs

1//! Standalone fetches for the catalog state that every object kind needs.
2//!
3//! Each of these is one query whose result is consulted by lookup instead of
4//! being re-joined into every per-kind query: the namespace map (OID → schema
5//! name), the `deptype = 'e'` extension-ownership edges, and the
6//! `pg_description` rows.
7//!
8//! These queries must run on the same connection as the rest of a catalog load:
9//! `pg_get_function_identity_arguments()` renders type names relative to
10//! `search_path`, so a shared fetch on a different connection could resolve
11//! names the per-kind fetches would not.
12
13use anyhow::Result;
14use sqlx::postgres::PgConnection;
15use sqlx::postgres::types::Oid;
16use std::collections::BTreeMap;
17use tracing::info;
18
19use crate::catalog::id::DbObjectId;
20use crate::catalog::utils::resolve_type_dependency;
21
22/// Names of the `pg_catalog` tables an OID can be addressed through.
23///
24/// An OID identifies a row within one catalog table, so both `pg_depend`
25/// (`classid`) and `pg_description` (`classoid`) qualify their OIDs by catalog
26/// table. Lookups here take the same qualification.
27pub mod class {
28    pub const PG_CLASS: &str = "pg_class";
29    pub const PG_PROC: &str = "pg_proc";
30    pub const PG_TYPE: &str = "pg_type";
31    pub const PG_NAMESPACE: &str = "pg_namespace";
32    pub const PG_OPERATOR: &str = "pg_operator";
33    pub const PG_CAST: &str = "pg_cast";
34    pub const PG_CONSTRAINT: &str = "pg_constraint";
35    pub const PG_OPCLASS: &str = "pg_opclass";
36    pub const PG_TRIGGER: &str = "pg_trigger";
37    pub const PG_POLICY: &str = "pg_policy";
38    pub const PG_EXTENSION: &str = "pg_extension";
39    pub const PG_COLLATION: &str = "pg_collation";
40
41    /// The catalog tables above, the only ones anything here is addressed
42    /// through.
43    pub const ALL: [&str; 12] = [
44        PG_CLASS,
45        PG_PROC,
46        PG_TYPE,
47        PG_NAMESPACE,
48        PG_OPERATOR,
49        PG_CAST,
50        PG_CONSTRAINT,
51        PG_OPCLASS,
52        PG_TRIGGER,
53        PG_POLICY,
54        PG_EXTENSION,
55        PG_COLLATION,
56    ];
57
58    /// The constant naming this catalog table, for a name that arrives as a
59    /// `relname` string from `pg_depend` or `pg_description`.
60    ///
61    /// The shared maps key on the constants rather than on owned strings, so a
62    /// lookup allocates nothing. A row addressed through some other catalog
63    /// table (a comment on a language, an extension owning a text-search
64    /// configuration) has no constant and is not interned: nothing here looks it
65    /// up.
66    pub fn intern(name: &str) -> Option<&'static str> {
67        ALL.into_iter().find(|known| *known == name)
68    }
69}
70
71/// Every namespace in the database, by OID.
72///
73/// Unfiltered on purpose: this is the resolution table a converter uses to turn
74/// `relnamespace`/`typnamespace`/… into a schema name. Deciding that a schema
75/// is out of scope is a separate, name-based step.
76#[derive(Debug, Clone, Default)]
77pub struct NamespaceMap {
78    by_oid: BTreeMap<u32, String>,
79}
80
81impl NamespaceMap {
82    /// A map of the given `(oid, name)` pairs.
83    #[allow(dead_code)]
84    pub fn from_pairs(pairs: impl IntoIterator<Item = (Oid, String)>) -> Self {
85        Self {
86            by_oid: pairs.into_iter().map(|(oid, name)| (oid.0, name)).collect(),
87        }
88    }
89
90    pub fn name(&self, oid: Oid) -> Option<&str> {
91        self.by_oid.get(&oid.0).map(String::as_str)
92    }
93
94    #[allow(dead_code)]
95    pub fn len(&self) -> usize {
96        self.by_oid.len()
97    }
98
99    /// Nothing calls this; a public `len` without it is a clippy lint.
100    #[allow(dead_code)]
101    pub fn is_empty(&self) -> bool {
102        self.by_oid.is_empty()
103    }
104
105    pub fn iter(&self) -> impl Iterator<Item = (Oid, &str)> {
106        self.by_oid
107            .iter()
108            .map(|(oid, name)| (Oid(*oid), name.as_str()))
109    }
110}
111
112/// Which objects belong to an extension, from the `pg_depend` rows with
113/// `deptype = 'e'`.
114#[derive(Debug, Clone, Default)]
115pub struct ExtensionOwnership {
116    /// (catalog table, object OID) → owning extension name.
117    owners: BTreeMap<(&'static str, u32), String>,
118}
119
120impl ExtensionOwnership {
121    /// The extension owning this object, for object classes that carry their own
122    /// `deptype = 'e'` row: tables, views, functions, types, sequences,
123    /// operators, casts — every first-class object.
124    pub fn owner(&self, class: &'static str, oid: Oid) -> Option<&str> {
125        self.owners.get(&(class, oid.0)).map(String::as_str)
126    }
127
128    #[allow(dead_code)]
129    pub fn is_owned(&self, class: &'static str, oid: Oid) -> bool {
130        self.owner(class, oid).is_some()
131    }
132
133    /// The extension owning a sub-object of a relation — a constraint, index,
134    /// trigger, or policy — given the OID of its parent table.
135    ///
136    /// Such sub-objects never get a `deptype = 'e'` row of their own, even when
137    /// an extension script created them: membership is recorded only on the
138    /// parent table. Asking about the sub-object's own OID always answers "not
139    /// owned" and leaks the object.
140    pub fn owner_of_relation_subobject(&self, parent_relation: Oid) -> Option<&str> {
141        self.owner(class::PG_CLASS, parent_relation)
142    }
143
144    #[allow(dead_code)]
145    pub fn is_relation_subobject_owned(&self, parent_relation: Oid) -> bool {
146        self.owner_of_relation_subobject(parent_relation).is_some()
147    }
148}
149
150/// Comments, keyed the way `pg_description` keys them:
151/// `(classoid, objoid, objsubid)`, where a non-zero `objsubid` addresses a
152/// sub-object (a column, by attnum).
153#[derive(Debug, Clone, Default)]
154pub struct Descriptions {
155    by_key: BTreeMap<(&'static str, u32, i32), String>,
156}
157
158impl Descriptions {
159    /// The comment on an object itself (`objsubid = 0`).
160    pub fn object(&self, class: &'static str, oid: Oid) -> Option<&str> {
161        self.get(class, oid, 0)
162    }
163
164    pub fn get(&self, class: &'static str, oid: Oid, objsubid: i32) -> Option<&str> {
165        self.by_key
166            .get(&(class, oid.0, objsubid))
167            .map(String::as_str)
168    }
169
170    /// Every sub-object comment on this object, as `(objsubid, comment)` in
171    /// ascending `objsubid` order. For a relation the `objsubid` is the column's
172    /// attnum.
173    pub fn subobjects(&self, class: &'static str, oid: Oid) -> impl Iterator<Item = (i32, &str)> {
174        self.by_key
175            .range((class, oid.0, 1)..=(class, oid.0, i32::MAX))
176            .map(|((_, _, subid), text)| (*subid, text.as_str()))
177    }
178}
179
180/// One `pg_type` row, as far as classifying a reference to it requires.
181#[derive(Debug, Clone)]
182pub struct TypeEntry {
183    pub oid: Oid,
184    pub namespace: Oid,
185    pub name: String,
186    /// `pg_type.typtype`: 'd' domain, 'c' composite, 'e' enum, 'r' range, …
187    pub typtype: String,
188    /// `pg_type.typelem`: non-zero when the type has an element type.
189    pub typelem: Oid,
190    /// `relkind` of `typrelid`, present only for composite types, and what
191    /// distinguishes a table's row type from a view's from a standalone
192    /// `CREATE TYPE ... AS`.
193    pub relkind: Option<String>,
194}
195
196/// Every type in the database, by OID.
197///
198/// This is the classification table a converter resolves `atttypid`,
199/// `prorettype`, `oprleft`, … against: an OID reference to a type says nothing
200/// about whether the target is a domain, an enum, a table's row type, or an
201/// array of any of those, and that distinction decides which dependency the
202/// referring object gets.
203#[derive(Debug, Clone, Default)]
204pub struct TypeMap {
205    by_oid: BTreeMap<u32, TypeEntry>,
206}
207
208impl TypeMap {
209    pub fn get(&self, oid: Oid) -> Option<&TypeEntry> {
210        self.by_oid.get(&oid.0)
211    }
212
213    /// The type a reference actually depends on: for an array, its element
214    /// type; otherwise the type itself.
215    ///
216    /// Arrays are detected through `typelem`, never by the leading underscore in
217    /// the array type's name — `_internal_status` is a legitimate type name and
218    /// stripping the prefix invents a type that does not exist.
219    pub fn element_or_self(&self, oid: Oid) -> Option<&TypeEntry> {
220        let entry = self.get(oid)?;
221        if entry.typelem.0 != 0 {
222            // A type whose element type is missing from the map cannot occur
223            // (typelem references pg_type), but fall back to the array type
224            // rather than dropping the reference entirely.
225            return self.get(entry.typelem).or(Some(entry));
226        }
227        Some(entry)
228    }
229}
230
231/// A type reference resolved into everything a converter needs to name it and
232/// to decide what depends on what: array references are already resolved to
233/// their element type, and the schema and owning extension are looked up.
234#[derive(Debug, Clone, Copy)]
235pub struct ResolvedType<'a> {
236    pub schema: Option<&'a str>,
237    pub name: &'a str,
238    pub typtype: &'a str,
239    pub relkind: Option<&'a str>,
240    /// The extension providing this type, if any.
241    pub extension: Option<&'a str>,
242    /// The reference was to an array of this type.
243    pub is_array: bool,
244}
245
246impl ResolvedType<'_> {
247    /// The dependency a reference to this type creates: the extension for an
248    /// extension-provided type, the domain/table/view/type otherwise, and
249    /// nothing for a built-in.
250    pub fn dependency(&self) -> Option<DbObjectId> {
251        resolve_type_dependency(
252            self.schema,
253            Some(self.name),
254            Some(self.typtype),
255            self.relkind,
256            self.extension.is_some(),
257            self.extension,
258        )
259    }
260}
261
262/// The cross-cutting catalog state, fetched once per catalog load.
263#[derive(Debug, Clone, Default)]
264pub struct SharedCatalog {
265    pub namespaces: NamespaceMap,
266    pub extensions: ExtensionOwnership,
267    pub descriptions: Descriptions,
268    pub types: TypeMap,
269}
270
271impl SharedCatalog {
272    /// Resolve a type reference (an `atttypid`, `prorettype`, `oprleft`, …)
273    /// through the array indirection, the namespace map, and the extension
274    /// ownership edges in one step.
275    pub fn resolve_type(&self, oid: Oid) -> Option<ResolvedType<'_>> {
276        let entry = self.types.element_or_self(oid)?;
277        Some(ResolvedType {
278            schema: self.namespaces.name(entry.namespace),
279            name: &entry.name,
280            typtype: &entry.typtype,
281            relkind: entry.relkind.as_deref(),
282            extension: self.extensions.owner(class::PG_TYPE, entry.oid),
283            is_array: entry.oid != oid,
284        })
285    }
286}
287
288/// Fetch all shared state on one connection.
289///
290/// Must be the same connection (and therefore the same `search_path`) the
291/// per-kind fetches use.
292pub async fn fetch(conn: &mut PgConnection) -> Result<SharedCatalog> {
293    info!("Fetching shared catalog state...");
294    let namespaces = fetch_namespaces(&mut *conn).await?;
295    let extensions = fetch_extension_ownership(&mut *conn).await?;
296    let descriptions = fetch_descriptions(&mut *conn).await?;
297    let types = fetch_types(&mut *conn).await?;
298
299    Ok(SharedCatalog {
300        namespaces,
301        extensions,
302        descriptions,
303        types,
304    })
305}
306
307pub async fn fetch_namespaces(conn: &mut PgConnection) -> Result<NamespaceMap> {
308    let rows = sqlx::query!(
309        r#"
310        SELECT
311            n.oid AS "oid!",
312            n.nspname AS "name!"
313        FROM pg_namespace n
314        ORDER BY n.oid
315        "#
316    )
317    .fetch_all(&mut *conn)
318    .await?;
319
320    Ok(NamespaceMap {
321        by_oid: rows.into_iter().map(|row| (row.oid.0, row.name)).collect(),
322    })
323}
324
325pub async fn fetch_extension_ownership(conn: &mut PgConnection) -> Result<ExtensionOwnership> {
326    let rows = sqlx::query!(
327        r#"
328        SELECT
329            cl.relname AS "class_name!",
330            dep.objid AS "objid!",
331            e.extname AS "extension!"
332        FROM pg_depend dep
333        JOIN pg_class cl ON cl.oid = dep.classid
334        JOIN pg_extension e ON e.oid = dep.refobjid
335        WHERE dep.deptype = 'e'
336          AND dep.refclassid = 'pg_extension'::regclass
337        ORDER BY cl.relname, dep.objid
338        "#
339    )
340    .fetch_all(&mut *conn)
341    .await?;
342
343    Ok(ExtensionOwnership {
344        owners: rows
345            .into_iter()
346            .filter_map(|row| {
347                let class = class::intern(&row.class_name)?;
348                Some(((class, row.objid.0), row.extension))
349            })
350            .collect(),
351    })
352}
353
354pub async fn fetch_descriptions(conn: &mut PgConnection) -> Result<Descriptions> {
355    // Comments on pinned system-catalog objects are never pgmt's to manage, and
356    // there are thousands of them. Every object created after initdb — user
357    // objects and extension members alike — is allocated an OID at or above
358    // FirstNormalObjectId (16384), so that bound separates them.
359    //
360    // Namespaces are the exception: `public` is created by initdb with a pinned
361    // OID, yet its comment is a user's to set and pgmt's to manage, so every
362    // `pg_namespace` comment is kept regardless of OID. The handful of extra
363    // rows belong to schemas that are excluded as system schemas anyway.
364    let rows = sqlx::query!(
365        r#"
366        SELECT
367            cl.relname AS "class_name!",
368            d.objoid AS "objoid!",
369            d.objsubid AS "objsubid!",
370            d.description AS "description!"
371        FROM pg_description d
372        JOIN pg_class cl ON cl.oid = d.classoid
373        WHERE d.objoid >= 16384
374           OR d.classoid = 'pg_namespace'::regclass
375        ORDER BY cl.relname, d.objoid, d.objsubid
376        "#
377    )
378    .fetch_all(&mut *conn)
379    .await?;
380
381    Ok(Descriptions {
382        by_key: rows
383            .into_iter()
384            .filter_map(|row| {
385                let class = class::intern(&row.class_name)?;
386                Some(((class, row.objoid.0, row.objsubid), row.description))
387            })
388            .collect(),
389    })
390}
391
392pub async fn fetch_types(conn: &mut PgConnection) -> Result<TypeMap> {
393    let rows = sqlx::query!(
394        r#"
395        SELECT
396            t.oid AS "oid!",
397            t.typnamespace AS "namespace!",
398            t.typname AS "name!",
399            t.typtype::text AS "typtype!",
400            t.typelem AS "typelem!",
401            rel.relkind::text AS "relkind?"
402        FROM pg_type t
403        LEFT JOIN pg_class rel ON rel.oid = t.typrelid AND t.typrelid != 0
404        ORDER BY t.oid
405        "#
406    )
407    .fetch_all(&mut *conn)
408    .await?;
409
410    Ok(TypeMap {
411        by_oid: rows
412            .into_iter()
413            .map(|row| {
414                (
415                    row.oid.0,
416                    TypeEntry {
417                        oid: row.oid,
418                        namespace: row.namespace,
419                        name: row.name,
420                        typtype: row.typtype,
421                        typelem: row.typelem,
422                        relkind: row.relkind,
423                    },
424                )
425            })
426            .collect(),
427    })
428}
429
430#[cfg(test)]
431mod tests {
432    use super::class;
433
434    /// The shared maps can only be looked up through the class constants, so a
435    /// constant that the interning misses would silently make every lookup of
436    /// that catalog table answer "absent".
437    #[test]
438    fn test_interning_covers_every_class_constant() {
439        for name in class::ALL {
440            assert_eq!(class::intern(name), Some(name));
441        }
442        // A catalog table nothing here is addressed through stays uninterned.
443        assert_eq!(class::intern("pg_language"), None);
444    }
445}