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_key: Option<String>,
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,
}
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_key: None,
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()
};
Ok(Some(CachedEntityInfo {
name,
distinct_on_key: meta.distinct_on_keys.first().cloned(),
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();
});
}
}
pub fn invalidate_all_caches() {
graph_cache::invalidate();
table_cache::invalidate();
crate::lifecycle::invalidate_jsonb_delta_cache();
crate::utils::invalidate_oid_relname_cache();
crate::utils::invalidate_view_columns_cache();
crate::utils::invalidate_dedup_dml_cache();
}
#[cfg(test)]
#[allow(clippy::wildcard_imports)] mod tests {
use super::*;
#[test]
fn test_graph_cache_invalidation() {
graph_cache::invalidate();
assert!(ENTITY_GRAPH_CACHE.lock().unwrap().is_none());
}
#[test]
fn test_table_cache_invalidation() {
{
let mut cache = TABLE_ENTITY_CACHE.lock().unwrap();
cache.insert(
pg_sys::Oid::from(123),
Some(CachedEntityInfo {
name: "test".to_string(),
distinct_on_key: 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_key: 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() {
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()); }
}