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>,
}
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);
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 entity_name = crate::catalog::entity_for_table_uncached(table_oid)?;
match entity_name {
Some(name) => {
let distinct_on_key = pgrx::spi::Spi::connect(|client| {
let args = vec![unsafe {
pgrx::datum::DatumWithOid::new(
&name,
pgrx::pg_sys::PgOid::BuiltIn(pgrx::pg_sys::PgBuiltInOids::TEXTOID)
.value(),
)
}];
let mut rows = client.select(
"SELECT distinct_on_keys FROM pg_tview_meta WHERE entity = $1",
None,
&args,
)?;
let result: Result<Option<String>, pgrx::spi::Error> = match rows.next() {
Some(row) => {
let keys: Option<Vec<String>> = row["distinct_on_keys"].value()?;
Ok(keys.and_then(|k| k.first().cloned()))
}
None => Ok(None),
};
result
})?;
Ok(Some(CachedEntityInfo {
name,
distinct_on_key,
}))
}
None => Ok(None),
}
}
pub fn invalidate() {
let mut cache = TABLE_ENTITY_CACHE
.lock()
.unwrap_or_else(PoisonError::into_inner);
cache.clear();
}
}
pub mod cascade_cache {
use super::*;
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,
}),
);
}
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_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()); }
}