use super::exclusion::sql;
use anyhow::Result;
use sqlx::PgPool;
#[derive(sqlx::FromRow)]
pub struct Row {
#[sqlx(rename = "type")]
pub kind: String,
pub schema: Option<String>,
pub name: String,
#[sqlx(rename = "tbl")]
pub table: Option<String>,
pub args: Option<String>,
pub oid: i64,
}
pub async fn fetch(pool: &PgPool) -> Result<Vec<Row>> {
Ok(sqlx::query_as(sqlx::AssertSqlSafe(query()))
.fetch_all(pool)
.await?)
}
pub async fn current_mark(pool: &PgPool) -> Result<i64> {
let mark: Option<i64> = sqlx::query_scalar(sqlx::AssertSqlSafe(marks_query()))
.fetch_one(pool)
.await?;
Ok(mark.unwrap_or(0))
}
const COLUMNS: [&str; 5] = ["type", "schema", "name", "tbl", "args"];
pub struct Branch {
kind: &'static str,
from: String,
oid_catalog: &'static str,
oid: String,
schema: String,
name: String,
table: String,
args: String,
predicates: Vec<String>,
}
impl Branch {
fn new(kind: &'static str, from: &str, oid_catalog: &'static str, oid: &str) -> Self {
Self {
kind,
from: from.to_string(),
oid_catalog,
oid: oid.to_string(),
schema: "NULL".to_string(),
name: "NULL".to_string(),
table: "NULL".to_string(),
args: "NULL".to_string(),
predicates: Vec::new(),
}
}
fn schema(mut self, expr: &str) -> Self {
self.schema = expr.to_string();
self
}
fn name(mut self, expr: &str) -> Self {
self.name = expr.to_string();
self
}
fn table(mut self, expr: &str) -> Self {
self.table = expr.to_string();
self
}
fn args(mut self, expr: &str) -> Self {
self.args = expr.to_string();
self
}
fn filter(mut self, predicate: impl Into<String>) -> Self {
self.predicates.push(predicate.into());
self
}
fn sql(&self) -> String {
let mut projections: Vec<String> = [
format!("'{}'", self.kind),
self.schema.clone(),
self.name.clone(),
self.table.clone(),
self.args.clone(),
]
.iter()
.zip(COLUMNS)
.map(|(expr, column)| format!(" ({expr})::text AS \"{column}\""))
.collect();
projections.push(format!(" ({})::int8 AS \"oid\"", self.oid));
let mut sql = format!("SELECT\n{}\nFROM {}", projections.join(",\n"), self.from);
if !self.predicates.is_empty() {
let conditions: Vec<String> = self
.predicates
.iter()
.map(|predicate| format!("({predicate})"))
.collect();
sql.push_str(&format!("\nWHERE {}", conditions.join("\n AND ")));
}
sql
}
}
pub fn query() -> String {
branches()
.iter()
.map(Branch::sql)
.collect::<Vec<_>>()
.join("\n\nUNION ALL\n\n")
}
pub fn marks_query() -> String {
let mut catalogs: Vec<&'static str> = branches().iter().map(|b| b.oid_catalog).collect();
catalogs.sort_unstable();
catalogs.dedup();
let maxima: Vec<String> = catalogs
.iter()
.map(|catalog| format!(" SELECT max(oid) AS m FROM {catalog}"))
.collect();
format!(
"SELECT max(m)::int8 AS mark FROM (\n{}\n) s",
maxima.join("\n UNION ALL\n")
)
}
pub fn branches() -> Vec<Branch> {
vec![
Branch::new("schema", "pg_namespace n", "pg_namespace", "n.oid")
.name("n.nspname")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter("n.nspname <> 'public'"),
Branch::new(
"table",
"pg_class c\n JOIN pg_namespace n ON c.relnamespace = n.oid",
"pg_class",
"c.oid",
)
.schema("n.nspname")
.name("c.relname")
.filter("c.relkind = 'r'")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::not_extension_owned("pg_class", "c.oid")),
Branch::new(
"view",
"pg_class c\n JOIN pg_namespace n ON c.relnamespace = n.oid",
"pg_class",
"c.oid",
)
.schema("n.nspname")
.name("c.relname")
.filter("c.relkind = 'v'")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::not_extension_owned("pg_class", "c.oid")),
Branch::new(
"sequence",
"pg_class c\n JOIN pg_namespace n ON c.relnamespace = n.oid",
"pg_class",
"c.oid",
)
.schema("n.nspname")
.name("c.relname")
.filter("c.relkind = 'S'")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::not_extension_owned("pg_class", "c.oid"))
.filter(sql::not_an_identity_sequence("c.oid")),
Branch::new(
"index",
"pg_index idx\n \
JOIN pg_class i ON idx.indexrelid = i.oid\n \
JOIN pg_namespace n ON i.relnamespace = n.oid\n \
JOIN pg_class t ON idx.indrelid = t.oid\n \
JOIN pg_namespace tn ON t.relnamespace = tn.oid",
"pg_class",
"i.oid",
)
.schema("n.nspname")
.name("i.relname")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::not_a_system_namespace("tn.nspname"))
.filter(sql::not_extension_owned("pg_class", "i.oid"))
.filter(sql::parent_relation_not_extension_owned("t.oid"))
.filter(sql::not_a_constraint_backing_index("i.oid")),
Branch::new(
"function",
"pg_proc p\n JOIN pg_namespace n ON p.pronamespace = n.oid",
"pg_proc",
"p.oid",
)
.schema("n.nspname")
.name("p.proname")
.args("pg_catalog.pg_get_function_identity_arguments(p.oid)")
.filter("p.prokind NOT IN ('a', 'p')")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::not_extension_owned("pg_proc", "p.oid")),
Branch::new(
"procedure",
"pg_proc p\n JOIN pg_namespace n ON p.pronamespace = n.oid",
"pg_proc",
"p.oid",
)
.schema("n.nspname")
.name("p.proname")
.args("pg_catalog.pg_get_function_identity_arguments(p.oid)")
.filter("p.prokind = 'p'")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::not_extension_owned("pg_proc", "p.oid")),
Branch::new(
"aggregate",
"pg_proc p\n JOIN pg_namespace n ON p.pronamespace = n.oid",
"pg_proc",
"p.oid",
)
.schema("n.nspname")
.name("p.proname")
.args("pg_catalog.pg_get_function_identity_arguments(p.oid)")
.filter("p.prokind = 'a'")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::not_extension_owned("pg_proc", "p.oid")),
Branch::new(
"type",
"pg_type t\n JOIN pg_namespace n ON t.typnamespace = n.oid",
"pg_type",
"t.oid",
)
.schema("n.nspname")
.name("t.typname")
.filter("t.typtype IN ('e', 'c', 'r')")
.filter(
"NOT EXISTS (\n SELECT 1 FROM pg_class c\n \
WHERE c.reltype = t.oid\n \
AND c.relkind != 'c'\n)",
)
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::not_extension_owned("pg_type", "t.oid")),
Branch::new(
"domain",
"pg_type t\n JOIN pg_namespace n ON t.typnamespace = n.oid",
"pg_type",
"t.oid",
)
.schema("n.nspname")
.name("t.typname")
.filter("t.typtype = 'd'")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::not_extension_owned("pg_type", "t.oid")),
Branch::new(
"collation",
"pg_collation c\n JOIN pg_namespace n ON c.collnamespace = n.oid",
"pg_collation",
"c.oid",
)
.schema("n.nspname")
.name("c.collname")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::not_extension_owned("pg_collation", "c.oid")),
Branch::new(
"constraint",
"pg_constraint co\n \
JOIN pg_class cl ON co.conrelid = cl.oid\n \
JOIN pg_namespace n ON cl.relnamespace = n.oid",
"pg_constraint",
"co.oid",
)
.schema("n.nspname")
.name("co.conname")
.table("cl.relname")
.filter("cl.relkind = 'r'")
.filter("co.contype IN ('u', 'f', 'c', 'x')")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::parent_relation_not_extension_owned("cl.oid")),
Branch::new(
"trigger",
"pg_trigger tg\n \
JOIN pg_class c ON tg.tgrelid = c.oid\n \
JOIN pg_namespace n ON c.relnamespace = n.oid",
"pg_trigger",
"tg.oid",
)
.schema("n.nspname")
.name("tg.tgname")
.table("c.relname")
.filter("c.relkind IN ('r', 'v', 'm')")
.filter("NOT tg.tgisinternal")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::parent_relation_not_extension_owned("c.oid")),
Branch::new(
"policy",
"pg_policy pol\n \
JOIN pg_class c ON pol.polrelid = c.oid\n \
JOIN pg_namespace n ON c.relnamespace = n.oid",
"pg_policy",
"pol.oid",
)
.schema("n.nspname")
.name("pol.polname")
.table("c.relname")
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::parent_relation_not_extension_owned("c.oid")),
Branch::new(
"operator",
"pg_operator o\n JOIN pg_namespace n ON o.oprnamespace = n.oid",
"pg_operator",
"o.oid",
)
.schema("n.nspname")
.name("o.oprname")
.args(
"CASE WHEN o.oprleft = 0 THEN 'NONE' \
ELSE pg_catalog.format_type(o.oprleft, NULL) END\n \
|| ', '\n \
|| CASE WHEN o.oprright = 0 THEN 'NONE' \
ELSE pg_catalog.format_type(o.oprright, NULL) END",
)
.filter(sql::not_a_system_namespace("n.nspname"))
.filter(sql::not_extension_owned("pg_operator", "o.oid")),
Branch::new(
"cast",
"pg_cast ca\n \
JOIN pg_type st ON ca.castsource = st.oid\n \
JOIN pg_type ste ON ste.oid = COALESCE(NULLIF(st.typelem, 0), st.oid)\n \
JOIN pg_namespace stn ON ste.typnamespace = stn.oid\n \
JOIN pg_type tt ON ca.casttarget = tt.oid\n \
JOIN pg_type tte ON tte.oid = COALESCE(NULLIF(tt.typelem, 0), tt.oid)\n \
JOIN pg_namespace ttn ON tte.typnamespace = ttn.oid",
"pg_cast",
"ca.oid",
)
.name("pg_catalog.format_type(ca.castsource, NULL)")
.table("pg_catalog.format_type(ca.casttarget, NULL)")
.filter(format!(
"{} OR {}",
sql::not_a_system_namespace("stn.nspname"),
sql::not_a_system_namespace("ttn.nspname")
))
.filter(sql::not_extension_owned("pg_cast", "ca.oid")),
Branch::new("extension", "pg_extension e", "pg_extension", "e.oid")
.name("e.extname")
.filter(sql::not_a_built_in_extension("e.extname")),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_every_branch_projects_the_identity_columns() {
for branch in branches() {
let sql = branch.sql();
for column in COLUMNS {
assert!(
sql.contains(&format!("AS \"{column}\"")),
"branch {} does not project {column}:\n{sql}",
branch.kind
);
}
}
}
#[test]
fn test_every_branch_projects_its_oid() {
for branch in branches() {
assert!(
branch.sql().contains("AS \"oid\""),
"branch {} does not project its oid",
branch.kind
);
}
}
#[test]
fn test_declared_oid_catalog_matches_the_expression() {
fn aliases(from: &str) -> std::collections::BTreeMap<&str, &str> {
let tokens: Vec<&str> = from.split_whitespace().collect();
let mut bound = std::collections::BTreeMap::new();
if tokens.len() >= 2 {
bound.insert(tokens[1], tokens[0]);
}
for (i, token) in tokens.iter().enumerate() {
if token.eq_ignore_ascii_case("JOIN") && i + 2 < tokens.len() {
bound.insert(tokens[i + 2], tokens[i + 1]);
}
}
bound
}
for branch in branches() {
let alias = branch
.oid
.split_once('.')
.unwrap_or_else(|| panic!("branch {}: oid is not <alias>.oid", branch.kind))
.0;
let bound = aliases(&branch.from);
let reads = bound.get(alias).unwrap_or_else(|| {
panic!(
"branch {}: oid expression {} names alias {alias}, which its FROM does not bind",
branch.kind, branch.oid
)
});
assert_eq!(
*reads, branch.oid_catalog,
"branch {} selects {} (from {reads}) but declares {}",
branch.kind, branch.oid, branch.oid_catalog
);
}
}
#[test]
fn test_query_unions_every_branch() {
let query = query();
assert_eq!(query.matches("UNION ALL").count(), branches().len() - 1);
for branch in branches() {
assert!(
query.contains(&format!("('{}')::text", branch.kind)),
"branch {} is missing from the query",
branch.kind
);
}
}
#[test]
fn test_exclusion_predicates_are_class_qualified() {
let query = query();
for fragment in query.split("dep.objid = ").skip(1) {
assert!(
fragment.contains("dep.classid = '"),
"an extension-ownership test is not class-qualified:\n{fragment}"
);
}
}
}