use std::collections::BTreeSet;
use super::CascadeInvalidator;
use crate::{
db::DatabaseAdapter,
error::Result,
schema::{CompiledSchema, regclass_literal},
};
#[must_use]
pub fn swept_view_names(schema: &CompiledSchema) -> BTreeSet<String> {
let queries = schema
.queries
.iter()
.flat_map(|q| q.sql_source.iter().chain(&q.additional_views));
let types = schema.types.iter().map(|t| t.sql_source.as_str());
let declared = schema.mutations.iter().flat_map(|m| &m.invalidates_views);
queries
.map(String::as_str)
.chain(types)
.chain(declared.map(String::as_str))
.filter(|name| !name.is_empty())
.map(str::to_string)
.collect()
}
const REGISTRY_PROBE: &str = "SELECT EXISTS (SELECT 1 FROM pg_attribute \
WHERE attrelid = to_regclass('tviews.registry') AND attname = 'cascade_kinds' \
AND NOT attisdropped) AS has_registry";
const REGISTRY_TVIEWS: &str = "SELECT to_regclass(quote_ident(g.schema) || '.' || \
quote_ident(g.name))::oid AS tview, b::oid AS base, \
coalesce(g.cascade_kinds ->> b::text, 'local') AS kind \
FROM tviews.registry g CROSS JOIN LATERAL unnest(g.base_tables) b";
const NO_TVIEWS: &str = "SELECT NULL::oid AS tview, NULL::oid AS base, NULL::text AS kind \
WHERE false";
pub(super) fn reads_cte(names: &BTreeSet<String>) -> String {
let src: Vec<String> = names
.iter()
.map(|name| {
format!("('{}', to_regclass('{}'))", name.replace('\'', "''"), regclass_literal(name))
})
.collect();
format!(
"WITH RECURSIVE \
src(name, rel) AS (VALUES {src}), \
roots AS (SELECT name, rel::oid AS rel FROM src WHERE rel IS NOT NULL), \
reads(root, rel) AS ( \
SELECT rel, rel FROM roots \
UNION \
SELECT r.root, d.refobjid FROM reads r \
JOIN pg_rewrite rw ON rw.ev_class = r.rel \
JOIN pg_depend d ON d.classid = 'pg_rewrite'::regclass AND d.objid = rw.oid \
AND d.refclassid = 'pg_class'::regclass AND d.refobjid <> r.rel \
)",
src = src.join(", "),
)
}
fn edges_sql(names: &BTreeSet<String>, tviews: &str) -> String {
format!(
"{reads}, \
tv AS ({tviews}), \
reads_all(root, rel) AS ( \
SELECT root, rel FROM reads \
UNION SELECT r.root, tv.base FROM reads r JOIN tv ON tv.tview = r.rel \
), \
owns_read(root, rel) AS ( \
SELECT r.root, r.rel FROM reads r JOIN pg_class c ON c.oid = r.root \
WHERE c.relkind IN ('v', 'm') OR r.rel = r.root \
), \
owns(root, rel) AS ( \
SELECT root, rel FROM owns_read \
UNION SELECT o.root, tv.base FROM owns_read o \
JOIN tv ON tv.tview = o.rel AND tv.kind = 'local' \
) \
SELECT DISTINCT v.name AS dependency, w.name AS dependent \
FROM roots v JOIN owns o ON o.root = v.rel \
JOIN reads_all a ON a.rel = o.rel \
JOIN roots w ON w.rel = a.root \
WHERE w.name <> v.name \
ORDER BY 1, 2",
reads = reads_cte(names),
)
}
pub async fn read_embedding_graph<A: DatabaseAdapter + ?Sized>(
adapter: &A,
schema: &CompiledSchema,
) -> Result<CascadeInvalidator> {
let mut graph = CascadeInvalidator::new();
let names = swept_view_names(schema);
if names.is_empty() {
return Ok(graph);
}
let has_registry = adapter
.execute_raw_query(REGISTRY_PROBE)
.await?
.first()
.and_then(|row| row.get("has_registry"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let tviews = if has_registry {
REGISTRY_TVIEWS
} else {
NO_TVIEWS
};
for row in adapter.execute_raw_query(&edges_sql(&names, tviews)).await? {
let name = |key: &str| row.get(key).and_then(serde_json::Value::as_str);
if let (Some(dependency), Some(dependent)) = (name("dependency"), name("dependent")) {
graph.add_dependency(dependent, dependency)?;
}
}
Ok(graph)
}
#[cfg(test)]
#[path = "embedding_graph_tests.rs"]
mod embedding_graph_tests;