use crate::ir::catalog::Catalog;
use crate::lint::finding::{Finding, Severity};
pub const RULE_ID: &str = "subscription-references-undeclared-publication";
pub fn check(source: &Catalog) -> Vec<Finding> {
let pub_names: std::collections::BTreeSet<_> =
source.publications.iter().map(|p| &p.name).collect();
source
.subscriptions
.iter()
.flat_map(|s| {
s.publications
.iter()
.filter(|p| !pub_names.contains(p))
.map(move |p| Finding {
rule: RULE_ID,
severity: Severity::Warning,
message: format!(
"subscription {} references undeclared publication {}",
s.name, p,
),
location: None,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identifier::Identifier;
use crate::ir::catalog::Catalog;
use crate::ir::publication::{Publication, PublicationScope, PublishKinds};
use crate::ir::subscription::{Subscription, SubscriptionOptions};
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn make_subscription(name: &str, pubs: &[&str]) -> Subscription {
Subscription {
name: id(name),
connection: "host=x dbname=app".into(),
publications: pubs.iter().map(|p| id(p)).collect(),
options: SubscriptionOptions::default(),
owner: None,
comment: None,
}
}
fn make_publication(name: &str) -> Publication {
Publication {
name: id(name),
scope: PublicationScope::AllTables,
publish: PublishKinds::pg_default(),
publish_via_partition_root: false,
owner: None,
comment: None,
}
}
#[test]
fn no_subscriptions_silent() {
let source = Catalog::empty();
assert!(check(&source).is_empty());
}
#[test]
fn subscription_references_declared_publication_silent() {
let mut source = Catalog::empty();
source.publications.push(make_publication("my_pub"));
source
.subscriptions
.push(make_subscription("sub", &["my_pub"]));
assert!(check(&source).is_empty());
}
#[test]
fn subscription_references_undeclared_publication_fires() {
let mut source = Catalog::empty();
source
.subscriptions
.push(make_subscription("sub", &["typo_pub"]));
let findings = check(&source);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].rule, RULE_ID);
assert_eq!(findings[0].severity, Severity::Warning);
assert!(
findings[0].message.contains("typo_pub"),
"message should name the undeclared publication: {}",
findings[0].message
);
assert!(
findings[0].message.contains("sub"),
"message should name the subscription: {}",
findings[0].message
);
}
#[test]
fn partial_overlap_fires_only_for_undeclared() {
let mut source = Catalog::empty();
source.publications.push(make_publication("pub_good"));
source
.subscriptions
.push(make_subscription("sub", &["pub_good", "pub_missing"]));
let findings = check(&source);
assert_eq!(findings.len(), 1);
assert!(findings[0].message.contains("pub_missing"));
}
#[test]
fn multiple_subscriptions_with_undeclared_each_fire() {
let mut source = Catalog::empty();
source
.subscriptions
.push(make_subscription("sub_a", &["unknown_pub_a"]));
source
.subscriptions
.push(make_subscription("sub_b", &["unknown_pub_b"]));
let findings = check(&source);
assert_eq!(findings.len(), 2);
assert!(findings.iter().all(|f| f.rule == RULE_ID));
}
#[test]
fn multiple_missing_pubs_in_one_subscription_each_fire() {
let mut source = Catalog::empty();
source
.subscriptions
.push(make_subscription("sub", &["p1", "p2", "p3"]));
let findings = check(&source);
assert_eq!(findings.len(), 3);
}
}