use super::QueryGraph;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RootColumn {
pub output: String,
pub column: String,
pub attnum: i16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ForeignKey {
pub name: String,
pub attnums: Vec<i16>,
pub table: String,
pub columns: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RootConstraints {
pub foreign_keys: Vec<ForeignKey>,
pub unique: Vec<i16>,
pub not_null: Vec<i16>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Reference {
pub column: String,
pub source: String,
pub table: String,
pub key: String,
pub constraint: String,
pub unique: bool,
pub nullable: bool,
}
impl QueryGraph {
#[must_use]
pub fn root_columns(&self) -> Option<(u32, Vec<RootColumn>)> {
let [root] = self.roots.as_slice() else {
return None;
};
if self.set_operation {
return None;
}
let occ = root.key.occ;
let columns = self
.outputs
.iter()
.filter_map(|(output, column)| {
column
.as_ref()
.filter(|c| c.occ == occ)
.map(|c| RootColumn {
output: output.clone(),
column: c.name.clone(),
attnum: c.attnum,
})
})
.collect();
Some((self.occurrences[occ].relid, columns))
}
}
#[must_use]
pub fn references(columns: &[RootColumn], constraints: &RootConstraints) -> Vec<Reference> {
columns
.iter()
.flat_map(|c| {
constraints
.foreign_keys
.iter()
.filter(move |fk| fk.attnums == [c.attnum])
.filter_map(move |fk| {
let [key] = fk.columns.as_slice() else {
return None;
};
Some(Reference {
column: c.output.clone(),
source: c.column.clone(),
table: fk.table.clone(),
key: key.clone(),
constraint: fk.name.clone(),
unique: constraints.unique.contains(&c.attnum),
nullable: !constraints.not_null.contains(&c.attnum),
})
})
})
.collect()
}
pub fn root_constraints(relid: u32) -> crate::TViewResult<RootConstraints> {
use crate::utils::spi;
let table = [spi::oid(pgrx::pg_sys::Oid::from(relid))];
let foreign_keys = spi::rows(
"SELECT k.conname::pg_catalog.text, k.conkey, \
pg_catalog.format('%I.%I', n.nspname, r.relname), \
ARRAY(SELECT a.attname::pg_catalog.text \
FROM pg_catalog.unnest(k.confkey) WITH ORDINALITY u(attnum, i) \
JOIN pg_catalog.pg_attribute a \
ON a.attrelid = k.confrelid AND a.attnum = u.attnum ORDER BY u.i) \
FROM pg_catalog.pg_constraint k \
JOIN pg_catalog.pg_class r ON r.oid = k.confrelid \
JOIN pg_catalog.pg_namespace n ON n.oid = r.relnamespace \
WHERE k.conrelid = $1 AND k.contype = 'f' ORDER BY k.conname",
&table,
|row| {
Ok(Some(ForeignKey {
name: row.get::<String>(1)?.unwrap_or_default(),
attnums: row.get::<Vec<i16>>(2)?.unwrap_or_default(),
table: row.get::<String>(3)?.unwrap_or_default(),
columns: row.get::<Vec<String>>(4)?.unwrap_or_default(),
}))
},
)?
.into_iter()
.flatten()
.collect();
let unique = spi::rows(
"SELECT DISTINCT i.indkey[0] FROM pg_catalog.pg_index i \
WHERE i.indrelid = $1 AND i.indisunique AND i.indnkeyatts = 1 AND i.indkey[0] > 0 \
AND i.indpred IS NULL AND i.indexprs IS NULL",
&table,
|row| Ok(row.get::<i16>(1)?),
)?
.into_iter()
.flatten()
.collect();
let not_null = spi::rows(
"SELECT a.attnum FROM pg_catalog.pg_attribute a \
WHERE a.attrelid = $1 AND a.attnum > 0 AND a.attnotnull AND NOT a.attisdropped",
&table,
|row| Ok(row.get::<i16>(1)?),
)?
.into_iter()
.flatten()
.collect();
Ok(RootConstraints {
foreign_keys,
unique,
not_null,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn root(output: &str, column: &str, attnum: i16) -> RootColumn {
RootColumn {
output: output.into(),
column: column.into(),
attnum,
}
}
fn fk(name: &str, attnums: &[i16], table: &str, columns: &[&str]) -> ForeignKey {
ForeignKey {
name: name.into(),
attnums: attnums.to_vec(),
table: table.into(),
columns: columns.iter().map(|c| (*c).to_string()).collect(),
}
}
fn comment(foreign_keys: Vec<ForeignKey>) -> RootConstraints {
RootConstraints {
foreign_keys,
unique: vec![1],
not_null: vec![1, 2, 3],
}
}
fn columns() -> Vec<RootColumn> {
vec![
root("pk_comment", "pk_comment", 1),
root("fk_post", "fk_post", 2),
root("fk_author", "fk_author", 3),
]
}
#[test]
fn a_role_named_key_references_the_table_its_constraint_names() {
let refs = references(
&columns(),
&comment(vec![
fk(
"tb_comment_fk_author_fkey",
&[3],
"public.tb_user",
&["pk_user"],
),
fk(
"tb_comment_fk_post_fkey",
&[2],
"public.tb_post",
&["pk_post"],
),
]),
);
assert_eq!(
refs,
[
Reference {
column: "fk_post".into(),
source: "fk_post".into(),
table: "public.tb_post".into(),
key: "pk_post".into(),
constraint: "tb_comment_fk_post_fkey".into(),
unique: false,
nullable: false,
},
Reference {
column: "fk_author".into(),
source: "fk_author".into(),
table: "public.tb_user".into(),
key: "pk_user".into(),
constraint: "tb_comment_fk_author_fkey".into(),
unique: false,
nullable: false,
},
]
);
}
#[test]
fn a_column_without_a_constraint_is_no_reference() {
assert!(references(&columns(), &comment(vec![])).is_empty());
}
#[test]
fn a_composite_foreign_key_is_no_reference() {
let refs = references(
&columns(),
&comment(vec![fk(
"tb_comment_pair_fkey",
&[2, 3],
"public.tb_pair",
&["a", "b"],
)]),
);
assert!(refs.is_empty());
}
#[test]
fn unique_and_nullable_come_from_the_root_column() {
let mut constraints = comment(vec![fk(
"tb_profile_fk_user_fkey",
&[2],
"public.tb_user",
&["pk_user"],
)]);
constraints.unique.push(2);
constraints.not_null.retain(|a| *a != 2);
let refs = references(&columns(), &constraints);
assert!(refs[0].unique, "a unique column is one-to-one");
assert!(refs[0].nullable, "a column without NOT NULL is nullable");
}
#[test]
fn a_root_column_projected_twice_is_two_references() {
let refs = references(
&[root("fk_post", "fk_post", 2), root("post_pk", "fk_post", 2)],
&comment(vec![fk(
"tb_comment_fk_post_fkey",
&[2],
"public.tb_post",
&["pk_post"],
)]),
);
let outputs: Vec<&str> = refs.iter().map(|r| r.column.as_str()).collect();
assert_eq!(outputs, ["fk_post", "post_pk"]);
}
#[test]
fn quoted_names_are_kept_as_the_catalog_writes_them() {
let refs = references(
&[root("Post Ref", "Post Ref", 2)],
&comment(vec![fk(
"Comment Post",
&[2],
r#""Blog"."Post""#,
&["Post Key"],
)]),
);
assert_eq!(refs[0].table, r#""Blog"."Post""#);
assert_eq!(refs[0].key, "Post Key");
assert_eq!(refs[0].column, "Post Ref");
}
mod root_columns {
use crate::lineage::{Column, Occurrence, QueryGraph, Root};
fn occ(relid: u32) -> Occurrence {
Occurrence {
relid,
relname: format!("t{relid}"),
qualified: format!("public.t{relid}"),
unions: Vec::new(),
via_view: None,
via_tview: None,
in_sublink: false,
opaque_level: None,
matview: false,
tview_table: None,
}
}
fn col(occ: usize, attnum: i16, name: &str) -> Column {
Column {
occ,
attnum,
name: name.into(),
}
}
fn root(key: Column) -> Root {
Root {
key,
expr: None,
scope: Vec::new(),
}
}
fn self_join() -> QueryGraph {
QueryGraph {
occurrences: vec![occ(10), occ(10)],
roots: vec![root(col(0, 1, "pk_comment"))],
outputs: vec![
("pk_comment".into(), Some(col(0, 1, "pk_comment"))),
("fk_post".into(), Some(col(0, 2, "fk_post"))),
("parent_post".into(), Some(col(1, 2, "fk_post"))),
("data".into(), None),
],
..QueryGraph::default()
}
}
#[test]
fn only_the_root_occurrence_s_columns() {
let (relid, columns) = self_join().root_columns().expect("one root");
assert_eq!(relid, 10);
let outputs: Vec<&str> = columns.iter().map(|c| c.output.as_str()).collect();
assert_eq!(outputs, ["pk_comment", "fk_post"]);
}
#[test]
fn a_union_has_none() {
let mut g = self_join();
g.roots.push(root(col(1, 1, "pk_comment")));
assert!(g.root_columns().is_none());
let mut g = self_join();
g.set_operation = true;
assert!(g.root_columns().is_none());
}
}
}