use anyhow::Result;
use sqlx::postgres::PgConnection;
use sqlx::postgres::types::Oid;
use std::collections::BTreeMap;
use tracing::info;
use crate::catalog::id::DbObjectId;
use crate::catalog::utils::resolve_type_dependency;
pub mod class {
pub const PG_CLASS: &str = "pg_class";
pub const PG_PROC: &str = "pg_proc";
pub const PG_TYPE: &str = "pg_type";
pub const PG_NAMESPACE: &str = "pg_namespace";
pub const PG_OPERATOR: &str = "pg_operator";
pub const PG_CAST: &str = "pg_cast";
pub const PG_CONSTRAINT: &str = "pg_constraint";
pub const PG_OPCLASS: &str = "pg_opclass";
pub const PG_TRIGGER: &str = "pg_trigger";
pub const PG_POLICY: &str = "pg_policy";
pub const PG_EXTENSION: &str = "pg_extension";
pub const PG_COLLATION: &str = "pg_collation";
pub const ALL: [&str; 12] = [
PG_CLASS,
PG_PROC,
PG_TYPE,
PG_NAMESPACE,
PG_OPERATOR,
PG_CAST,
PG_CONSTRAINT,
PG_OPCLASS,
PG_TRIGGER,
PG_POLICY,
PG_EXTENSION,
PG_COLLATION,
];
pub fn intern(name: &str) -> Option<&'static str> {
ALL.into_iter().find(|known| *known == name)
}
}
#[derive(Debug, Clone, Default)]
pub struct NamespaceMap {
by_oid: BTreeMap<u32, String>,
}
impl NamespaceMap {
#[allow(dead_code)]
pub fn from_pairs(pairs: impl IntoIterator<Item = (Oid, String)>) -> Self {
Self {
by_oid: pairs.into_iter().map(|(oid, name)| (oid.0, name)).collect(),
}
}
pub fn name(&self, oid: Oid) -> Option<&str> {
self.by_oid.get(&oid.0).map(String::as_str)
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.by_oid.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.by_oid.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (Oid, &str)> {
self.by_oid
.iter()
.map(|(oid, name)| (Oid(*oid), name.as_str()))
}
}
#[derive(Debug, Clone, Default)]
pub struct ExtensionOwnership {
owners: BTreeMap<(&'static str, u32), String>,
}
impl ExtensionOwnership {
pub fn owner(&self, class: &'static str, oid: Oid) -> Option<&str> {
self.owners.get(&(class, oid.0)).map(String::as_str)
}
#[allow(dead_code)]
pub fn is_owned(&self, class: &'static str, oid: Oid) -> bool {
self.owner(class, oid).is_some()
}
pub fn owner_of_relation_subobject(&self, parent_relation: Oid) -> Option<&str> {
self.owner(class::PG_CLASS, parent_relation)
}
#[allow(dead_code)]
pub fn is_relation_subobject_owned(&self, parent_relation: Oid) -> bool {
self.owner_of_relation_subobject(parent_relation).is_some()
}
}
#[derive(Debug, Clone, Default)]
pub struct Descriptions {
by_key: BTreeMap<(&'static str, u32, i32), String>,
}
impl Descriptions {
pub fn object(&self, class: &'static str, oid: Oid) -> Option<&str> {
self.get(class, oid, 0)
}
pub fn get(&self, class: &'static str, oid: Oid, objsubid: i32) -> Option<&str> {
self.by_key
.get(&(class, oid.0, objsubid))
.map(String::as_str)
}
pub fn subobjects(&self, class: &'static str, oid: Oid) -> impl Iterator<Item = (i32, &str)> {
self.by_key
.range((class, oid.0, 1)..=(class, oid.0, i32::MAX))
.map(|((_, _, subid), text)| (*subid, text.as_str()))
}
}
#[derive(Debug, Clone)]
pub struct TypeEntry {
pub oid: Oid,
pub namespace: Oid,
pub name: String,
pub typtype: String,
pub typelem: Oid,
pub relkind: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct TypeMap {
by_oid: BTreeMap<u32, TypeEntry>,
}
impl TypeMap {
pub fn get(&self, oid: Oid) -> Option<&TypeEntry> {
self.by_oid.get(&oid.0)
}
pub fn element_or_self(&self, oid: Oid) -> Option<&TypeEntry> {
let entry = self.get(oid)?;
if entry.typelem.0 != 0 {
return self.get(entry.typelem).or(Some(entry));
}
Some(entry)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ResolvedType<'a> {
pub schema: Option<&'a str>,
pub name: &'a str,
pub typtype: &'a str,
pub relkind: Option<&'a str>,
pub extension: Option<&'a str>,
pub is_array: bool,
}
impl ResolvedType<'_> {
pub fn dependency(&self) -> Option<DbObjectId> {
resolve_type_dependency(
self.schema,
Some(self.name),
Some(self.typtype),
self.relkind,
self.extension.is_some(),
self.extension,
)
}
}
#[derive(Debug, Clone, Default)]
pub struct SharedCatalog {
pub namespaces: NamespaceMap,
pub extensions: ExtensionOwnership,
pub descriptions: Descriptions,
pub types: TypeMap,
}
impl SharedCatalog {
pub fn resolve_type(&self, oid: Oid) -> Option<ResolvedType<'_>> {
let entry = self.types.element_or_self(oid)?;
Some(ResolvedType {
schema: self.namespaces.name(entry.namespace),
name: &entry.name,
typtype: &entry.typtype,
relkind: entry.relkind.as_deref(),
extension: self.extensions.owner(class::PG_TYPE, entry.oid),
is_array: entry.oid != oid,
})
}
}
pub async fn fetch(conn: &mut PgConnection) -> Result<SharedCatalog> {
info!("Fetching shared catalog state...");
let namespaces = fetch_namespaces(&mut *conn).await?;
let extensions = fetch_extension_ownership(&mut *conn).await?;
let descriptions = fetch_descriptions(&mut *conn).await?;
let types = fetch_types(&mut *conn).await?;
Ok(SharedCatalog {
namespaces,
extensions,
descriptions,
types,
})
}
pub async fn fetch_namespaces(conn: &mut PgConnection) -> Result<NamespaceMap> {
let rows = sqlx::query!(
r#"
SELECT
n.oid AS "oid!",
n.nspname AS "name!"
FROM pg_namespace n
ORDER BY n.oid
"#
)
.fetch_all(&mut *conn)
.await?;
Ok(NamespaceMap {
by_oid: rows.into_iter().map(|row| (row.oid.0, row.name)).collect(),
})
}
pub async fn fetch_extension_ownership(conn: &mut PgConnection) -> Result<ExtensionOwnership> {
let rows = sqlx::query!(
r#"
SELECT
cl.relname AS "class_name!",
dep.objid AS "objid!",
e.extname AS "extension!"
FROM pg_depend dep
JOIN pg_class cl ON cl.oid = dep.classid
JOIN pg_extension e ON e.oid = dep.refobjid
WHERE dep.deptype = 'e'
AND dep.refclassid = 'pg_extension'::regclass
ORDER BY cl.relname, dep.objid
"#
)
.fetch_all(&mut *conn)
.await?;
Ok(ExtensionOwnership {
owners: rows
.into_iter()
.filter_map(|row| {
let class = class::intern(&row.class_name)?;
Some(((class, row.objid.0), row.extension))
})
.collect(),
})
}
pub async fn fetch_descriptions(conn: &mut PgConnection) -> Result<Descriptions> {
let rows = sqlx::query!(
r#"
SELECT
cl.relname AS "class_name!",
d.objoid AS "objoid!",
d.objsubid AS "objsubid!",
d.description AS "description!"
FROM pg_description d
JOIN pg_class cl ON cl.oid = d.classoid
WHERE d.objoid >= 16384
OR d.classoid = 'pg_namespace'::regclass
ORDER BY cl.relname, d.objoid, d.objsubid
"#
)
.fetch_all(&mut *conn)
.await?;
Ok(Descriptions {
by_key: rows
.into_iter()
.filter_map(|row| {
let class = class::intern(&row.class_name)?;
Some(((class, row.objoid.0, row.objsubid), row.description))
})
.collect(),
})
}
pub async fn fetch_types(conn: &mut PgConnection) -> Result<TypeMap> {
let rows = sqlx::query!(
r#"
SELECT
t.oid AS "oid!",
t.typnamespace AS "namespace!",
t.typname AS "name!",
t.typtype::text AS "typtype!",
t.typelem AS "typelem!",
rel.relkind::text AS "relkind?"
FROM pg_type t
LEFT JOIN pg_class rel ON rel.oid = t.typrelid AND t.typrelid != 0
ORDER BY t.oid
"#
)
.fetch_all(&mut *conn)
.await?;
Ok(TypeMap {
by_oid: rows
.into_iter()
.map(|row| {
(
row.oid.0,
TypeEntry {
oid: row.oid,
namespace: row.namespace,
name: row.name,
typtype: row.typtype,
typelem: row.typelem,
relkind: row.relkind,
},
)
})
.collect(),
})
}
#[cfg(test)]
mod tests {
use super::class;
#[test]
fn test_interning_covers_every_class_constant() {
for name in class::ALL {
assert_eq!(class::intern(name), Some(name));
}
assert_eq!(class::intern("pg_language"), None);
}
}