pub mod collections;
pub mod graph;
pub(crate) mod indexes;
pub mod plan;
pub mod queries;
pub mod reads;
pub mod registered;
pub mod resolve;
pub mod row;
pub use collections::Collection;
pub use graph::EntityDepGraph;
use pgrx::pg_sys::Oid;
use pgrx::prelude::*;
use std::rc::Rc;
#[must_use]
pub fn meta_table() -> String {
format!("{}.pg_tview_meta", crate::utils::ext_schema())
}
#[derive(Debug, Clone)]
pub struct TviewMeta {
pub tview_oid: Oid,
pub view_oid: Oid,
pub entity_name: String,
pub plan: plan::TviewPlan,
pub uncascaded_policy: crate::config::UncascadedPolicy,
pub table_policies: Vec<(Oid, crate::config::UncascadedPolicy)>,
pub function_reads: Vec<(String, Vec<Oid>)>,
pub time_refresh_external: bool,
pub time_dependent: bool,
pub identity: RowIdentity,
pub typename: std::rc::Rc<str>,
pub public_id: Option<PublicId>,
pub collections: Vec<Rc<Collection>>,
}
#[derive(Debug, Clone, Default)]
pub struct PlanEntry {
pub entity: String,
pub plan: plan::TviewPlan,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PublicId {
Number,
String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RowId {
pub text: String,
pub kind: PublicId,
}
impl RowId {
#[must_use]
pub fn json(&self) -> String {
match self.kind {
PublicId::Number => self.text.clone(),
PublicId::String => serde_json::Value::String(self.text.clone()).to_string(),
}
}
#[must_use]
pub fn value(&self) -> serde_json::Value {
match self.kind {
PublicId::Number => serde_json::from_str(&self.text)
.unwrap_or_else(|_| serde_json::Value::String(self.text.clone())),
PublicId::String => serde_json::Value::String(self.text.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RowIdentity {
pub column: String,
pub kind: crate::lineage::IdentityKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyType {
Int,
Text(String),
}
impl RowIdentity {
pub fn from_catalog(
entity: &str,
json: Option<&serde_json::Value>,
) -> crate::TViewResult<Self> {
let unreadable = |why: &str| crate::TViewError::CatalogError {
operation: format!("Read the row identity of tv_{entity}"),
pg_error: why.to_string(),
};
let json = json.ok_or_else(|| unreadable("it is NULL"))?;
let column = json["columns"][0]["name"]
.as_str()
.ok_or_else(|| unreadable("it names no column"))?
.to_string();
let kind = match json["kind"].as_str() {
Some("pk") => crate::lineage::IdentityKind::Pk,
Some("distinct_on") => crate::lineage::IdentityKind::DistinctOn,
other => return Err(unreadable(&format!("unknown kind {other:?}"))),
};
Ok(Self { column, kind })
}
const fn unknown() -> Self {
Self {
column: String::new(),
kind: crate::lineage::IdentityKind::Pk,
}
}
#[must_use]
pub fn is_pk(&self, entity: &str) -> bool {
self.column == format!("pk_{entity}")
}
}
pub(crate) fn meta_select() -> String {
let collections = if crate::revision::in_own_script() {
"NULL::pg_catalog.jsonb AS collections".to_string()
} else {
collections::collections_sql()
};
format!(
"SELECT table_oid::oid AS tview_oid, view_oid::oid AS view_oid, entity, plan, \
uncascaded_policy, identity, \
uncascaded_table_oids::oid[] AS uncascaded_table_oids, uncascaded_table_policies, \
function_read_functions, function_read_tables::oid[] AS function_read_tables, \
time_refresh IS NOT DISTINCT FROM 'external' AS time_refresh_external, \
time_dependent, graphql_typename, \
(SELECT a.atttypid IN ('pg_catalog.int2'::pg_catalog.regtype, \
'pg_catalog.int4'::pg_catalog.regtype, \
'pg_catalog.int8'::pg_catalog.regtype, \
'pg_catalog.numeric'::pg_catalog.regtype) \
FROM pg_catalog.pg_attribute a WHERE a.attrelid = table_oid \
AND a.attname = 'id' AND a.attnum > 0 AND NOT a.attisdropped) AS public_id_number, \
{collections} \
FROM {}",
crate::catalog::meta_table()
)
}
fn cached(entity: &str) -> Option<Rc<TviewMeta>> {
if !crate::config::table_cache_enabled() {
return None;
}
let meta = crate::cache::METAS.with(|m| m.get(&entity.to_string()));
if meta.is_some() {
crate::metrics::metrics_api::record_table_cache_hit();
} else {
crate::metrics::metrics_api::record_table_cache_miss();
}
meta
}
fn remember(loaded: Option<TviewMeta>) -> Option<Rc<TviewMeta>> {
crate::metrics::metrics_api::record_catalog_lookup();
let loaded = loaded.map(Rc::new);
if crate::revision::in_own_script() {
return loaded;
}
if let Some(meta) = &loaded {
crate::cache::watch(&[meta.tview_oid, meta.view_oid]);
let parents: Vec<Oid> = meta
.collections
.iter()
.map(|c| c.parent_table_oid)
.collect();
crate::cache::watch(&parents);
crate::cache::METAS.with(|m| m.insert(meta.entity_name.clone(), meta.clone()));
}
loaded
}
impl TviewMeta {
#[must_use]
pub fn id_text(&self, table: &str) -> String {
if self.public_id.is_some() {
format!("{table}.\"id\"::pg_catalog.text")
} else {
"NULL::pg_catalog.text".to_string()
}
}
#[must_use]
pub fn row_id(&self, text: String) -> RowId {
RowId {
text,
kind: self.public_id.unwrap_or(PublicId::String),
}
}
pub fn key_type(&self) -> crate::TViewResult<KeyType> {
if let Some(key_type) = crate::cache::KEY_TYPES.with(|m| m.get(&self.tview_oid)) {
return Ok(key_type);
}
let name = Spi::get_one_with_args::<String>(
"SELECT CASE WHEN a.atttypid IN ('pg_catalog.int2'::pg_catalog.regtype, \
'pg_catalog.int4'::pg_catalog.regtype, \
'pg_catalog.int8'::pg_catalog.regtype) THEN NULL \
ELSE pg_catalog.quote_ident(n.nspname) || '.' || pg_catalog.quote_ident(t.typname) END \
FROM pg_catalog.pg_attribute a \
JOIN pg_catalog.pg_type t ON t.oid = a.atttypid \
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace \
WHERE a.attrelid = $1 AND a.attname = $2 AND NOT a.attisdropped",
&[
crate::utils::spi::oid(self.tview_oid),
crate::utils::spi::text(self.identity.column.as_str()),
],
)
.or_else(|e| match e {
spi::Error::InvalidPosition => Ok(None),
e => Err(e),
})?;
let key_type = name.map_or(KeyType::Int, KeyType::Text);
crate::cache::KEY_TYPES.with(|m| m.insert(self.tview_oid, key_type.clone()));
Ok(key_type)
}
#[must_use]
pub fn policy_for(&self, table_oid: Oid) -> crate::config::UncascadedPolicy {
self.table_policies
.iter()
.find(|(oid, _)| *oid == table_oid)
.map_or(self.uncascaded_policy, |(_, policy)| *policy)
}
#[must_use]
pub fn key_mapping(
&self,
table_oid: Oid,
root: Option<Oid>,
) -> Option<&crate::lineage::KeyMapping> {
self.plan
.tables
.iter()
.find(|m| m.relid == table_oid.to_u32() || root.is_some_and(|r| m.relid == r.to_u32()))
}
pub fn load_by_entity(entity_name: &str) -> crate::TViewResult<Option<Rc<Self>>> {
if let Some(meta) = cached(entity_name) {
return Ok(Some(meta));
}
let loaded = Spi::connect(|client| -> crate::TViewResult<Option<Self>> {
let args = vec![crate::utils::spi::text(entity_name)];
let mut rows =
client.select(&format!("{} WHERE entity = $1", meta_select()), None, &args)?;
match rows.next() {
Some(row) => Ok(Some(Self::from_spi_row(&row)?)),
None => Ok(None),
}
})?;
Ok(remember(loaded))
}
pub fn load_all() -> crate::TViewResult<Vec<Self>> {
Spi::connect(|client| -> crate::TViewResult<Vec<Self>> {
let rows = client.select(&format!("{} ORDER BY entity", meta_select()), None, &[])?;
let mut result = Vec::new();
for row in rows {
result.push(Self::from_spi_row(&row)?);
}
Ok(result)
})
}
pub fn load_plans() -> crate::TViewResult<Vec<PlanEntry>> {
Spi::connect(|client| -> crate::TViewResult<Vec<PlanEntry>> {
let rows = client.select(
&format!(
"SELECT entity, plan FROM {} ORDER BY entity",
crate::catalog::meta_table()
),
None,
&[],
)?;
let mut entries = Vec::new();
for row in rows {
let entity = row["entity"].value::<String>()?.unwrap_or_default();
let plan = plan::TviewPlan::decode(
&entity,
row["plan"]
.value::<pgrx::JsonB>()?
.map_or(serde_json::Value::Null, |j| j.0),
)?;
entries.push(PlanEntry { entity, plan });
}
Ok(entries)
})
}
pub fn unreadable() -> crate::TViewResult<Vec<(String, String)>> {
Spi::connect(|client| -> crate::TViewResult<Vec<(String, String)>> {
let rows = client.select(&format!("{} ORDER BY entity", meta_select()), None, &[])?;
let mut unreadable = Vec::new();
for row in rows {
if let Err(e) = Self::from_spi_row(&row) {
let entity = row["entity"].value::<String>()?.unwrap_or_default();
unreadable.push((entity, e.to_string()));
}
}
Ok(unreadable)
})
}
pub fn entity_of_table(tview_oid: Oid) -> crate::TViewResult<Option<String>> {
crate::revision::check();
crate::utils::spi::one::<String>(
&format!(
"SELECT entity FROM {} WHERE table_oid::pg_catalog.oid = $1",
crate::catalog::meta_table()
),
&[crate::utils::spi::oid(tview_oid)],
)
}
pub fn load_to_rederive(entity_name: &str) -> crate::TViewResult<Option<Self>> {
Spi::connect(|client| -> crate::TViewResult<Option<Self>> {
let args = vec![crate::utils::spi::text(entity_name)];
let mut rows =
client.select(&format!("{} WHERE entity = $1", meta_select()), None, &args)?;
rows.next()
.map(|row| Self::decode(&row, Derived::Rederive))
.transpose()
})
}
pub fn from_spi_row(row: &spi::SpiHeapTupleData) -> crate::TViewResult<Self> {
Self::decode(row, Derived::Strict)
}
fn decode(row: &spi::SpiHeapTupleData, derived: Derived) -> crate::TViewResult<Self> {
let entity_name: String =
row["entity"]
.value()?
.ok_or_else(|| crate::TViewError::SpiError {
query: String::new(),
error: "entity column is NULL".to_string(),
})?;
let policy = |name: &str| {
crate::config::UncascadedPolicy::parse(name).ok_or_else(|| {
crate::TViewError::CatalogError {
operation: format!("Read the uncascaded policies of tv_{entity_name}"),
pg_error: format!("unknown policy {name:?}"),
}
})
};
let uncascaded_policy = policy(
&row["uncascaded_policy"]
.value::<String>()?
.unwrap_or_default(),
)?;
let table_policies = row["uncascaded_table_oids"]
.value::<Vec<Oid>>()?
.unwrap_or_default()
.into_iter()
.zip(
row["uncascaded_table_policies"]
.value::<Vec<String>>()?
.unwrap_or_default(),
)
.map(|(oid, name)| Ok((oid, policy(&name)?)))
.collect::<crate::TViewResult<_>>()?;
let mut function_reads: Vec<(String, Vec<Oid>)> = Vec::new();
for (function, table) in row["function_read_functions"]
.value::<Vec<String>>()?
.unwrap_or_default()
.into_iter()
.zip(
row["function_read_tables"]
.value::<Vec<Option<Oid>>>()?
.unwrap_or_default(),
)
{
if function_reads.last().is_none_or(|(f, _)| *f != function) {
function_reads.push((function, Vec::new()));
}
if let (Some(table), Some((_, tables))) = (table, function_reads.last_mut()) {
tables.push(table);
}
}
let identity = RowIdentity::from_catalog(
&entity_name,
row["identity"]
.value::<pgrx::JsonB>()?
.map(|j| j.0)
.as_ref(),
);
let identity = match (identity, derived) {
(Ok(identity), _) => identity,
(Err(_), Derived::Rederive) => RowIdentity::unknown(),
(Err(e), Derived::Strict) => return Err(e),
};
let plan = plan::TviewPlan::decode(
&entity_name,
row["plan"]
.value::<pgrx::JsonB>()?
.map_or(serde_json::Value::Null, |j| j.0),
);
let plan = match (plan, derived) {
(Ok(plan), _) => plan,
(Err(_), Derived::Rederive) => plan::TviewPlan::default(),
(Err(e), Derived::Strict) => return Err(e),
};
let typename: Rc<str> =
typename(&entity_name, row["graphql_typename"].value::<String>()?).into();
let collections = collections::decode(
&typename,
row["collections"]
.value::<pgrx::JsonB>()?
.map(|j| j.0)
.as_ref(),
);
Ok(Self {
tview_oid: row["tview_oid"]
.value()?
.ok_or_else(|| crate::TViewError::SpiError {
query: String::new(),
error: "tview_oid column is NULL".to_string(),
})?,
view_oid: row["view_oid"]
.value()?
.ok_or_else(|| crate::TViewError::SpiError {
query: String::new(),
error: "view_oid column is NULL".to_string(),
})?,
entity_name,
plan,
uncascaded_policy,
table_policies,
function_reads,
time_refresh_external: row["time_refresh_external"]
.value::<bool>()?
.unwrap_or(false),
time_dependent: row["time_dependent"].value::<bool>()?.unwrap_or(false),
identity,
typename,
public_id: row["public_id_number"].value::<bool>()?.map(|number| {
if number {
PublicId::Number
} else {
PublicId::String
}
}),
collections,
})
}
}
#[derive(Clone, Copy)]
enum Derived {
Strict,
Rederive,
}
impl Default for TviewMeta {
fn default() -> Self {
Self {
tview_oid: pg_sys::Oid::INVALID,
view_oid: pg_sys::Oid::INVALID,
entity_name: String::new(),
plan: plan::TviewPlan::default(),
uncascaded_policy: crate::config::UncascadedPolicy::Warn,
table_policies: Vec::new(),
function_reads: Vec::new(),
time_refresh_external: false,
time_dependent: false,
identity: RowIdentity::unknown(),
typename: "".into(),
public_id: None,
collections: Vec::new(),
}
}
}
#[must_use]
pub fn typename(entity: &str, declared: Option<String>) -> String {
declared.unwrap_or_else(|| {
entity
.split('_')
.filter(|part| !part.is_empty())
.map(|part| {
let mut chars = part.chars();
chars.next().map_or_else(String::new, |first| {
first.to_ascii_uppercase().to_string() + chars.as_str()
})
})
.collect()
})
}
#[cfg(test)]
mod tests {
#[test]
fn a_public_id_is_written_in_json_as_to_jsonb_writes_it() {
let meta = |public_id| super::TviewMeta {
public_id,
..super::TviewMeta::default()
};
let number = meta(Some(super::PublicId::Number));
let string = meta(Some(super::PublicId::String));
assert_eq!(number.row_id("42".into()).json(), "42");
assert_eq!(number.row_id("42".into()).value(), serde_json::json!(42));
assert_eq!(string.row_id("8f0c".into()).json(), r#""8f0c""#);
assert_eq!(string.row_id(r#"a"b\c"#.into()).json(), r#""a\"b\\c""#);
assert_eq!(
string.row_id("8f0c".into()).value(),
serde_json::json!("8f0c")
);
assert_eq!(meta(None).id_text("t"), "NULL::pg_catalog.text");
assert_eq!(string.id_text("t"), r#"t."id"::pg_catalog.text"#);
}
#[test]
fn typename_is_the_option_or_the_pascal_case_of_the_entity() {
assert_eq!(super::typename("post", None), "Post");
assert_eq!(super::typename("blog_post", None), "BlogPost");
assert_eq!(super::typename("order_line_item", None), "OrderLineItem");
assert_eq!(super::typename("post", Some("Article".into())), "Article");
}
#[test]
fn typename_capitalises_ascii_letters_only_whatever_the_locale() {
assert_eq!(super::typename("item", None), "Item");
assert_eq!(super::typename("été", None), "été");
assert_eq!(super::typename("ßeta_x", None), "ßetaX");
assert_eq!(super::typename("_post__line_", None), "PostLine");
}
use super::*;
#[test]
fn row_identity_reads_the_catalog() {
use crate::lineage::IdentityKind;
let doc =
serde_json::json!({"kind": "distinct_on", "columns": [{"name": "id", "type": "uuid"}]});
let id = RowIdentity::from_catalog("doc", Some(&doc)).unwrap();
assert_eq!(id.column, "id");
assert_eq!(id.kind, IdentityKind::DistinctOn);
assert!(!id.is_pk("doc"));
let pk = serde_json::json!({"kind": "pk", "columns": [{"name": "pk_doc"}]});
assert!(
RowIdentity::from_catalog("doc", Some(&pk))
.unwrap()
.is_pk("doc")
);
}
#[test]
fn an_unreadable_row_identity_is_a_catalog_error() {
let unknown = serde_json::json!({"kind": "natural", "columns": [{"name": "id"}]});
for json in [
None,
Some(&serde_json::json!({"kind": "pk"})),
Some(&unknown),
] {
let err = RowIdentity::from_catalog("doc", json).unwrap_err();
assert!(err.to_string().contains("tv_doc"), "{err}");
}
}
}