use pgrx::pg_sys;
use pgrx::prelude::*;
use std::ffi::CStr;
mod ctas;
mod indexes;
mod statements;
mod tables;
use ctas::{
CTAS_HINT, Ctas, CtasTarget, create_tview_from_ctas, inspect_create_table_as, skipped_or_raise,
tview_ctas_target, utility_of,
};
use indexes::{IndexFollowUp, index_follow_up_of};
use statements::{
ColumnChange, ColumnRename, PartitionDdl, PrivilegesChange, column_change_of, column_rename_of,
drops_pg_tviews, extension_installed, extension_statement_names, matview_refresh_of,
partition_ddl_of, privileges_change_of, resolve_relation_oid, table_move_of,
};
use tables::handle_drop_table;
use crate::TViewError;
use crate::ddl::drop_tview;
use crate::error::TViewResult;
static mut PREV_PROCESS_UTILITY_HOOK: pg_sys::ProcessUtility_hook_type = None;
pub unsafe fn install_hook() {
unsafe {
PREV_PROCESS_UTILITY_HOOK = pg_sys::ProcessUtility_hook;
pg_sys::ProcessUtility_hook = Some(tview_process_utility_hook);
crate::executor::install_hooks();
}
}
pub unsafe fn ensure_hook_installed() {
unsafe {
static mut HOOK_INSTALLED: bool = false;
if !HOOK_INSTALLED {
install_hook();
HOOK_INSTALLED = true;
}
}
}
#[derive(Clone, Copy)]
struct UtilityCall {
pstmt: *mut pg_sys::PlannedStmt,
query_string: *const ::std::os::raw::c_char,
read_only_tree: bool,
context: pg_sys::ProcessUtilityContext::Type,
params: pg_sys::ParamListInfo,
query_env: *mut pg_sys::QueryEnvironment,
dest: *mut pg_sys::DestReceiver,
qc: *mut pg_sys::QueryCompletion,
}
impl UtilityCall {
fn pass_through(self) {
unsafe {
call_prev_hook_or_standard(
self.pstmt,
self.query_string,
self.read_only_tree,
self.context,
self.params,
self.query_env,
self.dest,
self.qc,
);
}
}
fn tag(self) -> Option<pg_sys::NodeTag> {
unsafe {
(!self.pstmt.is_null() && !(*self.pstmt).utilityStmt.is_null())
.then(|| (*(*self.pstmt).utilityStmt).type_)
}
}
}
#[pg_guard]
#[allow(clippy::too_many_arguments)] unsafe extern "C-unwind" fn tview_process_utility_hook(
pstmt: *mut pg_sys::PlannedStmt,
query_string: *const ::std::os::raw::c_char,
read_only_tree: bool,
context: pg_sys::ProcessUtilityContext::Type,
params: pg_sys::ParamListInfo,
query_env: *mut pg_sys::QueryEnvironment,
dest: *mut pg_sys::DestReceiver,
qc: *mut pg_sys::QueryCompletion,
) {
let mut call = UtilityCall {
pstmt,
query_string,
read_only_tree,
context,
params,
query_env,
dest,
qc,
};
if call.tag() == Some(pg_sys::NodeTag::T_TruncateStmt) {
crate::delta::begin_truncate();
}
if crate::internal_ddl::InternalDdl::active() {
call.pass_through();
return;
}
let pass_through = {
let _internal = crate::internal_ddl::InternalDdl::begin();
flush_before_transaction_end(call);
let follow_ups = unsafe { FollowUps::of(call.pstmt) };
let pass_through = handle(decide(call), &mut call);
pass_through.then_some(follow_ups)
};
if let Some(follow_ups) = pass_through {
call.pass_through();
let _internal = crate::internal_ddl::InternalDdl::begin();
follow_ups.run();
}
}
fn flush_before_transaction_end(call: UtilityCall) {
if call.context != pg_sys::ProcessUtilityContext::PROCESS_UTILITY_TOPLEVEL
|| call.tag() != Some(pg_sys::NodeTag::T_TransactionStmt)
{
return;
}
#[allow(clippy::cast_ptr_alignment)] let kind = unsafe { (*(*call.pstmt).utilityStmt.cast::<pg_sys::TransactionStmt>()).kind };
let stmt = match kind {
pg_sys::TransactionStmtKind::TRANS_STMT_COMMIT => "COMMIT",
pg_sys::TransactionStmtKind::TRANS_STMT_PREPARE => "PREPARE TRANSACTION",
_ => return,
};
if !unsafe { pg_sys::IsTransactionState() && !pg_sys::IsAbortedTransactionBlockState() } {
return;
}
if crate::suspend::is_suspended() {
crate::suspend::force_resume();
if let Err(e) = crate::suspend::catch_up() {
e.raise_in(&format!(
"TVIEW catch-up after suspension failed before {stmt}"
));
}
}
if let Err(e) = crate::flush::flush_refresh_queue() {
e.raise_in(&format!("TVIEW refresh failed before {stmt}"));
}
if let Err(e) = crate::audit::flush_audit_buffer() {
e.raise_in(&format!("Audit flush failed before {stmt}"));
}
}
fn decide(call: UtilityCall) -> Intercept {
let result = std::panic::catch_unwind(|| -> Result<Intercept, TViewError> {
let Some(node_tag) = call.tag() else {
return Ok(Intercept::PassThrough);
};
let utility_stmt = unsafe { (*call.pstmt).utilityStmt };
if let Some(extensions) = unsafe { extension_statement_names(utility_stmt) } {
if extensions.iter().any(|e| e == "jsonb_delta") {
crate::cache::invalidate_all();
}
if extensions.iter().any(|e| e == "pg_tviews") {
crate::revision::reset();
}
return Ok(Intercept::PassThrough);
}
if !extension_installed() {
return Ok(Intercept::PassThrough);
}
match node_tag {
pg_sys::NodeTag::T_CreateTableAsStmt => {
#[allow(clippy::cast_ptr_alignment)]
let ctas = utility_stmt.cast::<pg_sys::CreateTableAsStmt>();
unsafe { inspect_create_table_as(ctas, call.pstmt, call.query_string) }
}
pg_sys::NodeTag::T_ExplainStmt => {
#[allow(clippy::cast_ptr_alignment)] let query = unsafe { (*utility_stmt.cast::<pg_sys::ExplainStmt>()).query };
Ok(match unsafe { tview_ctas_target(utility_of(query)) } {
Some(table) => Intercept::Refuse(
format!("EXPLAIN of CREATE TABLE {table} AS … cannot create a TVIEW"),
None,
),
None => Intercept::PassThrough,
})
}
#[allow(clippy::cast_ptr_alignment)] pg_sys::NodeTag::T_DropStmt => Ok(Intercept::DropTable(
utility_stmt.cast::<pg_sys::DropStmt>(),
)),
_ => Ok(unsafe { column_change_of(utility_stmt) }
.map_or(Intercept::PassThrough, Intercept::ColumnChange)),
}
});
match result {
Ok(Ok(intercept)) => intercept,
Ok(Err(e)) => e.raise(),
Err(panic_info) => rethrow(panic_info),
}
}
fn rethrow(panic_info: Box<dyn std::any::Any + Send>) -> ! {
let panic_info = match panic_info.downcast::<pg_sys::panic::CaughtError>() {
Ok(caught) => caught.rethrow(),
Err(panic_info) => panic_info,
};
let panic_info = match panic_info.downcast::<pg_sys::panic::ErrorReportWithLevel>() {
Ok(report) => pg_sys::panic::CaughtError::ErrorReport(*report).rethrow(),
Err(panic_info) => panic_info,
};
let panic_msg = panic_info
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| panic_info.downcast_ref::<String>().cloned())
.unwrap_or_else(|| format!("{panic_info:?}"));
error!(
"PANIC in ProcessUtility hook: {panic_msg} - This is a bug in pg_tviews - please report it!"
)
}
fn handle(intercept: Intercept, call: &mut UtilityCall) -> bool {
match intercept {
Intercept::PassThrough => true,
Intercept::Refuse(_, Some(target)) | Intercept::CreateTview(Ctas { target, .. })
if skipped_or_raise(&target) =>
{
true
}
Intercept::Refuse(reason, _) => {
pg_sys::panic::ErrorReport::new(
PgSqlErrorCode::ERRCODE_FEATURE_NOT_SUPPORTED,
format!("pg_tviews: {reason}"),
function_name!(),
)
.set_hint(CTAS_HINT)
.report(PgLogLevel::ERROR);
unreachable!("ERROR does not return")
}
Intercept::CreateTview(ctas) => {
unsafe { create_tview_from_ctas(&ctas, call.qc) };
false
}
Intercept::ColumnChange(change) => match refuse_column_change(&change) {
Ok(()) => true,
Err(e) => e.raise(),
},
Intercept::DropTable(drop_stmt) => {
let drop_stmt = if call.read_only_tree {
unsafe {
call.pstmt = pg_sys::copyObjectImpl(call.pstmt.cast()).cast();
#[allow(clippy::cast_ptr_alignment)]
(*call.pstmt).utilityStmt.cast::<pg_sys::DropStmt>()
}
} else {
drop_stmt
};
match unsafe { handle_drop_table(drop_stmt, call.query_string) } {
Ok(handled) => !handled,
Err(e) => e.raise(),
}
}
}
}
fn refuse_column_change(change: &ColumnChange) -> TViewResult<()> {
let relid = unsafe { resolve_relation_oid(change.relation) };
if relid == pg_sys::InvalidOid || crate::catalog::TviewMeta::entity_of_table(relid)?.is_none() {
return Ok(());
}
let refused = |what: String| -> TViewResult<()> {
Err(TViewError::ColumnDdlRefused {
table: crate::utils::qualified_relname_from_oid(relid)?,
change: what,
})
};
if let Some(what) = change.removes {
return refused(what.to_string());
}
for (column, type_name) in &change.retypes {
let mut to = pg_sys::InvalidOid;
let mut typmod = -1;
unsafe {
pg_sys::typenameTypeIdAndMod(
std::ptr::null_mut(),
*type_name,
&raw mut to,
&raw mut typmod,
);
}
let from = crate::utils::spi::one::<pg_sys::Oid>(
&format!(
"SELECT a.atttypid FROM {} m JOIN pg_catalog.pg_attribute a \
ON a.attrelid = m.view_oid::pg_catalog.oid \
WHERE m.table_oid::pg_catalog.oid = $1 AND a.attname = $2 AND NOT a.attisdropped",
crate::utils::meta_table()
),
&[
crate::utils::spi::oid(relid),
crate::utils::spi::text(column.as_str()),
],
)?;
let fixed = column == "id" || column == "data" || column.starts_with("pk_");
let writable = !fixed
&& from.is_some_and(|from| unsafe {
pg_sys::can_coerce_type(
1,
&raw const from,
&raw const to,
pg_sys::CoercionContext::COERCION_ASSIGNMENT,
)
});
if !writable {
let new_type = crate::utils::qualified_type_name(to, typmod);
return refused(format!(
"ALTER COLUMN {} TYPE {}",
crate::utils::quote_identifier(column),
new_type.strip_prefix("pg_catalog.").unwrap_or(&new_type)
));
}
}
Ok(())
}
struct FollowUps {
column_rename: Option<ColumnRename>,
partition_ddl: Option<PartitionDdl>,
table_move: Option<pg_sys::Oid>,
privileges_change: Option<PrivilegesChange>,
extension_drop: Option<Vec<pg_sys::Oid>>,
matview_refresh: Option<pg_sys::Oid>,
index_follow_up: Option<IndexFollowUp>,
}
impl FollowUps {
unsafe fn of(pstmt: *mut pg_sys::PlannedStmt) -> Self {
if !extension_installed() {
return Self {
column_rename: None,
partition_ddl: None,
table_move: None,
privileges_change: None,
extension_drop: None,
matview_refresh: None,
index_follow_up: None,
};
}
let extension_drop = unsafe { drops_pg_tviews(pstmt) }.then(|| {
crate::ddl::drop::backing_views().unwrap_or_else(|e| {
e.raise_in("pg_tviews: could not read the backing views before DROP EXTENSION")
})
});
unsafe {
Self {
column_rename: column_rename_of(pstmt),
partition_ddl: partition_ddl_of(pstmt),
table_move: table_move_of(pstmt),
privileges_change: privileges_change_of(pstmt),
extension_drop,
matview_refresh: matview_refresh_of(pstmt),
index_follow_up: index_follow_up_of(pstmt).unwrap_or_else(|e| e.raise()),
}
}
}
fn run(self) {
if let Some((relid, old_name, new_name)) =
self.column_rename.and_then(ColumnRename::resolve)
&& let Err(e) = crate::ddl::rename::handle_column_rename(relid, &old_name, &new_name)
{
e.raise_in("pg_tviews: could not follow the column rename");
}
if let Some(ddl) = self.partition_ddl
&& let Err(e) = unsafe { ddl.apply() }
{
e.raise_in("pg_tviews: could not update the triggers of a partition");
}
if let Some(table) = self.table_move
&& let Err(e) = crate::ddl::follow_table_move(table)
{
e.raise_in("pg_tviews: could not rename the backing view of a moved TVIEW");
}
if let Some(PrivilegesChange { owners }) = self.privileges_change
&& let Err(e) = crate::ddl::privileges::follow(None, owners)
{
e.raise_in("pg_tviews: could not give the backing views their tables' privileges");
}
if let Some(views) = self.extension_drop
&& let Err(e) = crate::ddl::drop::drop_left_backing_views(&views)
{
e.raise_in("pg_tviews: could not drop the backing views of the dropped extension");
}
if let Some(follow_up) = self.index_follow_up
&& let Err(e) = follow_up.run()
{
e.raise_in("pg_tviews: could not record the indexes of a TVIEW's table");
}
if let Some(matview) = self.matview_refresh
&& let Err(e) = crate::ddl::uncascaded::refresh_readers_of(matview)
{
e.raise_in(
"pg_tviews: could not refresh the TVIEWs reading a refreshed materialized view",
);
}
}
}
enum Intercept {
PassThrough,
DropTable(*mut pg_sys::DropStmt),
ColumnChange(ColumnChange),
CreateTview(Ctas),
Refuse(String, Option<CtasTarget>),
}
#[allow(clippy::too_many_arguments)] unsafe fn call_prev_hook_or_standard(
pstmt: *mut pg_sys::PlannedStmt,
query_string: *const ::std::os::raw::c_char,
read_only_tree: bool,
context: pg_sys::ProcessUtilityContext::Type,
params: pg_sys::ParamListInfo,
query_env: *mut pg_sys::QueryEnvironment,
dest: *mut pg_sys::DestReceiver,
qc: *mut pg_sys::QueryCompletion,
) {
unsafe {
match PREV_PROCESS_UTILITY_HOOK {
Some(prev_hook) => pg_sys::ffi::pg_guard_ffi_boundary(|| {
prev_hook(
pstmt,
query_string,
read_only_tree,
context,
params,
query_env,
dest,
qc,
);
}),
None => {
pg_sys::standard_ProcessUtility(
pstmt,
query_string,
read_only_tree,
context,
params,
query_env,
dest,
qc,
);
}
}
}
}