use super::{PublicId, TviewMeta};
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Collection {
pub key: Rc<str>,
pub child_type: Rc<str>,
pub column: String,
pub parent_table: String,
pub parent_table_oid: pgrx::pg_sys::Oid,
pub parent_column: String,
pub parent_type: String,
pub parent_id: Option<PublicId>,
pub parent_typename: Rc<str>,
pub parent_keyed_by_pk: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParentRef {
pub key: String,
pub id: Option<String>,
}
pub type OldKeys = std::collections::HashMap<i64, Vec<Option<String>>>;
pub(super) fn collections_sql() -> String {
let schema = crate::utils::ext_schema();
format!(
"(SELECT pg_catalog.jsonb_agg(pg_catalog.jsonb_build_array( \
x.column_name, pg_catalog.format('%I.%I', pn.nspname, pc.relname), \
x.parent_table::pg_catalog.int8, x.parent_column, \
pg_catalog.format_type(pa.atttypid, NULL), \
(SELECT CASE WHEN ia.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) \
THEN 'number' ELSE 'string' END \
FROM pg_catalog.pg_attribute ia \
WHERE ia.attrelid = x.parent_table AND ia.attname = 'id' \
AND ia.attnum > 0 AND NOT ia.attisdropped), \
x.parent, pm.graphql_typename) \
ORDER BY x.column_name) \
FROM {schema}.pg_tview_references x \
JOIN {schema}.pg_tview_meta pm ON pm.entity = x.parent \
JOIN pg_catalog.pg_class pc ON pc.oid = x.parent_table \
JOIN pg_catalog.pg_namespace pn ON pn.oid = pc.relnamespace \
JOIN pg_catalog.pg_attribute pa \
ON pa.attrelid = x.parent_table AND pa.attname = x.parent_column \
WHERE x.entity = pg_tview_meta.entity AND x.chosen) AS collections"
)
}
pub(super) fn decode(
child_type: &Rc<str>,
json: Option<&serde_json::Value>,
) -> Vec<Rc<Collection>> {
let Some(serde_json::Value::Array(rows)) = json else {
return Vec::new();
};
rows.iter()
.filter_map(|row| {
let text = |i: usize| row.get(i)?.as_str().map(str::to_string);
let column = text(0)?;
let parent_column = text(3)?;
let parent_entity = text(6)?;
Some(Rc::new(Collection {
key: format!("{child_type}.{column}").into(),
child_type: Rc::clone(child_type),
column,
parent_table: text(1)?,
parent_table_oid: pgrx::pg_sys::Oid::from(
u32::try_from(row.get(2)?.as_u64()?).ok()?,
),
parent_keyed_by_pk: parent_column == format!("pk_{parent_entity}"),
parent_column,
parent_type: text(4)?,
parent_id: match row.get(5)?.as_str() {
Some("number") => Some(PublicId::Number),
Some(_) => Some(PublicId::String),
None => None,
},
parent_typename: super::typename(&parent_entity, text(7)).into(),
}))
})
.collect()
}
impl TviewMeta {
#[must_use]
pub fn returned(&self, table: &str, pre_image: bool) -> String {
format!(
"{}, {}",
self.id_text(table),
self.reference_values(table, pre_image)
)
}
#[must_use]
pub fn returned_keys(&self, table: &str) -> String {
let mut columns = vec![self.id_text(table)];
if self.publishes_collections() {
columns.extend(self.collections.iter().map(|c| {
format!(
"{table}.{}::pg_catalog.text",
crate::utils::ident::quoted(&c.column)
)
}));
}
columns.join(", ")
}
#[must_use]
pub fn publishes_collections(&self) -> bool {
!self.collections.is_empty()
&& crate::config::publish_changes() == Some(crate::queue::published::Publish::Rows)
}
#[must_use]
pub fn reference_values(&self, table: &str, pre_image: bool) -> String {
if !self.publishes_collections() {
return "NULL::pg_catalog.text[]".to_string();
}
let pairs = |row: &str| -> Vec<String> {
self.collections
.iter()
.flat_map(|c| {
let key = format!("{row}.{}", crate::utils::ident::quoted(&c.column));
let id = match c.parent_id {
Some(_) => format!(
"(SELECT p.\"id\"::pg_catalog.text FROM {} p \
WHERE p.{} OPERATOR(pg_catalog.=) {key})",
c.parent_table,
crate::utils::ident::quoted(&c.parent_column)
),
None => "NULL::pg_catalog.text".to_string(),
};
[format!("{key}::pg_catalog.text"), id]
})
.collect()
};
let new = format!("ARRAY[{}]", pairs(table).join(", "));
if !pre_image {
return new;
}
let key = crate::utils::ident::quoted(&self.identity.column);
let qi_tv = crate::utils::qualified_relname_from_oid(self.tview_oid)
.unwrap_or_else(|_| table.to_string());
format!(
"{new} OPERATOR(pg_catalog.||) COALESCE((SELECT ARRAY[{}] FROM {qi_tv} o \
WHERE o.{key} OPERATOR(pg_catalog.=) {table}.{key}), '{{}}'::pg_catalog.text[])",
pairs("o").join(", ")
)
}
#[must_use]
pub fn reference_keys(&self, table: &str) -> String {
if !self.publishes_collections() {
return "NULL::pg_catalog.text[]".to_string();
}
let keys: Vec<String> = self
.collections
.iter()
.map(|c| {
format!(
"{table}.{}::pg_catalog.text",
crate::utils::ident::quoted(&c.column)
)
})
.collect();
format!("ARRAY[{}]", keys.join(", "))
}
#[must_use]
pub fn parents(
&self,
values: &[Option<String>],
old: Option<&[Option<String>]>,
) -> Vec<(Rc<Collection>, ParentRef)> {
parents(&self.collections, values, old)
}
}
#[must_use]
pub fn key_values(keys: &[Option<String>]) -> Vec<Option<String>> {
keys.iter().flat_map(|k| [k.clone(), None]).collect()
}
#[must_use]
pub fn parents(
collections: &[Rc<Collection>],
values: &[Option<String>],
old: Option<&[Option<String>]>,
) -> Vec<(Rc<Collection>, ParentRef)> {
let n = collections.len();
let pair = |at: usize| -> Option<ParentRef> {
Some(ParentRef {
key: values.get(at)?.clone()?,
id: values.get(at + 1).cloned().flatten(),
})
};
let mut parents: Vec<(Rc<Collection>, ParentRef)> = Vec::new();
for (i, collection) in collections.iter().enumerate() {
let locked = old
.and_then(|o| o.get(i).cloned().flatten())
.map(|key| ParentRef { key, id: None });
for parent in [pair(2 * i), pair(2 * (n + i)), locked]
.into_iter()
.flatten()
{
if !parents
.iter()
.any(|(c, p)| Rc::ptr_eq(c, collection) && p.key == parent.key)
{
parents.push((Rc::clone(collection), parent));
}
}
}
parents
}
#[cfg(test)]
mod tests {
use super::*;
fn decoded() -> Vec<Rc<Collection>> {
decode(
&Rc::from("Comment"),
Some(&serde_json::json!([
[
"fk_author",
"public.tv_user",
16400,
"pk_user",
"integer",
"string",
"user",
null
],
[
"fk_post",
"public.tv_post",
16410,
"pk_post",
"integer",
null,
"post",
"Article"
],
[
"fk_doc",
"public.tv_doc",
16420,
"id",
"uuid",
"string",
"doc",
null
]
])),
)
}
fn text(values: &[Option<&str>]) -> Vec<Option<String>> {
values.iter().map(|v| v.map(str::to_string)).collect()
}
fn named(
values: &[Option<&str>],
old: Option<&[Option<&str>]>,
) -> Vec<(String, String, Option<String>)> {
let collections = decoded();
let old = old.map(text);
parents(&collections, &text(values), old.as_deref())
.into_iter()
.map(|(c, p)| (c.key.to_string(), p.key, p.id))
.collect()
}
fn row(key: &str, parent: &str, id: Option<&str>) -> (String, String, Option<String>) {
(key.to_string(), parent.to_string(), id.map(str::to_string))
}
#[test]
fn a_collection_is_keyed_by_child_type_and_column() {
let c = decoded();
assert_eq!(&*c[0].key, "Comment.fk_author");
assert_eq!(&*c[0].child_type, "Comment");
assert_eq!(c[0].parent_id, Some(PublicId::String));
assert_eq!(&*c[0].parent_typename, "User");
assert!(c[0].parent_keyed_by_pk);
assert_eq!(c[1].parent_id, None, "a parent without an id column");
assert_eq!(&*c[1].parent_typename, "Article", "the typename option");
assert!(!c[2].parent_keyed_by_pk, "a parent named by another column");
assert!(decode(&Rc::from("Comment"), None).is_empty());
}
#[test]
fn patched_keys_read_as_pairs_without_ids() {
let pairs = key_values(&text(&[Some("7"), Some("2"), None]));
assert_eq!(pairs, text(&[Some("7"), None, Some("2"), None, None, None]));
}
#[test]
fn a_recompute_names_each_parent_with_its_id() {
assert_eq!(
named(
&[Some("7"), Some("u7"), None, None, Some("d"), Some("did")],
None
),
[
row("Comment.fk_author", "7", Some("u7")),
row("Comment.fk_doc", "d", Some("did")),
]
);
}
#[test]
fn a_moved_row_names_the_parent_it_left_once() {
assert_eq!(
named(
&[Some("7"), Some("u7"), Some("2"), None, None, None],
Some(&[Some("7"), Some("1"), None])
),
[
row("Comment.fk_author", "7", Some("u7")),
row("Comment.fk_post", "2", None),
row("Comment.fk_post", "1", None),
]
);
assert_eq!(
named(
&[
Some("7"),
Some("u7"),
Some("2"),
None,
None,
None,
Some("7"),
Some("u7"),
Some("1"),
None,
None,
None,
],
None
),
[
row("Comment.fk_author", "7", Some("u7")),
row("Comment.fk_post", "2", None),
row("Comment.fk_post", "1", None),
]
);
}
}