use anyhow::{Context, Result};
use sqlx::postgres::PgConnection;
use sqlx::postgres::types::Oid;
use std::collections::BTreeMap;
use tracing::info;
use super::dedup_preserving_order;
use super::exclusion::{Converted, Excluded, ExclusionReason, is_system_schema};
use super::oid_index::OidIndex;
use super::reference::RawReference;
use super::shared::{SharedCatalog, class};
use crate::catalog::DependsOn;
use crate::catalog::id::DbObjectId;
use crate::catalog::triggers::Trigger;
#[derive(Debug, Clone)]
pub struct RawTrigger {
pub oid: Oid,
pub name: String,
pub table_oid: Oid,
pub table_namespace: Oid,
pub table_name: String,
pub is_internal: bool,
pub function_namespace: Oid,
pub function_name: String,
pub function_args: String,
pub definition: String,
}
#[derive(Debug, Clone, Default)]
pub struct RawTriggers {
pub triggers: Vec<RawTrigger>,
pub dependencies: Vec<RawReference>,
}
pub async fn fetch(conn: &mut PgConnection) -> Result<RawTriggers> {
Ok(RawTriggers {
triggers: fetch_triggers(&mut *conn).await?,
dependencies: fetch_dependencies(&mut *conn).await?,
})
}
async fn fetch_dependencies(conn: &mut PgConnection) -> Result<Vec<RawReference>> {
let rows = sqlx::query!(
r#"
SELECT DISTINCT
d.objid AS "trigger_oid!",
cl.relname AS "ref_class!",
d.refobjid AS "ref_oid!",
p.pronamespace AS "function_namespace?",
p.proname AS "function_name?",
pg_catalog.pg_get_function_identity_arguments(p.oid) AS "function_args?",
o.oprnamespace AS "operator_namespace?",
o.oprname AS "operator_name?",
NULLIF(pg_catalog.format_type(o.oprleft, NULL), '-') AS "operator_left_type?",
NULLIF(pg_catalog.format_type(o.oprright, NULL), '-') AS "operator_right_type?"
FROM pg_depend d
JOIN pg_class cl ON cl.oid = d.refclassid
LEFT JOIN pg_proc p ON d.refclassid = 'pg_proc'::regclass AND d.refobjid = p.oid
LEFT JOIN pg_operator o ON d.refclassid = 'pg_operator'::regclass AND d.refobjid = o.oid
WHERE d.classid = 'pg_trigger'::regclass
AND d.deptype = 'n'
AND d.refclassid IN ('pg_type'::regclass, 'pg_proc'::regclass, 'pg_operator'::regclass)
ORDER BY d.objid, cl.relname, d.refobjid
"#
)
.fetch_all(&mut *conn)
.await?;
Ok(rows
.into_iter()
.map(|row| RawReference {
source_oid: row.trigger_oid,
ref_class: row.ref_class,
ref_oid: row.ref_oid,
function_namespace: row.function_namespace,
function_name: row.function_name,
function_args: row.function_args,
operator_namespace: row.operator_namespace,
operator_name: row.operator_name,
operator_left_type: row.operator_left_type,
operator_right_type: row.operator_right_type,
})
.collect())
}
async fn fetch_triggers(conn: &mut PgConnection) -> Result<Vec<RawTrigger>> {
info!("Fetching triggers...");
let rows = sqlx::query!(
r#"
SELECT
t.oid AS "oid!",
t.tgname AS "name!",
c.oid AS "table_oid!",
c.relnamespace AS "table_namespace!",
c.relname AS "table_name!",
t.tgisinternal AS "is_internal!",
p.pronamespace AS "function_namespace!",
p.proname AS "function_name!",
pg_catalog.pg_get_function_identity_arguments(p.oid) AS "function_args!",
pg_catalog.pg_get_triggerdef(t.oid) AS "definition!"
FROM pg_trigger t
JOIN pg_class c ON t.tgrelid = c.oid
JOIN pg_proc p ON t.tgfoid = p.oid
WHERE c.relkind IN ('r', 'v', 'm')
ORDER BY t.oid
"#
)
.fetch_all(&mut *conn)
.await?;
Ok(rows
.into_iter()
.map(|row| RawTrigger {
oid: row.oid,
name: row.name,
table_oid: row.table_oid,
table_namespace: row.table_namespace,
table_name: row.table_name,
is_internal: row.is_internal,
function_namespace: row.function_namespace,
function_name: row.function_name,
function_args: row.function_args,
definition: row.definition,
})
.collect())
}
#[allow(dead_code)]
pub async fn load(conn: &mut PgConnection, shared: &SharedCatalog) -> Result<Vec<Trigger>> {
Ok(load_with_exclusions(conn, shared)
.await?
.log_and_take_objects("trigger"))
}
pub async fn load_with_exclusions(
conn: &mut PgConnection,
shared: &SharedCatalog,
) -> Result<Converted<Trigger>> {
let raw = fetch(conn).await?;
let mut converted = convert(&raw, shared)?;
let index = OidIndex::from_pairs(
class::PG_TRIGGER,
converted
.objects
.iter()
.map(|(oid, trigger)| (*oid, trigger.id())),
)?;
let comments = index.object_comments(&shared.descriptions, class::PG_TRIGGER);
for (_, trigger) in &mut converted.objects {
trigger.comment = comments.get(&trigger.id()).map(|text| text.to_string());
}
converted.index = index;
Ok(converted.map(|(_, trigger)| trigger))
}
pub fn convert(raw: &RawTriggers, shared: &SharedCatalog) -> Result<Converted<(Oid, Trigger)>> {
let mut converted: Converted<(Oid, Trigger)> = Converted::new();
let mut kept: BTreeMap<u32, usize> = BTreeMap::new();
for row in &raw.triggers {
let schema = shared
.namespaces
.name(row.table_namespace)
.with_context(|| format!("trigger {} has no namespace entry", row.name))?;
if is_system_schema(schema) {
converted.excluded.push(Excluded::new(
row.oid,
"trigger",
schema,
&row.name,
ExclusionReason::SystemSchema,
));
continue;
}
if let Some(extension) = shared.extensions.owner_of_relation_subobject(row.table_oid) {
converted.excluded.push(Excluded::new(
row.oid,
"trigger",
schema,
&row.name,
ExclusionReason::ExtensionOwned {
extension: extension.to_string(),
},
));
continue;
}
if row.is_internal {
converted.excluded.push(Excluded::new(
row.oid,
"trigger",
schema,
&row.name,
ExclusionReason::InternalTrigger,
));
continue;
}
let function_schema = shared
.namespaces
.name(row.function_namespace)
.with_context(|| {
format!(
"trigger function {} has no namespace entry",
row.function_name
)
})?;
let depends_on = vec![
DbObjectId::Table {
schema: schema.to_string(),
name: row.table_name.clone(),
},
DbObjectId::Function {
schema: function_schema.to_string(),
name: row.function_name.clone(),
arguments: row.function_args.clone(),
},
];
kept.insert(row.oid.0, converted.objects.len());
converted.objects.push((
row.oid,
Trigger {
schema: schema.to_string(),
table_name: row.table_name.clone(),
name: row.name.clone(),
function_schema: function_schema.to_string(),
function_name: row.function_name.clone(),
function_args: row.function_args.clone(),
comment: None,
depends_on,
definition: row.definition.clone(),
},
));
}
for row in &raw.dependencies {
let Some(&idx) = kept.get(&row.source_oid.0) else {
continue;
};
if let Some(dep) = row.dependency(shared) {
converted.objects[idx].1.depends_on.push(dep);
}
}
for (_, trigger) in &mut converted.objects {
dedup_preserving_order(&mut trigger.depends_on);
}
converted.objects.sort_by(|(_, a), (_, b)| {
(&a.schema, &a.table_name, &a.name).cmp(&(&b.schema, &b.table_name, &b.name))
});
Ok(converted)
}