use std::collections::HashSet;
use crate::ir::constraint::ConstraintKind;
use crate::ir::index::IndexParent;
use crate::lint::finding::Finding;
use crate::lint::source_tree::{ObjectKey, SourceTree};
pub fn check(tree: &SourceTree) -> Vec<Finding> {
let mut out = Vec::new();
let table_names: HashSet<_> = tree
.catalog
.tables
.iter()
.map(|t| t.qname.clone())
.collect();
let mv_names: HashSet<_> = tree
.catalog
.materialized_views
.iter()
.map(|mv| mv.qname.clone())
.collect();
for table in &tree.catalog.tables {
for c in &table.constraints {
if let ConstraintKind::ForeignKey(fk) = &c.kind
&& !table_names.contains(&fk.referenced_table)
{
let loc = tree
.object_locations
.get(&ObjectKey::Table(table.qname.clone()))
.cloned();
let mut f = Finding::error(
"closed_world_references",
format!(
"FK `{constraint}` on `{owner}` references unknown table `{ref_table}`",
constraint = c.qname.name,
owner = table.qname,
ref_table = fk.referenced_table,
),
);
if let Some(l) = loc {
f = f.at(l);
}
out.push(f);
}
}
}
for idx in &tree.catalog.indexes {
let parent_known = match &idx.on {
IndexParent::Table(q) => table_names.contains(q),
IndexParent::Mv(q) => mv_names.contains(q),
};
if !parent_known {
let parent_kind = if idx.on.is_mv() {
"materialized view"
} else {
"table"
};
let mut f = Finding::error(
"closed_world_references",
format!(
"index `{idx}` references unknown {parent_kind} `{tbl}`",
idx = idx.qname,
tbl = idx.on.qname(),
),
);
if let Some(loc) = tree
.object_locations
.get(&ObjectKey::Index(idx.qname.clone()))
{
f = f.at(loc.clone());
}
out.push(f);
}
}
for seq in &tree.catalog.sequences {
if let Some(owner) = &seq.owned_by
&& !table_names.contains(&owner.table)
{
let mut f = Finding::error(
"closed_world_references",
format!(
"sequence `{seq}` is OWNED BY unknown table `{tbl}`",
seq = seq.qname,
tbl = owner.table,
),
);
if let Some(loc) = tree
.object_locations
.get(&ObjectKey::Sequence(seq.qname.clone()))
{
f = f.at(loc.clone());
}
out.push(f);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::catalog::Catalog;
use crate::ir::constraint::{
Constraint, ConstraintKind, Deferrable, FkMatchType, ForeignKey, ReferentialAction,
};
use crate::ir::schema::Schema;
use crate::ir::table::Table;
use crate::lint::test_helpers::{empty_tree, id, qn};
#[test]
fn closed_world_references_flags_dangling_fk() {
let mut c = Catalog::empty();
c.schemas.push(Schema::new(id("app")));
c.tables.push(Table {
qname: qn("app", "users"),
columns: vec![],
constraints: vec![Constraint {
qname: qn("app", "users_fk"),
kind: ConstraintKind::ForeignKey(ForeignKey {
columns: vec![id("orgs_id")],
referenced_table: qn("app", "orgs"), referenced_columns: vec![id("id")],
on_update: ReferentialAction::NoAction,
on_delete: ReferentialAction::NoAction,
match_type: FkMatchType::Simple,
}),
deferrable: Deferrable::NotDeferrable,
comment: None,
}],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
access_method: None,
tablespace: None,
});
let tree = empty_tree(c);
let findings = check(&tree);
let count_cwr = findings
.iter()
.filter(|f| f.rule == "closed_world_references")
.count();
assert_eq!(count_cwr, 1);
}
}