use super::ManagedConfig;
use super::finding::Finding;
use super::rules;
use super::source_tree::SourceTree;
use crate::diff::changeset::ChangeSet;
use crate::ir::catalog::Catalog;
pub const LINT_AT_PLAN_RULES: &[&str] = &["column-position-drift"];
pub fn check_universal(tree: &SourceTree, managed: &ManagedConfig) -> Vec<Finding> {
let mut out = Vec::new();
out.extend(rules::managed_schemas_match::check(tree, managed));
out.extend(rules::no_duplicate_qnames::check(tree));
out.extend(rules::closed_world_references::check(tree));
out.extend(rules::view_shadows_table::check(tree));
out.extend(rules::mv_no_unique_index::check(tree));
out.extend(rules::view_body_references_unmanaged_schema::check(
tree, managed,
));
out.extend(rules::type_shadows_table::check(tree));
out.extend(rules::enum_value_collision::check(tree));
out.extend(rules::composite_attribute_collision::check(tree));
out.extend(rules::domain_check_references_unmanaged_type::check(
tree, managed,
));
out.extend(rules::pl_pgsql_dynamic_sql::check(tree));
out.extend(rules::procedure_contains_commit::check(tree));
out.extend(rules::function_references_unmanaged_schema::check(
tree, managed,
));
out.extend(rules::extension_version_unpinned::check(tree));
out.extend(rules::extension_references_unmanaged_schema::check(tree));
out.extend(rules::trigger_references_unmanaged_table::check(tree));
out.extend(rules::trigger_references_unmanaged_function::check(tree));
out.extend(rules::partition_references_unmanaged_parent::check(tree));
out.extend(rules::force_rls_without_policies::check(&tree.catalog));
out
}
pub fn run_drift_lints(source: &Catalog, target: &Catalog) -> Vec<Finding> {
let mut out = Vec::new();
rules::column_position_drift::check(source, target, &mut out);
out.extend(rules::unmanaged_reloption::check(source, target));
out.extend(rules::unmanaged_publication::check(source, target));
out.extend(rules::publication_captures_unmanaged_table::check(
source, target,
));
out.extend(rules::publication_row_filter_references_unmanaged_column::check(source));
out.extend(rules::unmanaged_statistic::check(source, target));
out.extend(rules::unmanaged_subscription::check(source, target));
out.extend(rules::unmanaged_collation::check(source, target));
out
}
pub fn check_changeset(cs: &ChangeSet) -> Vec<Finding> {
let mut out = Vec::new();
out.extend(rules::storage_downgrade_not_retroactive::check(cs));
out.extend(rules::compression_change_not_retroactive::check(cs));
out.extend(rules::grants_to_unmanaged_role::check(cs));
out.extend(rules::revoke_from_owner::check(cs));
out
}
pub fn check_plan_time_catalog(source: &Catalog, min_pg_version: u32) -> Vec<Finding> {
let mut out = rules::force_rls_without_policies::check(source);
out.extend(rules::publication_feature_requires_pg_version::check(
source,
min_pg_version,
));
out.extend(rules::subscription_references_undeclared_publication::check(source));
out.extend(rules::subscription_feature_requires_pg_version::check(
source,
min_pg_version,
));
out.extend(rules::subscription_password_in_source::check(source));
out.extend(rules::column_references_unmanaged_collation::check(source));
out.extend(rules::range_type_references_unmanaged_subtype::check(
source,
));
out.extend(rules::nondeterministic_collation_requires_pg_12::check(
source,
min_pg_version,
));
out.extend(rules::builtin_provider_requires_pg_17::check(
source,
min_pg_version,
));
out
}
pub fn check_universal_with_cluster(
tree: &SourceTree,
managed: &ManagedConfig,
cluster_role_names: Option<&std::collections::BTreeSet<crate::identifier::Identifier>>,
) -> Vec<Finding> {
let mut out = check_universal(tree, managed);
out.extend(rules::grant_references_unknown_role::check(
&tree.catalog,
cluster_role_names,
));
out
}
pub fn check_cluster_changeset(
source: &crate::ir::cluster::catalog::ClusterCatalog,
cs: &crate::diff::cluster::ClusterChangeSet,
) -> Vec<Finding> {
let mut out = Vec::new();
out.extend(rules::role_loses_superuser::check(cs));
out.extend(rules::role_membership_cycle::check(source, cs));
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::schema::Schema;
use crate::ir::table::Table;
use crate::ir::view::{MaterializedView, View};
use crate::lint::test_helpers::{empty_tree, id, make_function_bare, make_trigger, qn};
use crate::parse::normalize_body::NormalizedBody;
#[test]
fn trigger_references_managed_table_and_function_no_findings() {
let mut c = Catalog::empty();
c.schemas.push(Schema::new(id("app")));
c.tables.push(Table {
qname: qn("app", "orders"),
columns: vec![],
constraints: vec![],
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(),
});
c.functions.push(make_function_bare("app", "audit_fn"));
c.triggers.push(make_trigger(
"app",
"trg_orders",
"app",
"orders",
"app",
"audit_fn",
));
c.views.push(View {
qname: qn("app", "active_orders"),
columns: vec![],
body_canonical: NormalizedBody::from_sql("SELECT 1").unwrap(),
body_dependencies: vec![],
security_barrier: None,
security_invoker: None,
check_option: None,
comment: None,
raw_body: String::new(),
owner: None,
grants: vec![],
});
c.materialized_views.push(MaterializedView {
qname: qn("app", "order_summary"),
columns: vec![],
body_canonical: NormalizedBody::from_sql("SELECT 1").unwrap(),
body_dependencies: vec![],
comment: None,
raw_body: String::new(),
owner: None,
grants: vec![],
storage: crate::ir::reloptions::MaterializedViewStorageOptions::default(),
});
let tree = empty_tree(c);
let findings = check_universal(&tree, &ManagedConfig::default());
let trg_findings: Vec<_> = findings
.iter()
.filter(|f| {
f.rule == "trigger-references-unmanaged-table"
|| f.rule == "trigger-references-unmanaged-function"
})
.collect();
assert!(
trg_findings.is_empty(),
"expected no trigger lint findings when table and function are managed: {trg_findings:?}",
);
}
}