use crate::{TViewError, TViewResult, utils::quote_identifier};
use pgrx::pg_sys::panic::ErrorReport;
use pgrx::prelude::*;
#[pg_extern]
fn pg_tviews_refresh(entity: &str) -> Result<(), ErrorReport> {
crate::revision::check();
let meta = crate::catalog::TviewMeta::load_by_entity(entity)?.ok_or_else(|| {
TViewError::MetadataNotFound {
entity: entity.to_string(),
}
})?;
crate::owner::require_owner(meta.tview_oid, &format!("tv_{entity}"))?;
rebuild_with_dependents(&[entity.to_string()])?;
Ok(())
}
#[pg_extern]
fn pg_tviews_refresh_time_dependent(
tview: default!(Option<&str>, "NULL"),
) -> Result<SetOfIterator<'static, String>, ErrorReport> {
crate::revision::check();
let rows: Vec<(String, pgrx::pg_sys::Oid, bool, bool)> = Spi::connect(|client| {
let mut rows = Vec::new();
for row in client.select(
&format!(
"SELECT m.entity::text, m.table_oid::oid, m.time_dependent, \
pg_catalog.pg_has_role(c.relowner, 'USAGE') \
FROM {} m JOIN pg_catalog.pg_class c ON c.oid = m.table_oid \
WHERE $1::text IS NULL \
OR m.table_oid::oid = $1::text::pg_catalog.regclass::pg_catalog.oid",
crate::utils::meta_table()
),
None,
&[crate::utils::spi::text(tview)],
)? {
if let (Some(entity), Some(table)) =
(row.get::<String>(1)?, row.get::<pgrx::pg_sys::Oid>(2)?)
{
rows.push((
entity,
table,
row.get::<bool>(3)?.unwrap_or(false),
row.get::<bool>(4)?.unwrap_or(false),
));
}
}
Ok::<_, pgrx::spi::Error>(rows)
})
.map_err(|e| TViewError::CatalogError {
operation: "Find the time-dependent TVIEWs".to_string(),
pg_error: e.to_string(),
})?;
let chosen: Vec<(String, pgrx::pg_sys::Oid)> = match tview {
Some(name) => {
let Some((entity, table, dependent, _)) = rows.into_iter().next() else {
return Err(TViewError::InvalidInput {
parameter: "tview".to_string(),
reason: format!("{name} is not a TVIEW"),
}
.into());
};
if !dependent {
return Err(TViewError::InvalidInput {
parameter: "tview".to_string(),
reason: format!("{name} does not read the time: nothing to refresh"),
}
.into());
}
crate::owner::require_owner(table, name)?;
vec![(entity, table)]
}
None => rows
.into_iter()
.filter(|(_, _, dependent, owned)| *dependent && *owned)
.map(|(entity, table, _, _)| (entity, table))
.collect(),
};
let order = crate::flush::EntityDepGraph::load()?.topo_order;
let mut chosen = chosen;
chosen.sort_by_key(|(entity, _)| order.iter().position(|e| e == entity).unwrap_or(usize::MAX));
let mut refreshed = Vec::new();
for (entity, table) in &chosen {
if crate::config::suspend_triggers() || crate::suspend::is_suspended() {
crate::suspend::record_change(entity);
} else {
crate::queue::enqueue_refresh_all(entity);
}
refreshed.push(crate::utils::qualified_relname_from_oid(*table)?);
}
crate::flush::flush_refresh_queue()?;
Ok(SetOfIterator::new(refreshed))
}
pub fn rebuild_with_dependents(entities: &[String]) -> TViewResult<Vec<String>> {
let graph = crate::flush::EntityDepGraph::load()?;
let mut readers: std::collections::HashMap<&str, Vec<&str>> = std::collections::HashMap::new();
for (reader, read) in &graph.children {
for entity in read {
readers.entry(entity).or_default().push(reader);
}
}
let mut order: Vec<String> = Vec::new();
let mut pending: std::collections::VecDeque<&str> =
entities.iter().map(String::as_str).collect();
while let Some(entity) = pending.pop_front() {
if order.iter().any(|e| e == entity) {
continue;
}
pending.extend(readers.get(entity).into_iter().flatten());
order.push(entity.to_string());
}
order.sort_by_key(|e| {
graph
.topo_order
.iter()
.position(|t| t == e)
.unwrap_or(usize::MAX)
});
for entity in &order {
rebuild_one(entity)?;
}
flush_after_rebuilds()?;
Ok(order)
}
pub fn flush_after_rebuilds() -> TViewResult<()> {
crate::flush::flush_refresh_queue()
}
pub fn rebuild_one(entity: &str) -> TViewResult<()> {
if let Some(meta) = crate::catalog::TviewMeta::load_by_entity(entity)? {
crate::concurrency::lock_relation(
meta.tview_oid.to_u32(),
crate::concurrency::Side::Writer,
);
crate::concurrency::reads::lock_whole_read_set(&meta)?;
}
let owner = crate::owner::AsOwner::of_entity(entity)?;
let (qi_tv, insert) = rebuild_statements(&owner, entity)?;
Spi::run(&format!("TRUNCATE {qi_tv}"))?;
Spi::run(&insert)?;
drop(owner);
let meta = crate::catalog::TviewMeta::load_by_entity(entity)?.ok_or_else(|| {
TViewError::MetadataNotFound {
entity: entity.to_string(),
}
})?;
crate::lifecycle::validity::mark(meta.tview_oid)?;
Ok(())
}
pub fn refill(entity: &str) -> TViewResult<()> {
if let Some(meta) = crate::catalog::TviewMeta::load_by_entity(entity)? {
crate::concurrency::reads::lock_whole_read_set(&meta)?;
}
let owner = crate::owner::AsOwner::of_entity(entity)?;
let (qi_tv, insert) = rebuild_statements(&owner, entity)?;
Spi::run(&format!("DELETE FROM {qi_tv}"))?;
Spi::run(&format!("{insert} ON CONFLICT DO NOTHING"))?;
Ok(())
}
pub fn fill_empty_tview(entity: &str) -> TViewResult<()> {
let owner = crate::owner::AsOwner::of_entity(entity)?;
let (_, insert) = rebuild_statements(&owner, entity)?;
Spi::run(&insert)?;
Ok(())
}
fn rebuild_statements(
_owner: &crate::owner::AsOwner,
entity: &str,
) -> TViewResult<(String, String)> {
use crate::catalog::TviewMeta;
let meta = TviewMeta::load_by_entity(entity)?.ok_or_else(|| TViewError::MetadataNotFound {
entity: entity.to_string(),
})?;
let qi_tv = crate::utils::qualified_relname_from_oid(meta.tview_oid)?;
let qi_view = crate::utils::qualified_relname_from_oid(meta.view_oid)?;
let view_columns = crate::utils::get_view_columns_by_oid(meta.view_oid)?;
if view_columns.is_empty() {
return Err(TViewError::CatalogError {
operation: format!("Get columns for view {qi_view}"),
pg_error: "View has no selectable columns".to_string(),
});
}
let col_list = view_columns
.iter()
.map(|c| quote_identifier(c))
.collect::<Vec<_>>()
.join(", ");
let insert = format!("INSERT INTO {qi_tv} ({col_list}) SELECT {col_list} FROM {qi_view}");
Ok((qi_tv, insert))
}
#[pg_extern]
fn pg_tviews_ensure_propagation_indexes(
entity: default!(Option<&str>, "NULL"),
dry_run: default!(bool, false),
) -> Result<SetOfIterator<'static, String>, ErrorReport> {
crate::revision::check();
let metas = match entity {
Some(entity) => vec![
crate::catalog::TviewMeta::load_by_entity(entity)?.ok_or_else(|| {
TViewError::MetadataNotFound {
entity: entity.to_string(),
}
})?,
],
None => crate::catalog::TviewMeta::load_all()?,
};
let mut missing = Vec::new();
for meta in &metas {
let tview = format!("tv_{}", meta.entity_name);
crate::owner::require_owner(meta.tview_oid, &tview)?;
let key = &meta.identity.column;
let mut lookups = meta.plan.lookup_columns();
lookups.remove(key.as_str());
if lookups.is_empty() {
continue;
}
let (schema, table) = crate::ddl::relation_name(meta.tview_oid)?;
let mut recorded = Vec::new();
for column in lookups {
let index = crate::ddl::create::ManagedIndex::propagation(&table, column, key);
let indexed = crate::utils::spi::one::<bool>(
"SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_index i \
JOIN pg_catalog.pg_attribute a \
ON a.attrelid = i.indrelid AND a.attnum = i.indkey[0] \
WHERE i.indrelid = $1 AND a.attname = $2)",
&[
crate::utils::spi::oid(meta.tview_oid),
crate::utils::spi::text(column),
],
)?;
if indexed != Some(true) {
missing.push(index.ddl(&schema, &table));
if !dry_run {
recorded.extend(index.create(&schema, &table)?);
}
} else if !dry_run && index.exists_on(meta.tview_oid)? {
recorded.push(index.name);
}
}
if !recorded.is_empty() {
crate::catalog::indexes::record(meta.tview_oid, &recorded)?;
}
}
Ok(SetOfIterator::new(missing))
}
#[pg_extern]
fn pg_tviews_refresh_all_entities() -> Result<(), ErrorReport> {
crate::revision::check();
let order = refresh_all_in_dependency_order()?;
if order.is_empty() {
info!("No TVIEWs found to refresh");
} else {
info!("Successfully refreshed {} TVIEWs", order.len());
}
Ok(())
}
pub fn refresh_all_in_dependency_order() -> TViewResult<Vec<String>> {
let graph = crate::flush::EntityDepGraph::load()?;
for entity in &graph.topo_order {
rebuild_one(entity)?;
}
flush_after_rebuilds()?;
Ok(graph.topo_order)
}
#[pg_extern]
fn pg_tviews_show_cascade_path(
entity: &str,
) -> Result<
TableIterator<
'static,
(
name!(depth, i32),
name!(entity_name, String),
name!(depends_on, String),
),
>,
ErrorReport,
> {
crate::revision::check();
if crate::catalog::TviewMeta::load_by_entity(entity)?.is_none() {
return Err(TViewError::MetadataNotFound {
entity: entity.to_string(),
}
.into());
}
let results = Spi::connect(|client| {
let args = vec![crate::utils::spi::text(entity)];
let rows = client.select(
&format!(
"WITH RECURSIVE dep_tree AS (
SELECT
pg_tview_meta.entity,
0 as depth,
ARRAY[pg_tview_meta.entity] as path,
pg_tview_meta.entity as depends_on
FROM {meta} pg_tview_meta
WHERE pg_tview_meta.entity = $1
UNION ALL
SELECT
m.entity,
dt.depth + 1,
dt.path || m.entity,
dt.entity as depends_on
FROM dep_tree dt
JOIN {meta} m ON m.plan->'embeds'
@> pg_catalog.jsonb_build_array(pg_catalog.jsonb_build_object('entity', dt.entity))
WHERE NOT (m.entity = ANY(dt.path))
AND dt.depth < 10
)
SELECT depth, entity AS entity_name, depends_on
FROM dep_tree
ORDER BY depth, entity_name",
meta = crate::utils::meta_table()
),
None,
&args,
)?;
let mut paths = Vec::new();
for row in rows {
let depth = row["depth"].value::<i32>()?.unwrap_or(0);
let entity_name = row["entity_name"].value::<String>()?.unwrap_or_default();
let depends_on = row["depends_on"].value::<String>()?.unwrap_or_default();
paths.push((depth, entity_name, depends_on));
}
Ok::<_, spi::Error>(paths)
})
.map_err(|e| crate::utils::spi::catalog_error("Read the cascade path", &e))?;
Ok(TableIterator::new(results))
}