From df51f5f209541b98b10e12231e228cf46c015365 Mon Sep 17 00:00:00 2001 From: t-monaghan Date: Thu, 1 Oct 2026 11:33:36 +1000 Subject: [PATCH 1/2] feat(linter): add compatibility rules for dropped objects and defaults --- crates/squawk_linter/src/lib.rs | 44 +++++++++++++-- .../src/rules/ban_drop_default.rs | 54 +++++++++++++++++++ .../src/rules/ban_drop_function.rs | 53 ++++++++++++++++++ .../src/rules/ban_drop_trigger.rs | 45 ++++++++++++++++ .../squawk_linter/src/rules/ban_drop_type.rs | 45 ++++++++++++++++ .../squawk_linter/src/rules/ban_drop_view.rs | 53 ++++++++++++++++++ crates/squawk_linter/src/rules/mod.rs | 10 ++++ ...r__rules__ban_drop_default__test__err.snap | 20 +++++++ ...__rules__ban_drop_function__test__err.snap | 20 +++++++ ...r__rules__ban_drop_trigger__test__err.snap | 12 +++++ ...nter__rules__ban_drop_type__test__err.snap | 12 +++++ ...nter__rules__ban_drop_view__test__err.snap | 20 +++++++ 12 files changed, 385 insertions(+), 3 deletions(-) create mode 100644 crates/squawk_linter/src/rules/ban_drop_default.rs create mode 100644 crates/squawk_linter/src/rules/ban_drop_function.rs create mode 100644 crates/squawk_linter/src/rules/ban_drop_trigger.rs create mode 100644 crates/squawk_linter/src/rules/ban_drop_type.rs create mode 100644 crates/squawk_linter/src/rules/ban_drop_view.rs create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_default__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_function__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_trigger__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_type__test__err.snap create mode 100644 crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_view__test__err.snap diff --git a/crates/squawk_linter/src/lib.rs b/crates/squawk_linter/src/lib.rs index af7dcbf96..16c004c14 100644 --- a/crates/squawk_linter/src/lib.rs +++ b/crates/squawk_linter/src/lib.rs @@ -37,8 +37,13 @@ use rules::ban_concurrent_index_creation_in_transaction; use rules::ban_create_domain_with_constraint; use rules::ban_drop_column; use rules::ban_drop_database; +use rules::ban_drop_default; +use rules::ban_drop_function; use rules::ban_drop_not_null; use rules::ban_drop_table; +use rules::ban_drop_trigger; +use rules::ban_drop_type; +use rules::ban_drop_view; use rules::ban_duplicate_column_assignments; use rules::ban_truncate_cascade; use rules::ban_uncommitted_transaction; @@ -107,6 +112,11 @@ pub enum Rule { RequireLockTimeout, RequireStatementTimeout, BanDuplicateColumnAssignments, + BanDropView, + BanDropFunction, + BanDropType, + BanDropDefault, + BanDropTrigger, // xtask:new-rule:error-name } @@ -117,7 +127,7 @@ impl Rule { // require-timeout-settings is an alias, see `Rule::expands_to` matches!( self, - Rule::RequireTableSchema | Rule::RequireTimeoutSettings + Rule::RequireTableSchema | Rule::RequireTimeoutSettings | Rule::BanDropTrigger ) } @@ -180,6 +190,11 @@ impl TryFrom<&str> for Rule { "require-lock-timeout" => Ok(Rule::RequireLockTimeout), "require-statement-timeout" => Ok(Rule::RequireStatementTimeout), "ban-duplicate-column-assignments" => Ok(Rule::BanDuplicateColumnAssignments), + "ban-drop-view" => Ok(Rule::BanDropView), + "ban-drop-function" => Ok(Rule::BanDropFunction), + "ban-drop-type" => Ok(Rule::BanDropType), + "ban-drop-default" => Ok(Rule::BanDropDefault), + "ban-drop-trigger" => Ok(Rule::BanDropTrigger), // xtask:new-rule:str-name _ => Err(format!("Unknown violation name: {s}")), } @@ -251,6 +266,11 @@ impl fmt::Display for Rule { Rule::RequireLockTimeout => "require-lock-timeout", Rule::RequireStatementTimeout => "require-statement-timeout", Rule::BanDuplicateColumnAssignments => "ban-duplicate-column-assignments", + Rule::BanDropView => "ban-drop-view", + Rule::BanDropFunction => "ban-drop-function", + Rule::BanDropType => "ban-drop-type", + Rule::BanDropDefault => "ban-drop-default", + Rule::BanDropTrigger => "ban-drop-trigger", // xtask:new-rule:variant-to-name }; write!(f, "{val}") @@ -505,6 +525,21 @@ impl Linter { if self.rules.contains(&Rule::BanDuplicateColumnAssignments) { ban_duplicate_column_assignments(self, file); } + if self.rules.contains(&Rule::BanDropView) { + ban_drop_view(self, file); + } + if self.rules.contains(&Rule::BanDropFunction) { + ban_drop_function(self, file); + } + if self.rules.contains(&Rule::BanDropType) { + ban_drop_type(self, file); + } + if self.rules.contains(&Rule::BanDropDefault) { + ban_drop_default(self, file); + } + if self.rules.contains(&Rule::BanDropTrigger) { + ban_drop_trigger(self, file); + } // xtask:new-rule:rule-call // locate any ignores in the file @@ -599,12 +634,15 @@ mod tests { fn with_rules_opt_in_disabled_by_default() { let linter = Linter::with_rules(&[], &[]); assert!(!linter.rules.contains(&Rule::RequireTableSchema)); + assert!(!linter.rules.contains(&Rule::BanDropTrigger)); } #[test] fn with_rules_opt_in_enabled_via_include() { - let linter = Linter::with_rules(&[Rule::RequireTableSchema], &[]); - assert!(linter.rules.contains(&Rule::RequireTableSchema)); + for rule in [Rule::RequireTableSchema, Rule::BanDropTrigger] { + let linter = Linter::with_rules(&[rule], &[]); + assert!(linter.rules.contains(&rule)); + } } #[test] diff --git a/crates/squawk_linter/src/rules/ban_drop_default.rs b/crates/squawk_linter/src/rules/ban_drop_default.rs new file mode 100644 index 000000000..b43165db6 --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_drop_default.rs @@ -0,0 +1,54 @@ +use squawk_syntax::{ + Parse, SourceFile, + ast::{self, AstNode}, +}; + +use crate::{Linter, Rule, Violation}; + +pub(crate) fn ban_drop_default(ctx: &mut Linter, parse: &Parse) { + for stmt in parse.tree().stmts() { + if let ast::Stmt::AlterTable(alter_table) = stmt { + for action in alter_table.actions() { + if let ast::AlterTableAction::AlterColumn(alter_column) = action { + if let Some(ast::AlterColumnOption::DropDefault(drop_default)) = + alter_column.option() + { + ctx.report(Violation::for_node( + Rule::BanDropDefault, + "Dropping a column default may break existing clients.".into(), + drop_default.syntax(), + )); + } + } + } + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_snapshot; + + use crate::{ + Rule, + test_utils::{lint_errors, lint_ok}, + }; + + #[test] + fn err() { + let sql = r#" +ALTER TABLE tbl ALTER COLUMN c DROP DEFAULT; +ALTER TABLE IF EXISTS tbl ALTER COLUMN c DROP DEFAULT; +ALTER TABLE tbl ALTER COLUMN c DROP DEFAULT, ALTER COLUMN d DROP DEFAULT; +"#; + assert_snapshot!(lint_errors(sql, Rule::BanDropDefault)); + } + + #[test] + fn ok() { + lint_ok( + "ALTER TABLE tbl ALTER COLUMN c SET DEFAULT 1; ALTER TABLE tbl ALTER COLUMN c DROP NOT NULL; DROP DOMAIN d; ALTER DOMAIN d DROP DEFAULT; DROP INDEX i;", + Rule::BanDropDefault, + ); + } +} diff --git a/crates/squawk_linter/src/rules/ban_drop_function.rs b/crates/squawk_linter/src/rules/ban_drop_function.rs new file mode 100644 index 000000000..2191708e1 --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_drop_function.rs @@ -0,0 +1,53 @@ +use squawk_syntax::{ + Parse, SourceFile, + ast::{self, AstNode}, +}; + +use crate::{Linter, Rule, Violation}; + +pub(crate) fn ban_drop_function(ctx: &mut Linter, parse: &Parse) { + for stmt in parse.tree().stmts() { + match stmt { + ast::Stmt::DropFunction(node) => ctx.report(Violation::for_node( + Rule::BanDropFunction, + "Dropping a function may break existing clients.".into(), + node.syntax(), + )), + ast::Stmt::DropProcedure(node) => ctx.report(Violation::for_node( + Rule::BanDropFunction, + "Dropping a function may break existing clients.".into(), + node.syntax(), + )), + _ => (), + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_snapshot; + + use crate::{ + Rule, + test_utils::{lint_errors, lint_ok}, + }; + + #[test] + fn err() { + let sql = r#" +DROP FUNCTION f(int); +DROP FUNCTION IF EXISTS f(int) CASCADE; +DROP PROCEDURE p(int); +DROP PROCEDURE IF EXISTS p(int) CASCADE; +"#; + assert_snapshot!(lint_errors(sql, Rule::BanDropFunction)); + } + + #[test] + fn ok() { + lint_ok( + "DROP INDEX i; CREATE VIEW v AS SELECT 1;", + Rule::BanDropFunction, + ); + } +} diff --git a/crates/squawk_linter/src/rules/ban_drop_trigger.rs b/crates/squawk_linter/src/rules/ban_drop_trigger.rs new file mode 100644 index 000000000..f6c9f6a34 --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_drop_trigger.rs @@ -0,0 +1,45 @@ +use squawk_syntax::{ + Parse, SourceFile, + ast::{self, AstNode}, +}; + +use crate::{Linter, Rule, Violation}; + +pub(crate) fn ban_drop_trigger(ctx: &mut Linter, parse: &Parse) { + for stmt in parse.tree().stmts() { + if let ast::Stmt::DropTrigger(node) = stmt { + ctx.report(Violation::for_node( + Rule::BanDropTrigger, + "Dropping a trigger may silently change behaviour for existing clients.".into(), + node.syntax(), + )); + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_snapshot; + + use crate::{ + Rule, + test_utils::{lint_errors, lint_ok}, + }; + + #[test] + fn err() { + let sql = r#" +DROP TRIGGER trg ON tbl; +DROP TRIGGER IF EXISTS trg ON tbl CASCADE; +"#; + assert_snapshot!(lint_errors(sql, Rule::BanDropTrigger)); + } + + #[test] + fn ok() { + lint_ok( + "DROP INDEX i; CREATE VIEW v AS SELECT 1;", + Rule::BanDropTrigger, + ); + } +} diff --git a/crates/squawk_linter/src/rules/ban_drop_type.rs b/crates/squawk_linter/src/rules/ban_drop_type.rs new file mode 100644 index 000000000..f5283da1e --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_drop_type.rs @@ -0,0 +1,45 @@ +use squawk_syntax::{ + Parse, SourceFile, + ast::{self, AstNode}, +}; + +use crate::{Linter, Rule, Violation}; + +pub(crate) fn ban_drop_type(ctx: &mut Linter, parse: &Parse) { + for stmt in parse.tree().stmts() { + if let ast::Stmt::DropType(node) = stmt { + ctx.report(Violation::for_node( + Rule::BanDropType, + "Dropping a type may break existing clients.".into(), + node.syntax(), + )); + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_snapshot; + + use crate::{ + Rule, + test_utils::{lint_errors, lint_ok}, + }; + + #[test] + fn err() { + let sql = r#" +DROP TYPE t; +DROP TYPE IF EXISTS t CASCADE; +"#; + assert_snapshot!(lint_errors(sql, Rule::BanDropType)); + } + + #[test] + fn ok() { + lint_ok( + "CREATE TYPE t AS ENUM ('a'); DROP INDEX i;", + Rule::BanDropType, + ); + } +} diff --git a/crates/squawk_linter/src/rules/ban_drop_view.rs b/crates/squawk_linter/src/rules/ban_drop_view.rs new file mode 100644 index 000000000..88e3bb7f7 --- /dev/null +++ b/crates/squawk_linter/src/rules/ban_drop_view.rs @@ -0,0 +1,53 @@ +use squawk_syntax::{ + Parse, SourceFile, + ast::{self, AstNode}, +}; + +use crate::{Linter, Rule, Violation}; + +pub(crate) fn ban_drop_view(ctx: &mut Linter, parse: &Parse) { + for stmt in parse.tree().stmts() { + match stmt { + ast::Stmt::DropView(node) => ctx.report(Violation::for_node( + Rule::BanDropView, + "Dropping a view may break existing clients.".into(), + node.syntax(), + )), + ast::Stmt::DropMaterializedView(node) => ctx.report(Violation::for_node( + Rule::BanDropView, + "Dropping a view may break existing clients.".into(), + node.syntax(), + )), + _ => (), + } + } +} + +#[cfg(test)] +mod test { + use insta::assert_snapshot; + + use crate::{ + Rule, + test_utils::{lint_errors, lint_ok}, + }; + + #[test] + fn err() { + let sql = r#" +DROP VIEW v; +DROP VIEW IF EXISTS v CASCADE; +DROP MATERIALIZED VIEW mv; +DROP MATERIALIZED VIEW IF EXISTS mv CASCADE; +"#; + assert_snapshot!(lint_errors(sql, Rule::BanDropView)); + } + + #[test] + fn ok() { + lint_ok( + "CREATE VIEW v AS SELECT 1; DROP INDEX i;", + Rule::BanDropView, + ); + } +} diff --git a/crates/squawk_linter/src/rules/mod.rs b/crates/squawk_linter/src/rules/mod.rs index dbd06c20c..e62d5272e 100644 --- a/crates/squawk_linter/src/rules/mod.rs +++ b/crates/squawk_linter/src/rules/mod.rs @@ -9,8 +9,13 @@ pub(crate) mod ban_concurrent_index_creation_in_transaction; pub(crate) mod ban_create_domain_with_constraint; pub(crate) mod ban_drop_column; pub(crate) mod ban_drop_database; +pub(crate) mod ban_drop_default; +pub(crate) mod ban_drop_function; pub(crate) mod ban_drop_not_null; pub(crate) mod ban_drop_table; +pub(crate) mod ban_drop_trigger; +pub(crate) mod ban_drop_type; +pub(crate) mod ban_drop_view; pub(crate) mod ban_duplicate_column_assignments; pub(crate) mod ban_truncate_cascade; pub(crate) mod ban_uncommitted_transaction; @@ -48,8 +53,13 @@ pub(crate) use ban_concurrent_index_creation_in_transaction::ban_concurrent_inde pub(crate) use ban_create_domain_with_constraint::ban_create_domain_with_constraint; pub(crate) use ban_drop_column::ban_drop_column; pub(crate) use ban_drop_database::ban_drop_database; +pub(crate) use ban_drop_default::ban_drop_default; +pub(crate) use ban_drop_function::ban_drop_function; pub(crate) use ban_drop_not_null::ban_drop_not_null; pub(crate) use ban_drop_table::ban_drop_table; +pub(crate) use ban_drop_trigger::ban_drop_trigger; +pub(crate) use ban_drop_type::ban_drop_type; +pub(crate) use ban_drop_view::ban_drop_view; pub(crate) use ban_duplicate_column_assignments::ban_duplicate_column_assignments; pub(crate) use ban_truncate_cascade::ban_truncate_cascade; pub(crate) use ban_uncommitted_transaction::ban_uncommitted_transaction; diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_default__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_default__test__err.snap new file mode 100644 index 000000000..18bb9be0e --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_default__test__err.snap @@ -0,0 +1,20 @@ +--- +source: crates/squawk_linter/src/rules/ban_drop_default.rs +expression: "lint_errors(sql, Rule::BanDropDefault)" +--- +warning[ban-drop-default]: Dropping a column default may break existing clients. + ╭▸ +2 │ ALTER TABLE tbl ALTER COLUMN c DROP DEFAULT; + ╰╴ ━━━━━━━━━━━━ +warning[ban-drop-default]: Dropping a column default may break existing clients. + ╭▸ +3 │ ALTER TABLE IF EXISTS tbl ALTER COLUMN c DROP DEFAULT; + ╰╴ ━━━━━━━━━━━━ +warning[ban-drop-default]: Dropping a column default may break existing clients. + ╭▸ +4 │ ALTER TABLE tbl ALTER COLUMN c DROP DEFAULT, ALTER COLUMN d DROP DEFAULT; + ╰╴ ━━━━━━━━━━━━ +warning[ban-drop-default]: Dropping a column default may break existing clients. + ╭▸ +4 │ ALTER TABLE tbl ALTER COLUMN c DROP DEFAULT, ALTER COLUMN d DROP DEFAULT; + ╰╴ ━━━━━━━━━━━━ diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_function__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_function__test__err.snap new file mode 100644 index 000000000..0102c98dd --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_function__test__err.snap @@ -0,0 +1,20 @@ +--- +source: crates/squawk_linter/src/rules/ban_drop_function.rs +expression: "lint_errors(sql, Rule::BanDropFunction)" +--- +warning[ban-drop-function]: Dropping a function may break existing clients. + ╭▸ +2 │ DROP FUNCTION f(int); + ╰╴━━━━━━━━━━━━━━━━━━━━━ +warning[ban-drop-function]: Dropping a function may break existing clients. + ╭▸ +3 │ DROP FUNCTION IF EXISTS f(int) CASCADE; + ╰╴━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +warning[ban-drop-function]: Dropping a function may break existing clients. + ╭▸ +4 │ DROP PROCEDURE p(int); + ╰╴━━━━━━━━━━━━━━━━━━━━━━ +warning[ban-drop-function]: Dropping a function may break existing clients. + ╭▸ +5 │ DROP PROCEDURE IF EXISTS p(int) CASCADE; + ╰╴━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_trigger__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_trigger__test__err.snap new file mode 100644 index 000000000..b002213d0 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_trigger__test__err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/ban_drop_trigger.rs +expression: "lint_errors(sql, Rule::BanDropTrigger)" +--- +warning[ban-drop-trigger]: Dropping a trigger may silently change behaviour for existing clients. + ╭▸ +2 │ DROP TRIGGER trg ON tbl; + ╰╴━━━━━━━━━━━━━━━━━━━━━━━━ +warning[ban-drop-trigger]: Dropping a trigger may silently change behaviour for existing clients. + ╭▸ +3 │ DROP TRIGGER IF EXISTS trg ON tbl CASCADE; + ╰╴━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_type__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_type__test__err.snap new file mode 100644 index 000000000..2a8e74b58 --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_type__test__err.snap @@ -0,0 +1,12 @@ +--- +source: crates/squawk_linter/src/rules/ban_drop_type.rs +expression: "lint_errors(sql, Rule::BanDropType)" +--- +warning[ban-drop-type]: Dropping a type may break existing clients. + ╭▸ +2 │ DROP TYPE t; + ╰╴━━━━━━━━━━━━ +warning[ban-drop-type]: Dropping a type may break existing clients. + ╭▸ +3 │ DROP TYPE IF EXISTS t CASCADE; + ╰╴━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_view__test__err.snap b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_view__test__err.snap new file mode 100644 index 000000000..1b53bc00f --- /dev/null +++ b/crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__ban_drop_view__test__err.snap @@ -0,0 +1,20 @@ +--- +source: crates/squawk_linter/src/rules/ban_drop_view.rs +expression: "lint_errors(sql, Rule::BanDropView)" +--- +warning[ban-drop-view]: Dropping a view may break existing clients. + ╭▸ +2 │ DROP VIEW v; + ╰╴━━━━━━━━━━━━ +warning[ban-drop-view]: Dropping a view may break existing clients. + ╭▸ +3 │ DROP VIEW IF EXISTS v CASCADE; + ╰╴━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +warning[ban-drop-view]: Dropping a view may break existing clients. + ╭▸ +4 │ DROP MATERIALIZED VIEW mv; + ╰╴━━━━━━━━━━━━━━━━━━━━━━━━━━ +warning[ban-drop-view]: Dropping a view may break existing clients. + ╭▸ +5 │ DROP MATERIALIZED VIEW IF EXISTS mv CASCADE; + ╰╴━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ From 956243747dabbf8b2cb01fd34bbb380f4b9bcb18 Mon Sep 17 00:00:00 2001 From: t-monaghan Date: Thu, 1 Oct 2026 11:33:40 +1000 Subject: [PATCH 2/2] docs: describe new ban-drop rules --- CHANGELOG.md | 5 +++++ docs/docs/ban-drop-default.md | 12 ++++++++++++ docs/docs/ban-drop-function.md | 12 ++++++++++++ docs/docs/ban-drop-trigger.md | 14 ++++++++++++++ docs/docs/ban-drop-type.md | 12 ++++++++++++ docs/docs/ban-drop-view.md | 12 ++++++++++++ docs/sidebars.js | 5 +++++ docs/src/pages/index.js | 25 +++++++++++++++++++++++++ 8 files changed, 97 insertions(+) create mode 100644 docs/docs/ban-drop-default.md create mode 100644 docs/docs/ban-drop-function.md create mode 100644 docs/docs/ban-drop-trigger.md create mode 100644 docs/docs/ban-drop-type.md create mode 100644 docs/docs/ban-drop-view.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a9934f1e3..9999a9c11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- linter: ban-drop-view, ban-drop-function, ban-drop-type, ban-drop-default rules +- linter: opt-in ban-drop-trigger rule + ## v2.66.0 - 2026-09-23 ### Added diff --git a/docs/docs/ban-drop-default.md b/docs/docs/ban-drop-default.md new file mode 100644 index 000000000..df349f6c4 --- /dev/null +++ b/docs/docs/ban-drop-default.md @@ -0,0 +1,12 @@ +--- +id: ban-drop-default +title: ban-drop-default +--- + +## problem + +Dropping a column default may break existing clients. Inserts that omit a `NOT NULL` column fail with `23502 not_null_violation`. Inserts that omit a nullable column silently write `NULL`. + +## solution + +Update your application code to provide a value for the column, then drop the default in a later migration. diff --git a/docs/docs/ban-drop-function.md b/docs/docs/ban-drop-function.md new file mode 100644 index 000000000..e104bf1e5 --- /dev/null +++ b/docs/docs/ban-drop-function.md @@ -0,0 +1,12 @@ +--- +id: ban-drop-function +title: ban-drop-function +--- + +## problem + +Dropping a function or procedure may break existing clients. Calls can fail with `42883 undefined_function`. + +## solution + +Update your application code to no longer call the function or procedure, then drop it in a later migration. diff --git a/docs/docs/ban-drop-trigger.md b/docs/docs/ban-drop-trigger.md new file mode 100644 index 000000000..77b01cfa7 --- /dev/null +++ b/docs/docs/ban-drop-trigger.md @@ -0,0 +1,14 @@ +--- +id: ban-drop-trigger +title: ban-drop-trigger +--- + +## problem + +Dropping a trigger may silently change behaviour for existing clients. For example, updates may stop setting `updated_at` or writing audit rows. This rule is opt-in. + +## solution + +Update your application code to no longer depend on the trigger, then drop it in a later migration. + +Enable this rule with `--include ban-drop-trigger` (or add `ban-drop-trigger` to your configured include list). diff --git a/docs/docs/ban-drop-type.md b/docs/docs/ban-drop-type.md new file mode 100644 index 000000000..5ce4f6733 --- /dev/null +++ b/docs/docs/ban-drop-type.md @@ -0,0 +1,12 @@ +--- +id: ban-drop-type +title: ban-drop-type +--- + +## problem + +Dropping a type may break existing clients. Casts and parameters that name the type can fail with `42704 undefined_object`. `CASCADE` can also drop columns of that type. + +## solution + +Update your application code to no longer use the type, then drop it in a later migration. diff --git a/docs/docs/ban-drop-view.md b/docs/docs/ban-drop-view.md new file mode 100644 index 000000000..e8d742596 --- /dev/null +++ b/docs/docs/ban-drop-view.md @@ -0,0 +1,12 @@ +--- +id: ban-drop-view +title: ban-drop-view +--- + +## problem + +Dropping a view or materialized view may break existing clients. Queries against the view can fail with `42P01 undefined_table`. + +## solution + +Update your application code to no longer use the view, then drop it in a later migration. diff --git a/docs/sidebars.js b/docs/sidebars.js index 591d869bb..679fca6bd 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -12,8 +12,13 @@ module.exports = { "ban-concurrent-index-creation-in-transaction", "ban-drop-column", "ban-drop-database", + "ban-drop-default", + "ban-drop-function", "ban-drop-not-null", "ban-drop-table", + "ban-drop-trigger", + "ban-drop-type", + "ban-drop-view", "changing-column-type", "constraint-missing-not-valid", "disallowed-unique-constraint", diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index 2087dbf4d..ca30535f1 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -242,6 +242,31 @@ const rules = [ tags: ["queries"], description: "Prevent invalid assignments to the same column more than once.", }, + { + name: "ban-drop-view", + tags: ["backwards compatibility"], + description: "Prevent breaking clients that depend on views or materialized views.", + }, + { + name: "ban-drop-function", + tags: ["backwards compatibility"], + description: "Prevent breaking clients that call functions or procedures.", + }, + { + name: "ban-drop-type", + tags: ["backwards compatibility"], + description: "Prevent breaking clients that use dropped types.", + }, + { + name: "ban-drop-default", + tags: ["backwards compatibility"], + description: "Prevent inserts from failing or writing NULL after dropping a column default.", + }, + { + name: "ban-drop-trigger", + tags: ["backwards compatibility"], + description: "Prevent silent changes when a trigger is dropped (opt-in).", + }, // xtask:new-rule:rule-doc-meta ]