use pgrx::prelude::*;
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex, PoisonError};
use crate::cascade_path::CascadePath;
#[derive(Clone, Debug)]
pub struct CachedEntityInfo {
pub name: String,
pub distinct_on: bool,
pub legacy_root: Option<LegacyRoot>,
pub direct_map: HashMap<String, String>,
pub fk_columns: Vec<String>,
pub uuid_fk_columns: Vec<String>,
pub output_columns: Vec<String>,
pub is_union: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LegacyRoot {
Pk,
DistinctOn,
}
static ENTITY_GRAPH_CACHE: LazyLock<Mutex<Option<super::graph::EntityDepGraph>>> =
LazyLock::new(|| Mutex::new(None));
static TABLE_ENTITY_CACHE: LazyLock<Mutex<HashMap<pg_sys::Oid, Option<CachedEntityInfo>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
thread_local! {
static CASCADE_PATH_CACHE: std::cell::RefCell<HashMap<pg_sys::Oid, Vec<CascadePath>>> =
std::cell::RefCell::new(HashMap::new());
}
pub mod graph_cache {
#[allow(clippy::wildcard_imports)] use super::*;
pub fn load_cached() -> crate::TViewResult<crate::queue::graph::EntityDepGraph> {
if !crate::config::graph_cache_enabled() {
return crate::queue::graph::EntityDepGraph::load();
}
let mut cache = ENTITY_GRAPH_CACHE
.lock()
.unwrap_or_else(PoisonError::into_inner);
if let Some(graph) = cache.as_ref() {
crate::metrics::metrics_api::record_graph_cache_hit();
return Ok(graph.clone());
}
crate::metrics::metrics_api::record_graph_cache_miss();
let graph = crate::queue::graph::EntityDepGraph::load()?;
*cache = Some(graph.clone());
drop(cache);
Ok(graph)
}
pub fn invalidate() {
let mut cache = ENTITY_GRAPH_CACHE
.lock()
.unwrap_or_else(PoisonError::into_inner);
*cache = None;
}
}
pub mod table_cache {
#[allow(clippy::wildcard_imports)] use super::*;
pub fn entity_info_cached(
table_oid: pg_sys::Oid,
) -> crate::TViewResult<Option<CachedEntityInfo>> {
if !crate::config::table_cache_enabled() {
return load_entity_info_uncached(table_oid);
}
{
let cache = TABLE_ENTITY_CACHE
.lock()
.unwrap_or_else(PoisonError::into_inner);
if let Some(cached_value) = cache.get(&table_oid) {
crate::metrics::metrics_api::record_table_cache_hit();
return Ok(cached_value.clone());
}
}
crate::metrics::metrics_api::record_table_cache_miss();
let info = load_entity_info_uncached(table_oid)?;
{
let mut cache = TABLE_ENTITY_CACHE
.lock()
.unwrap_or_else(PoisonError::into_inner);
crate::utils::bound_cache(&mut cache);
cache.insert(table_oid, info.clone());
}
Ok(info)
}
pub fn entity_for_table_cached(table_oid: pg_sys::Oid) -> crate::TViewResult<Option<String>> {
entity_info_cached(table_oid).map(|info| info.map(|i| i.name))
}
fn load_entity_info_uncached(
table_oid: pg_sys::Oid,
) -> crate::TViewResult<Option<CachedEntityInfo>> {
let Some(name) = crate::catalog::entity_for_table_uncached(table_oid)? else {
return Ok(None);
};
let Some(meta) = crate::catalog::TviewMeta::load_by_entity(&name)? else {
return Ok(Some(CachedEntityInfo {
name,
distinct_on: false,
legacy_root: Some(LegacyRoot::Pk),
direct_map: HashMap::new(),
fk_columns: Vec::new(),
uuid_fk_columns: Vec::new(),
output_columns: Vec::new(),
is_union: false,
}));
};
let mut direct_map: HashMap<String, String> = meta
.direct_map_columns
.iter()
.cloned()
.zip(meta.direct_map_keys.iter().cloned())
.collect();
let output_columns = if let Ok(cols) = crate::utils::get_view_columns_by_oid(meta.tview_oid)
{
cols
} else {
direct_map.clear();
Vec::new()
};
let args = [unsafe {
pgrx::datum::DatumWithOid::new(
name.as_str(),
PgOid::BuiltIn(PgBuiltInOids::TEXTOID).value(),
)
}];
let definition: Option<String> = Spi::get_one_with_args(
&format!(
"SELECT definition FROM {} WHERE entity = $1",
crate::utils::meta_table()
),
&args,
)
.unwrap_or(None);
match definition
.as_deref()
.and_then(crate::schema::direct_map::columns_referenced_outside_data)
{
Some(projected) => direct_map.retain(|col, _| !projected.contains(&col.to_lowercase())),
None => direct_map.clear(),
}
Ok(Some(CachedEntityInfo {
name,
distinct_on: meta.identity.kind == crate::lineage::IdentityKind::DistinctOn,
legacy_root: if !meta.identity.legacy {
None
} else if meta.identity.legacy_distinct_on {
Some(LegacyRoot::DistinctOn)
} else {
Some(LegacyRoot::Pk)
},
direct_map,
fk_columns: meta.fk_columns,
uuid_fk_columns: meta.uuid_fk_columns,
output_columns,
is_union: meta.is_union,
}))
}
pub fn invalidate() {
let mut cache = TABLE_ENTITY_CACHE
.lock()
.unwrap_or_else(PoisonError::into_inner);
cache.clear();
}
}
pub mod cascade_cache {
use super::{CASCADE_PATH_CACHE, CascadePath, pg_sys};
pub fn cascade_paths_for_table(table_oid: pg_sys::Oid) -> crate::TViewResult<Vec<CascadePath>> {
CASCADE_PATH_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(paths) = cache.get(&table_oid) {
return Ok(paths.clone());
}
let paths = load_cascade_paths_for_table(table_oid)?;
cache.insert(table_oid, paths.clone());
Ok(paths)
})
}
fn load_cascade_paths_for_table(
table_oid: pg_sys::Oid,
) -> crate::TViewResult<Vec<CascadePath>> {
let meta_list = crate::catalog::TviewMeta::load_all()?;
let mut relevant_paths = Vec::new();
for meta in meta_list {
for path in meta.cascade_paths {
if path.source_oid == table_oid {
relevant_paths.push(path);
}
}
}
Ok(relevant_paths)
}
pub fn clear_cache() {
CASCADE_PATH_CACHE.with(|cache| {
cache.borrow_mut().clear();
});
}
}
thread_local! {
static GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
static SEEN_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
static WATCHED: std::cell::RefCell<std::collections::HashSet<pg_sys::Oid>> =
std::cell::RefCell::new(std::collections::HashSet::new());
}
thread_local! {
static CATALOG_WATCHED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
fn watch_catalog() {
if CATALOG_WATCHED.with(std::cell::Cell::get) {
return;
}
crate::metrics::metrics_api::record_catalog_lookup();
if let Ok(Some(oid)) = Spi::connect(|client| {
client
.select(
&format!("SELECT to_regclass('{}')::oid", crate::utils::meta_table()),
None,
&[],
)?
.first()
.get_one::<pg_sys::Oid>()
}) {
watch(&[oid]);
CATALOG_WATCHED.with(|w| w.set(true));
}
}
pub fn watch(oids: &[pg_sys::Oid]) {
WATCHED.with(|w| w.borrow_mut().extend(oids.iter().copied()));
}
pub fn sync_generation() {
watch_catalog();
let current = GENERATION.with(std::cell::Cell::get);
if SEEN_GENERATION.with(std::cell::Cell::get) != current {
SEEN_GENERATION.with(|s| s.set(current));
invalidate_all_caches();
}
}
#[pg_guard]
unsafe extern "C-unwind" fn relcache_callback(_arg: pg_sys::Datum, relid: pg_sys::Oid) {
let relevant = relid == pg_sys::InvalidOid
|| WATCHED.with(|w| w.try_borrow().map_or(true, |w| w.contains(&relid)));
if relevant {
GENERATION.with(|g| g.set(g.get().wrapping_add(1)));
}
}
pub fn register_relcache_callback() {
unsafe {
pg_sys::CacheRegisterRelcacheCallback(Some(relcache_callback), pg_sys::Datum::from(0));
}
}
#[pg_extern]
fn pg_tviews_invalidate_caches(relid: pg_sys::Oid) {
unsafe { pg_sys::CacheInvalidateRelcacheByRelid(relid) };
}
pub fn invalidate_all_caches() {
graph_cache::invalidate();
table_cache::invalidate();
cascade_cache::clear_cache();
crate::catalog::clear_meta_cache();
super::ops::clear_crash_recovery_cache();
crate::lifecycle::invalidate_jsonb_delta_cache();
crate::utils::invalidate_oid_relname_cache();
crate::utils::invalidate_view_columns_cache();
crate::delta::clear_caches();
}
#[cfg(test)]
#[allow(clippy::wildcard_imports)] mod tests {
use super::*;
static TABLE_CACHE_TEST_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn test_graph_cache_invalidation() {
graph_cache::invalidate();
assert!(ENTITY_GRAPH_CACHE.lock().unwrap().is_none());
}
#[test]
fn test_table_cache_invalidation() {
let _guard = TABLE_CACHE_TEST_LOCK
.lock()
.unwrap_or_else(PoisonError::into_inner);
{
let mut cache = TABLE_ENTITY_CACHE.lock().unwrap();
cache.insert(
pg_sys::Oid::from(123),
Some(CachedEntityInfo {
name: "test".to_string(),
distinct_on: false,
legacy_root: None,
direct_map: HashMap::new(),
fk_columns: Vec::new(),
uuid_fk_columns: Vec::new(),
output_columns: Vec::new(),
is_union: false,
}),
);
}
assert!(
TABLE_ENTITY_CACHE
.lock()
.unwrap()
.get(&pg_sys::Oid::from(123))
.is_some()
);
table_cache::invalidate();
assert!(TABLE_ENTITY_CACHE.lock().unwrap().is_empty());
}
#[test]
fn test_cached_entity_info_carries_direct_patch_fields() {
let mut direct_map = HashMap::new();
direct_map.insert("bio".to_string(), "bio".to_string());
direct_map.insert("name".to_string(), "display_name".to_string());
let info = CachedEntityInfo {
name: "user".to_string(),
distinct_on: false,
legacy_root: None,
direct_map,
fk_columns: vec!["fk_org".to_string()],
uuid_fk_columns: vec![],
output_columns: vec!["pk_user".to_string(), "id".to_string(), "data".to_string()],
is_union: false,
};
assert_eq!(info.direct_map.get("bio").map(String::as_str), Some("bio"));
assert_eq!(
info.direct_map.get("name").map(String::as_str),
Some("display_name")
);
assert!(info.fk_columns.contains(&"fk_org".to_string()));
assert!(info.output_columns.contains(&"data".to_string()));
assert!(!info.is_union);
}
#[test]
fn test_negative_cache_entry() {
let _guard = TABLE_CACHE_TEST_LOCK
.lock()
.unwrap_or_else(PoisonError::into_inner);
table_cache::invalidate();
TABLE_ENTITY_CACHE
.lock()
.unwrap()
.insert(pg_sys::Oid::from(999), None);
let cache = TABLE_ENTITY_CACHE.lock().unwrap();
assert!(cache.get(&pg_sys::Oid::from(999)).is_some()); assert!(cache.get(&pg_sys::Oid::from(999)).unwrap().is_none()); }
}