use crate::error::{TViewError, TViewResult};
use pgrx::datum::DatumWithOid;
use pgrx::prelude::*;
pub fn drop_tview(tview_name: &str, if_exists: bool, cascade: bool) -> TViewResult<bool> {
crate::revision::check();
let (schema, entity) = super::replace::parse_name(tview_name)?;
let entity_name = entity.as_str();
super::lock_entity(entity_name)?;
let exists = tview_exists_in_metadata(entity_name)?
&& match &schema {
Some(schema) => {
super::replace::registered_schema(entity_name)?.as_ref() == Some(schema)
}
None => true,
};
if !exists && !if_exists {
return Err(TViewError::MetadataNotFound {
entity: tview_name.to_string(),
});
}
if !exists {
notice!("TVIEW \"{tview_name}\" does not exist, skipping");
return Ok(false);
}
let meta = crate::catalog::TviewMeta::load_by_entity(entity_name).map_err(|e| {
TViewError::SpiError {
query: "Load TviewMeta by entity".to_string(),
error: e.to_string(),
}
})?;
if let Some(ref m) = meta
&& let Some(owned) = owned_relation(m)?
{
crate::owner::require_owner(owned, &format!("tv_{entity_name}"))?;
}
crate::dependency::remove_entity_triggers(entity_name)?;
if let Some(ref m) = meta {
drop_by_oid(m.tview_oid, "TABLE", cascade)?;
}
if let Some(ref m) = meta {
drop_by_oid(m.view_oid, "VIEW", cascade)?;
}
drop_metadata(entity_name)?;
crate::queue::cache::invalidate_all_caches();
crate::audit::log_drop(entity_name);
if let Err(e) = crate::audit::flush_audit_buffer() {
warning!("Failed to flush audit after DROP: {}", e);
}
Ok(true)
}
pub fn handle_dropped(entity: &str) -> TViewResult<()> {
let args =
[unsafe { DatumWithOid::new(entity, PgOid::BuiltIn(PgBuiltInOids::TEXTOID).value()) }];
let dropped = Spi::get_one_with_args::<bool>(
&format!(
"SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_event_trigger_dropped_objects() d \
JOIN {} m ON d.objid IN (m.view_oid, m.table_oid) \
WHERE m.entity = $1 AND NOT d.original AND d.objsubid = 0 \
AND d.classid = 'pg_catalog.pg_class'::pg_catalog.regclass)",
crate::utils::meta_table()
),
&args,
)
.map_err(|e| TViewError::SpiError {
query: "pg_event_trigger_dropped_objects()".to_string(),
error: e.to_string(),
})?;
if dropped != Some(true) {
return Err(TViewError::InvalidInput {
parameter: "entity".to_string(),
reason: format!("the current statement did not drop TVIEW {entity}'s view or table"),
});
}
let (table_left, table_owned) = Spi::get_two_with_args::<bool, bool>(
&format!(
"SELECT t.oid IS NOT NULL, \
COALESCE(pg_catalog.pg_has_role(t.relowner, 'USAGE'), false) \
FROM {} m LEFT JOIN pg_catalog.pg_class t ON t.oid = m.table_oid \
WHERE m.entity = $1",
crate::utils::meta_table()
),
&args,
)
.map_err(|e| TViewError::SpiError {
query: "table of a dropped TVIEW".to_string(),
error: e.to_string(),
})?;
if table_owned == Some(true) {
return drop_tview(entity, true, true).map(|_| ());
}
crate::dependency::remove_entity_triggers(entity)?;
drop_metadata(entity)?;
crate::queue::cache::invalidate_all_caches();
crate::audit::log_drop(entity);
if table_left == Some(true) {
notice!(
"pg_tviews: TVIEW {entity} deregistered; its table belongs to another role and \
was kept as a plain table"
);
}
Ok(())
}
fn drop_by_oid(oid: pg_sys::Oid, kind: &str, cascade: bool) -> TViewResult<()> {
let qualified = crate::utils::spi_get_string(&format!(
"SELECT quote_ident(n.nspname::text) || '.' || quote_ident(c.relname::text) \
FROM pg_class c \
JOIN pg_namespace n ON c.relnamespace = n.oid \
WHERE c.oid = {}",
oid.to_u32()
))
.map_err(|e| TViewError::CatalogError {
operation: format!("Resolve qualified name for OID {}", oid.to_u32()),
pg_error: e.to_string(),
})?;
if let Some(qname) = qualified {
let cascade_kw = if cascade { " CASCADE" } else { "" };
let sql = format!("DROP {kind} IF EXISTS {qname}{cascade_kw}");
crate::utils::spi_run_ddl(&sql).map_err(|e| TViewError::SpiError {
query: sql,
error: e,
})?;
}
Ok(())
}
fn tview_exists_in_metadata(entity_name: &str) -> TViewResult<bool> {
let args = vec![unsafe {
DatumWithOid::new(entity_name, PgOid::BuiltIn(PgBuiltInOids::TEXTOID).value())
}];
Spi::get_one_with_args::<bool>(
&format!(
"SELECT COUNT(*) > 0 FROM {} WHERE entity = $1",
crate::utils::meta_table()
),
&args,
)
.map_err(|e| TViewError::CatalogError {
operation: format!("Check TVIEW metadata: {entity_name}"),
pg_error: format!("{e:?}"),
})
.map(|opt| opt.unwrap_or(false))
}
fn owned_relation(meta: &crate::catalog::TviewMeta) -> TViewResult<Option<pg_sys::Oid>> {
let args = [meta.tview_oid, meta.view_oid].map(|oid| {
unsafe { DatumWithOid::new(oid, PgOid::BuiltIn(PgBuiltInOids::OIDOID).value()) }
});
Spi::connect(|client| {
client
.select(
"SELECT COALESCE((SELECT oid FROM pg_catalog.pg_class WHERE oid = $1), \
(SELECT oid FROM pg_catalog.pg_class WHERE oid = $2))",
None,
&args,
)?
.first()
.get_one::<pg_sys::Oid>()
})
.map_err(|e| TViewError::CatalogError {
operation: format!("Find the relations of TVIEW {}", meta.entity_name),
pg_error: e.to_string(),
})
}
fn drop_metadata(entity_name: &str) -> TViewResult<()> {
let args =
[
unsafe {
DatumWithOid::new(entity_name, PgOid::BuiltIn(PgBuiltInOids::TEXTOID).value())
},
];
let sql = format!(
"DELETE FROM {} WHERE entity = $1",
crate::utils::meta_table()
);
let _owner = crate::owner::AsOwner::of_extension()?;
Spi::run_with_args(&sql, &args).map_err(|e| TViewError::SpiError {
query: sql.clone(),
error: e.to_string(),
})?;
Ok(())
}
#[cfg(any(test, feature = "pg_test"))]
#[pg_schema]
mod tests {
use pgrx::prelude::*;
#[pg_test]
fn test_drop_tview_nonexistent_if_exists() {
let result = Spi::run("SELECT pg_tviews_drop('nonexistent', true, false)");
assert!(
result.is_ok(),
"IF EXISTS drop of non-existent TVIEW should succeed"
);
}
#[pg_test]
fn test_drop_tview_nonexistent_strict() {
let result = Spi::run("SELECT pg_tviews_drop('nonexistent', false, false)");
assert!(
result.is_err(),
"Strict drop of non-existent TVIEW should fail"
);
}
}