diff --git a/crates/pglt_analyser/src/lint/safety.rs b/crates/pglt_analyser/src/lint/safety.rs index 6beb4803d..b92fc752b 100644 --- a/crates/pglt_analyser/src/lint/safety.rs +++ b/crates/pglt_analyser/src/lint/safety.rs @@ -3,4 +3,5 @@ use pglt_analyse::declare_lint_group; pub mod ban_drop_column; pub mod ban_drop_not_null; -declare_lint_group! { pub Safety { name : "safety" , rules : [self :: ban_drop_column :: BanDropColumn , self :: ban_drop_not_null :: BanDropNotNull ,] } } +pub mod ban_drop_table; +declare_lint_group! { pub Safety { name : "safety" , rules : [self :: ban_drop_column :: BanDropColumn , self :: ban_drop_not_null :: BanDropNotNull , self :: ban_drop_table :: BanDropTable ,] } } diff --git a/crates/pglt_analyser/src/lint/safety/ban_drop_table.rs b/crates/pglt_analyser/src/lint/safety/ban_drop_table.rs new file mode 100644 index 000000000..cb15609da --- /dev/null +++ b/crates/pglt_analyser/src/lint/safety/ban_drop_table.rs @@ -0,0 +1,52 @@ +use pglt_analyse::{context::RuleContext, declare_lint_rule, Rule, RuleDiagnostic, RuleSource}; +use pglt_console::markup; + +declare_lint_rule! { + /// Dropping a table may break existing clients. + /// + /// Update your application code to no longer read or write the table. + /// + /// Once the table is no longer needed, you can delete it by running the command "DROP TABLE mytable;". + /// + /// This command will permanently remove the table from the database and all its contents. + /// Be sure to back up the table before deleting it, just in case you need to restore it in the future. + /// + /// ## Examples + /// ```sql,expect_diagnostic + /// drop table some_table; + /// ``` + pub BanDropTable { + version: "next", + name: "banDropTable", + recommended: true, + sources: &[RuleSource::Squawk("ban-drop-table")], + } +} + +impl Rule for BanDropTable { + type Options = (); + + fn run(ctx: &RuleContext) -> Vec { + let mut diagnostics = vec![]; + + if let pglt_query_ext::NodeEnum::DropStmt(stmt) = &ctx.stmt() { + if stmt.remove_type() == pglt_query_ext::protobuf::ObjectType::ObjectTable { + diagnostics.push( + RuleDiagnostic::new( + rule_category!(), + None, + markup! { + "Dropping a table may break existing clients." + }, + ) + .detail( + None, + "Update your application code to no longer read or write the table, and only then delete the table. Be sure to create a backup.", + ), + ); + } + } + + diagnostics + } +} diff --git a/crates/pglt_analyser/src/options.rs b/crates/pglt_analyser/src/options.rs index 6f0ab007b..3ff797435 100644 --- a/crates/pglt_analyser/src/options.rs +++ b/crates/pglt_analyser/src/options.rs @@ -5,3 +5,4 @@ pub type BanDropColumn = ::Options; pub type BanDropNotNull = ::Options; +pub type BanDropTable = ::Options; diff --git a/crates/pglt_analyser/tests/specs/safety/banDropTable/basic.sql b/crates/pglt_analyser/tests/specs/safety/banDropTable/basic.sql new file mode 100644 index 000000000..16f6fd624 --- /dev/null +++ b/crates/pglt_analyser/tests/specs/safety/banDropTable/basic.sql @@ -0,0 +1,2 @@ +-- expect_only_lint/safety/banDropTable +drop table test; \ No newline at end of file diff --git a/crates/pglt_analyser/tests/specs/safety/banDropTable/basic.sql.snap b/crates/pglt_analyser/tests/specs/safety/banDropTable/basic.sql.snap new file mode 100644 index 000000000..47441012b --- /dev/null +++ b/crates/pglt_analyser/tests/specs/safety/banDropTable/basic.sql.snap @@ -0,0 +1,16 @@ +--- +source: crates/pglt_analyser/tests/rules_tests.rs +expression: snapshot +--- +# Input +``` +-- expect_only_lint/safety/banDropTable +drop table test; +``` + +# Diagnostics +lint/safety/banDropTable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Dropping a table may break existing clients. + + i Update your application code to no longer read or write the table, and only then delete the table. Be sure to create a backup. diff --git a/crates/pglt_configuration/src/analyser/linter/rules.rs b/crates/pglt_configuration/src/analyser/linter/rules.rs index 8e9544008..ba8754cd5 100644 --- a/crates/pglt_configuration/src/analyser/linter/rules.rs +++ b/crates/pglt_configuration/src/analyser/linter/rules.rs @@ -149,18 +149,25 @@ pub struct Safety { #[doc = "Dropping a NOT NULL constraint may break existing clients."] #[serde(skip_serializing_if = "Option::is_none")] pub ban_drop_not_null: Option>, + #[doc = "Dropping a table may break existing clients."] + #[serde(skip_serializing_if = "Option::is_none")] + pub ban_drop_table: Option>, } impl Safety { const GROUP_NAME: &'static str = "safety"; - pub(crate) const GROUP_RULES: &'static [&'static str] = &["banDropColumn", "banDropNotNull"]; - const RECOMMENDED_RULES: &'static [&'static str] = &["banDropColumn", "banDropNotNull"]; + pub(crate) const GROUP_RULES: &'static [&'static str] = + &["banDropColumn", "banDropNotNull", "banDropTable"]; + const RECOMMENDED_RULES: &'static [&'static str] = + &["banDropColumn", "banDropNotNull", "banDropTable"]; const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -187,6 +194,11 @@ impl Safety { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); } } + if let Some(rule) = self.ban_drop_table.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -201,6 +213,11 @@ impl Safety { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1])); } } + if let Some(rule) = self.ban_drop_table.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -245,6 +262,10 @@ impl Safety { .ban_drop_not_null .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "banDropTable" => self + .ban_drop_table + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), _ => None, } } diff --git a/crates/pglt_diagnostics_categories/src/categories.rs b/crates/pglt_diagnostics_categories/src/categories.rs index 5c590c428..d20a01483 100644 --- a/crates/pglt_diagnostics_categories/src/categories.rs +++ b/crates/pglt_diagnostics_categories/src/categories.rs @@ -15,6 +15,7 @@ define_categories! { "lint/safety/banDropColumn": "https://pglt.dev/linter/rules/ban-drop-column", "lint/safety/banDropNotNull": "https://pglt.dev/linter/rules/ban-drop-not-null", + "lint/safety/banDropTable": "https://pglt.dev/linter/rules/ban-drop-table", // end lint rules ; // General categories