use crate::error::{TViewError, TViewResult};
use crate::utils::ident;
use pgrx::prelude::*;
use std::collections::{HashMap, HashSet};
pub struct TviewRelation {
pub entity: String,
pub schema: String,
pub table: String,
pub unlogged: bool,
pub table_oid: pg_sys::Oid,
}
impl TviewRelation {
pub fn load(entity: Option<&str>) -> TViewResult<Vec<Self>> {
Ok(crate::catalog::queries::tables(entity)?
.into_iter()
.map(|t| Self {
entity: t.entity,
schema: t.schema,
table: t.name,
unlogged: t.unlogged,
table_oid: t.oid,
})
.collect())
}
fn qualified(&self, name: &str) -> String {
format!("{}.{}", ident::quoted(&self.schema), ident::quoted(name))
}
pub fn table_is_empty(&self) -> TViewResult<bool> {
has_no_rows(&self.qualified(&self.table))
}
pub fn needs_rebuild(&self) -> TViewResult<bool> {
crate::lifecycle::validity::needs_fill(&self.entity, self.table_oid)
}
}
fn has_no_rows(qualified: &str) -> TViewResult<bool> {
let sql = format!("SELECT NOT EXISTS (SELECT 1 FROM {qualified}) AS empty");
Spi::connect(|client| {
let rows = client.select(&sql, Some(1), &[])?;
rows.first().get_one::<bool>()
})
.map(|v| v.unwrap_or(true))
.map_err(|e| TViewError::SpiError {
query: sql,
error: e.to_string(),
})
}
fn in_recovery() -> bool {
unsafe { pg_sys::RecoveryInProgress() }
}
#[allow(clippy::type_complexity)] pub(crate) fn replication_status()
-> TViewResult<Vec<(String, String, bool, Option<bool>, Option<bool>)>> {
let recovering = in_recovery();
let relations = TviewRelation::load(None)?;
let mut rows = Vec::new();
for rel in relations {
let readable = !rel.unlogged;
let is_empty = if recovering && rel.unlogged {
None
} else {
Some(rel.table_is_empty()?)
};
let needs_rebuild = if recovering {
None
} else {
Some(rel.needs_rebuild()?)
};
let persistence = if rel.unlogged { "unlogged" } else { "logged" };
rows.push((
rel.entity,
persistence.to_string(),
readable,
is_empty,
needs_rebuild,
));
}
Ok(rows)
}
pub fn rebuild_all(only_empty: bool) -> TViewResult<Vec<(String, i64)>> {
if in_recovery() {
return Err(TViewError::InvalidInput {
parameter: "pg_tviews_rebuild_all".to_string(),
reason: "cannot rebuild TVIEWs during recovery; run it on the primary \
(or after promotion)"
.to_string(),
});
}
let mut targets = TviewRelation::load(None)?;
let graph = crate::cache::graph()?;
let order = dependencies_first(&graph.children);
targets.sort_by_key(|rel| {
order
.iter()
.position(|e| e == &rel.entity)
.unwrap_or(usize::MAX)
});
let mut rebuilt = Vec::new();
for rel in targets {
let inserted = if only_empty {
match crate::lifecycle::validity::fill_if_reset(&rel.entity)? {
Some(inserted) => inserted,
None => continue,
}
} else {
crate::refresh::full::rebuild_one(&rel.entity)?
};
rebuilt.push((rel.entity, i64::try_from(inserted).unwrap_or(i64::MAX)));
}
if rebuilt.is_empty() {
return Ok(rebuilt);
}
crate::admin::flush_after_rebuilds()?;
Ok(rebuilt)
}
fn dependencies_first(depends_on: &HashMap<String, Vec<String>>) -> Vec<String> {
fn visit(
entity: &str,
depends_on: &HashMap<String, Vec<String>>,
seen: &mut HashSet<String>,
out: &mut Vec<String>,
) {
if !seen.insert(entity.to_string()) {
return;
}
let mut deps = depends_on.get(entity).cloned().unwrap_or_default();
deps.sort();
for dep in &deps {
visit(dep, depends_on, seen, out);
}
out.push(entity.to_string());
}
let mut roots: Vec<&String> = depends_on.keys().collect();
roots.sort();
let (mut seen, mut out) = (HashSet::new(), Vec::new());
for entity in roots {
visit(entity, depends_on, &mut seen, &mut out);
}
out
}
#[cfg(test)]
mod tests {
use super::dependencies_first;
use std::collections::HashMap;
#[test]
fn dependencies_come_first() {
let depends_on = HashMap::from([
(
"comment".to_string(),
vec!["post".to_string(), "user".to_string()],
),
("post".to_string(), vec!["user".to_string()]),
]);
assert_eq!(dependencies_first(&depends_on), ["user", "post", "comment"]);
}
}