Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 41 additions & 3 deletions crates/squawk_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,6 +112,11 @@ pub enum Rule {
RequireLockTimeout,
RequireStatementTimeout,
BanDuplicateColumnAssignments,
BanDropView,
BanDropFunction,
BanDropType,
BanDropDefault,
BanDropTrigger,
// xtask:new-rule:error-name
}

Expand All @@ -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
)
}

Expand Down Expand Up @@ -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}")),
}
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
54 changes: 54 additions & 0 deletions crates/squawk_linter/src/rules/ban_drop_default.rs
Original file line number Diff line number Diff line change
@@ -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<SourceFile>) {
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,
);
}
}
53 changes: 53 additions & 0 deletions crates/squawk_linter/src/rules/ban_drop_function.rs
Original file line number Diff line number Diff line change
@@ -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<SourceFile>) {
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,
);
}
}
45 changes: 45 additions & 0 deletions crates/squawk_linter/src/rules/ban_drop_trigger.rs
Original file line number Diff line number Diff line change
@@ -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<SourceFile>) {
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,
);
}
}
45 changes: 45 additions & 0 deletions crates/squawk_linter/src/rules/ban_drop_type.rs
Original file line number Diff line number Diff line change
@@ -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<SourceFile>) {
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,
);
}
}
53 changes: 53 additions & 0 deletions crates/squawk_linter/src/rules/ban_drop_view.rs
Original file line number Diff line number Diff line change
@@ -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<SourceFile>) {
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,
);
}
}
10 changes: 10 additions & 0 deletions crates/squawk_linter/src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading