1use 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
22pub 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 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 pub fn intern(name: &str) -> Option<&'static str> {
67 ALL.into_iter().find(|known| *known == name)
68 }
69}
70
71#[derive(Debug, Clone, Default)]
77pub struct NamespaceMap {
78 by_oid: BTreeMap<u32, String>,
79}
80
81impl NamespaceMap {
82 #[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 #[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#[derive(Debug, Clone, Default)]
115pub struct ExtensionOwnership {
116 owners: BTreeMap<(&'static str, u32), String>,
118}
119
120impl ExtensionOwnership {
121 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 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#[derive(Debug, Clone, Default)]
154pub struct Descriptions {
155 by_key: BTreeMap<(&'static str, u32, i32), String>,
156}
157
158impl Descriptions {
159 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 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#[derive(Debug, Clone)]
182pub struct TypeEntry {
183 pub oid: Oid,
184 pub namespace: Oid,
185 pub name: String,
186 pub typtype: String,
188 pub typelem: Oid,
190 pub relkind: Option<String>,
194}
195
196#[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 pub fn element_or_self(&self, oid: Oid) -> Option<&TypeEntry> {
220 let entry = self.get(oid)?;
221 if entry.typelem.0 != 0 {
222 return self.get(entry.typelem).or(Some(entry));
226 }
227 Some(entry)
228 }
229}
230
231#[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 pub extension: Option<&'a str>,
242 pub is_array: bool,
244}
245
246impl ResolvedType<'_> {
247 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#[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 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
288pub 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 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 #[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 assert_eq!(class::intern("pg_language"), None);
444 }
445}