use crate::queue::key::KeyValue;
use crate::queue::{enqueue_refresh, enqueue_refresh_patched};
use pgrx::PgTupleDesc;
use pgrx::prelude::*;
use pgrx::spi;
enum KeyExtraction {
Value(KeyValue),
Null,
Missing,
}
fn attnum_of(tupdesc: &PgTupleDesc<'_>, name: &str, hint: Option<i16>) -> Option<usize> {
let named = |i: usize| {
tupdesc
.get(i)
.is_some_and(|att| !att.attisdropped && att.name() == name)
};
hint.and_then(|n| usize::try_from(n).ok())
.and_then(|n| n.checked_sub(1))
.filter(|&i| named(i))
.or_else(|| (0..tupdesc.len()).find(|&i| named(i)))
.map(|i| i + 1)
}
unsafe fn tuple_key(
tuple: *mut pg_sys::HeapTupleData,
tupdesc: &PgTupleDesc<'_>,
name: &str,
hint: Option<i16>,
) -> KeyExtraction {
let Some(attnum) = attnum_of(tupdesc, name, hint) else {
return KeyExtraction::Missing;
};
let Some(att) = tupdesc.get(attnum - 1) else {
return KeyExtraction::Missing;
};
let typid = unsafe { pg_sys::getBaseType(att.atttypid) };
unsafe {
let mut isnull = false;
let datum = pg_sys::heap_getattr(
tuple,
i32::try_from(attnum).unwrap_or(i32::MAX),
tupdesc.as_ptr(),
&mut isnull,
);
if isnull {
return KeyExtraction::Null;
}
let int = match typid {
pg_sys::INT2OID => i16::from_datum(datum, false).map(i64::from),
pg_sys::INT4OID => i32::from_datum(datum, false).map(i64::from),
pg_sys::INT8OID => i64::from_datum(datum, false),
_ => {
let mut output = pg_sys::InvalidOid;
let mut varlena = false;
pg_sys::getTypeOutputInfo(typid, &raw mut output, &raw mut varlena);
let text = pg_sys::OidOutputFunctionCall(output, datum);
let value = std::ffi::CStr::from_ptr(text)
.to_string_lossy()
.into_owned();
pg_sys::pfree(text.cast());
return KeyExtraction::Value(KeyValue::Text(value));
}
};
int.map_or(KeyExtraction::Null, |v| {
KeyExtraction::Value(KeyValue::Int(v))
})
}
}
fn row_images(trigger: &PgTrigger<'_>) -> Vec<*mut pg_sys::HeapTupleData> {
let td = trigger.trigger_data();
[td.tg_trigtuple, td.tg_newtuple]
.into_iter()
.filter(|t| !t.is_null())
.collect()
}
fn trigger_tupdesc<'a>(trigger: &'a PgTrigger<'a>) -> Option<PgTupleDesc<'a>> {
let rel = trigger.trigger_data().tg_relation;
unsafe {
(!rel.is_null() && !(*rel).rd_att.is_null())
.then(|| PgTupleDesc::from_pg_unchecked((*rel).rd_att))
}
}
#[pg_trigger]
#[allow(clippy::unnecessary_wraps)] fn pg_tview_trigger_handler<'a>(
trigger: &'a PgTrigger<'a>,
) -> Result<Option<PgHeapTuple<'a, AllocatedByPostgres>>, spi::Error> {
crate::revision::check();
let table_oid = trigger
.relation()
.unwrap_or_else(|e| error!("pg_tviews: the row trigger has no relation: {e}"))
.oid();
let served = crate::delta::trigger_entity(trigger);
let table_oid = crate::delta::partition_root(table_oid)
.unwrap_or_else(|e| e.raise_in("pg_tviews: could not resolve the partition root"));
let paths: Vec<crate::catalog::plan::LocalPath> = crate::cache::cascade_paths(table_oid)
.unwrap_or_else(|e| e.raise_in("pg_tviews: could not read the propagation plans"))
.into_iter()
.filter(|p| p.entity_name == served)
.collect();
if crate::config::suspend_triggers() || crate::suspend::is_suspended() {
crate::suspend::record_change(&served);
return Ok(None);
}
enqueue_local_keys(trigger, &paths);
if let Err(e) = crate::delta::map_row(trigger, &served, table_oid) {
e.raise_in(&format!(
"pg_tviews: could not map the changed row to tv_{served} keys"
));
}
Ok(None)
}
fn new_image(trigger: &PgTrigger<'_>) -> Option<*mut pg_sys::HeapTupleData> {
let new = trigger.trigger_data().tg_newtuple;
(!new.is_null()).then_some(new)
}
fn enqueue_local_keys(trigger: &PgTrigger, paths: &[crate::catalog::plan::LocalPath]) {
if paths.is_empty() {
return;
}
let Some(tupdesc) = trigger_tupdesc(trigger) else {
error!("pg_tviews: the row trigger has no relation");
};
let images = row_images(trigger);
if images.is_empty() {
error!("pg_tviews: the row trigger has no row");
}
let changed = changed_columns(trigger);
for path in paths {
if let Some(changed) = &changed
&& !path.root
&& !path.source_columns.is_empty()
&& !path.source_columns.iter().any(|c| changed.contains(c))
{
continue;
}
if path.root
&& let Some(fields) = try_capture_direct_patch(trigger, path, changed.as_deref())
&& let Some(new) = new_image(trigger)
&& let KeyExtraction::Value(KeyValue::Int(pk)) =
unsafe { tuple_key(new, &tupdesc, &path.initial_col, path.initial_attnum) }
{
enqueue_refresh_patched(&path.entity_name, pk, fields);
continue;
}
for &image in &images {
follow_local_path(path, image, &tupdesc);
}
}
}
fn follow_local_path(
path: &crate::catalog::plan::LocalPath,
image: *mut pg_sys::HeapTupleData,
tupdesc: &PgTupleDesc<'_>,
) {
match unsafe { tuple_key(image, tupdesc, &path.initial_col, path.initial_attnum) } {
KeyExtraction::Value(key) => enqueue_refresh(&path.entity_name, key),
KeyExtraction::Null => {} KeyExtraction::Missing => crate::TViewError::CatalogError {
operation: format!(
"Read the key of tv_{} off a row of {}",
path.entity_name, path.source_table
),
pg_error: format!(
"the table has no column \"{}\" for tv_{} to read its key from",
path.initial_col, path.entity_name
),
}
.raise(),
}
}
unsafe extern "C" {
#[link_name = "datumIsEqual"]
fn datum_is_equal(
value1: pg_sys::Datum,
value2: pg_sys::Datum,
typ_by_val: bool,
typ_len: i32,
) -> bool;
}
fn try_capture_direct_patch(
trigger: &PgTrigger,
path: &crate::catalog::plan::LocalPath,
changed: Option<&[String]>,
) -> Option<serde_json::Map<String, serde_json::Value>> {
if !crate::config::direct_patch_enabled() {
return None;
}
let changed = changed.filter(|c| !c.is_empty())?;
if changed.contains(&path.initial_col) {
return None;
}
let meta = crate::catalog::TviewMeta::load_by_entity(&path.entity_name)
.unwrap_or_else(|e| e.raise_in("pg_tviews: could not read the propagation plan"))?;
let direct = &meta.plan.direct;
let data_key = |col: &str| direct.iter().find(|(c, _)| c == col).map(|(_, k)| k);
if direct.is_empty()
|| !changed.iter().all(|c| data_key(c).is_some())
|| !crate::jsonb_delta::check_jsonb_delta_available()
{
return None;
}
let new_tuple = trigger.new()?;
let mut fields = serde_json::Map::with_capacity(changed.len());
for col in changed {
fields.insert(data_key(col)?.clone(), capture_value(&new_tuple, col)?);
}
Some(fields)
}
fn changed_columns(trigger: &PgTrigger) -> Option<Vec<String>> {
unsafe {
let td: &pg_sys::TriggerData = trigger.trigger_data();
let old_tuple = td.tg_trigtuple;
let new_tuple = td.tg_newtuple;
if old_tuple.is_null() || new_tuple.is_null() {
return None; }
let rel = td.tg_relation;
if rel.is_null() {
return None;
}
let tupdesc_ptr = (*rel).rd_att;
if tupdesc_ptr.is_null() {
return None;
}
let tupdesc = PgTupleDesc::from_pg_unchecked(tupdesc_ptr);
let mut changed = Vec::new();
for i in 0..tupdesc.len() {
let Some(att) = tupdesc.get(i) else { continue };
if att.attisdropped {
continue;
}
let attnum = i32::try_from(i + 1).ok()?;
let mut old_isnull = false;
let mut new_isnull = false;
let old_datum = pg_sys::heap_getattr(old_tuple, attnum, tupdesc_ptr, &mut old_isnull);
let new_datum = pg_sys::heap_getattr(new_tuple, attnum, tupdesc_ptr, &mut new_isnull);
let differs = if old_isnull != new_isnull {
true
} else if old_isnull {
false } else {
!datum_is_equal(old_datum, new_datum, att.attbyval, i32::from(att.attlen))
};
if differs {
changed.push(att.name().to_string());
}
}
Some(changed)
}
}
fn capture_value(
tuple: &PgHeapTuple<'_, AllocatedByPostgres>,
col: &str,
) -> Option<serde_json::Value> {
use serde_json::Value;
match tuple.get_by_name::<String>(col) {
Ok(Some(v)) => return Some(Value::String(v)),
Ok(None) => return Some(Value::Null),
Err(_) => {}
}
match tuple.get_by_name::<bool>(col) {
Ok(Some(v)) => return Some(Value::Bool(v)),
Ok(None) => return Some(Value::Null),
Err(_) => {}
}
match tuple.get_by_name::<i64>(col) {
Ok(Some(v)) => return Some(Value::Number(v.into())),
Ok(None) => return Some(Value::Null),
Err(_) => {}
}
match tuple.get_by_name::<i32>(col) {
Ok(Some(v)) => return Some(Value::Number(v.into())),
Ok(None) => return Some(Value::Null),
Err(_) => {}
}
match tuple.get_by_name::<i16>(col) {
Ok(Some(v)) => return Some(Value::Number(v.into())),
Ok(None) => return Some(Value::Null),
Err(_) => {}
}
match tuple.get_by_name::<pgrx::Uuid>(col) {
Ok(Some(v)) => return Some(Value::String(v.to_string())),
Ok(None) => return Some(Value::Null),
Err(_) => {}
}
match tuple.get_by_name::<pgrx::JsonB>(col) {
Ok(Some(v)) => return Some(v.0),
Ok(None) => return Some(Value::Null),
Err(_) => {}
}
None
}
#[pg_trigger]
#[allow(clippy::unnecessary_wraps)] fn pg_tview_flush_trigger<'a>(
_trigger: &'a PgTrigger<'a>,
) -> Result<Option<PgHeapTuple<'a, AllocatedByPostgres>>, spi::Error> {
if !crate::executor::flush_deferred() {
flush_after_statement();
}
Ok(None)
}
pub fn flush_after_statement() {
if crate::config::suspend_triggers() || crate::suspend::is_suspended() {
return;
}
if let Err(e) = crate::flush::flush_refresh_queue() {
e.raise_in("TVIEW refresh failed");
}
if let Err(e) = crate::audit::flush_audit_buffer() {
e.raise_in("Audit flush failed");
}
}