Skip to content

Commit 6cf5b45

Browse files
committed
feat(analyser): mutiple alter table
1 parent 47684c6 commit 6cf5b45

26 files changed

+417
-29
lines changed

crates/pgt_analyser/src/lint/safety.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub mod ban_truncate_cascade;
1717
pub mod changing_column_type;
1818
pub mod constraint_missing_not_valid;
1919
pub mod disallow_unique_constraint;
20+
pub mod multiple_alter_table;
2021
pub mod prefer_big_int;
2122
pub mod prefer_bigint_over_int;
2223
pub mod prefer_bigint_over_smallint;
@@ -30,4 +31,4 @@ pub mod renaming_table;
3031
pub mod require_concurrent_index_creation;
3132
pub mod require_concurrent_index_deletion;
3233
pub mod transaction_nesting;
33-
declare_lint_group! { pub Safety { name : "safety" , rules : [self :: add_serial_column :: AddSerialColumn , self :: adding_field_with_default :: AddingFieldWithDefault , self :: adding_foreign_key_constraint :: AddingForeignKeyConstraint , self :: adding_not_null_field :: AddingNotNullField , self :: adding_primary_key_constraint :: AddingPrimaryKeyConstraint , self :: adding_required_field :: AddingRequiredField , self :: ban_char_field :: BanCharField , self :: ban_concurrent_index_creation_in_transaction :: BanConcurrentIndexCreationInTransaction , self :: ban_drop_column :: BanDropColumn , self :: ban_drop_database :: BanDropDatabase , self :: ban_drop_not_null :: BanDropNotNull , self :: ban_drop_table :: BanDropTable , self :: ban_truncate_cascade :: BanTruncateCascade , self :: changing_column_type :: ChangingColumnType , self :: constraint_missing_not_valid :: ConstraintMissingNotValid , self :: disallow_unique_constraint :: DisallowUniqueConstraint , self :: prefer_big_int :: PreferBigInt , self :: prefer_bigint_over_int :: PreferBigintOverInt , self :: prefer_bigint_over_smallint :: PreferBigintOverSmallint , self :: prefer_identity :: PreferIdentity , self :: prefer_jsonb :: PreferJsonb , self :: prefer_robust_stmts :: PreferRobustStmts , self :: prefer_text_field :: PreferTextField , self :: prefer_timestamptz :: PreferTimestamptz , self :: renaming_column :: RenamingColumn , self :: renaming_table :: RenamingTable , self :: require_concurrent_index_creation :: RequireConcurrentIndexCreation , self :: require_concurrent_index_deletion :: RequireConcurrentIndexDeletion , self :: transaction_nesting :: TransactionNesting ,] } }
34+
declare_lint_group! { pub Safety { name : "safety" , rules : [self :: add_serial_column :: AddSerialColumn , self :: adding_field_with_default :: AddingFieldWithDefault , self :: adding_foreign_key_constraint :: AddingForeignKeyConstraint , self :: adding_not_null_field :: AddingNotNullField , self :: adding_primary_key_constraint :: AddingPrimaryKeyConstraint , self :: adding_required_field :: AddingRequiredField , self :: ban_char_field :: BanCharField , self :: ban_concurrent_index_creation_in_transaction :: BanConcurrentIndexCreationInTransaction , self :: ban_drop_column :: BanDropColumn , self :: ban_drop_database :: BanDropDatabase , self :: ban_drop_not_null :: BanDropNotNull , self :: ban_drop_table :: BanDropTable , self :: ban_truncate_cascade :: BanTruncateCascade , self :: changing_column_type :: ChangingColumnType , self :: constraint_missing_not_valid :: ConstraintMissingNotValid , self :: disallow_unique_constraint :: DisallowUniqueConstraint , self :: multiple_alter_table :: MultipleAlterTable , self :: prefer_big_int :: PreferBigInt , self :: prefer_bigint_over_int :: PreferBigintOverInt , self :: prefer_bigint_over_smallint :: PreferBigintOverSmallint , self :: prefer_identity :: PreferIdentity , self :: prefer_jsonb :: PreferJsonb , self :: prefer_robust_stmts :: PreferRobustStmts , self :: prefer_text_field :: PreferTextField , self :: prefer_timestamptz :: PreferTimestamptz , self :: renaming_column :: RenamingColumn , self :: renaming_table :: RenamingTable , self :: require_concurrent_index_creation :: RequireConcurrentIndexCreation , self :: require_concurrent_index_deletion :: RequireConcurrentIndexDeletion , self :: transaction_nesting :: TransactionNesting ,] } }
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use pgt_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
2+
use pgt_console::markup;
3+
use pgt_diagnostics::Severity;
4+
5+
declare_lint_rule! {
6+
/// Multiple ALTER TABLE statements on the same table should be combined into a single statement.
7+
///
8+
/// When you run multiple ALTER TABLE statements on the same table, PostgreSQL must scan and potentially
9+
/// rewrite the table multiple times. Each ALTER TABLE command requires acquiring locks and performing
10+
/// table operations that can be expensive, especially on large tables.
11+
///
12+
/// Combining multiple ALTER TABLE operations into a single statement with comma-separated actions
13+
/// allows PostgreSQL to scan and modify the table only once, improving performance and reducing
14+
/// the time locks are held.
15+
///
16+
/// ## Examples
17+
///
18+
/// ### Invalid
19+
///
20+
/// ```sql,expect_diagnostic
21+
/// ALTER TABLE authors ALTER COLUMN name SET NOT NULL;
22+
/// ALTER TABLE authors ALTER COLUMN email SET NOT NULL;
23+
/// ```
24+
///
25+
/// ### Valid
26+
///
27+
/// ```sql
28+
/// ALTER TABLE authors
29+
/// ALTER COLUMN name SET NOT NULL,
30+
/// ALTER COLUMN email SET NOT NULL;
31+
/// ```
32+
///
33+
pub MultipleAlterTable {
34+
version: "next",
35+
name: "multipleAlterTable",
36+
severity: Severity::Warning,
37+
recommended: true,
38+
sources: &[RuleSource::Eugene("W12")],
39+
}
40+
}
41+
42+
impl Rule for MultipleAlterTable {
43+
type Options = ();
44+
45+
fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
46+
let mut diagnostics = Vec::new();
47+
48+
// Check if current statement is ALTER TABLE
49+
if let pgt_query::NodeEnum::AlterTableStmt(stmt) = &ctx.stmt() {
50+
if let Some(relation) = &stmt.relation {
51+
let current_schema = &relation.schemaname;
52+
let current_table = &relation.relname;
53+
54+
// Check previous statements for ALTER TABLE on the same table
55+
let file_ctx = ctx.file_context();
56+
57+
// Normalize schema name: treat empty string as "public"
58+
let current_schema_normalized = if current_schema.is_empty() {
59+
"public"
60+
} else {
61+
current_schema.as_str()
62+
};
63+
64+
let has_previous_alter =
65+
file_ctx
66+
.previous_stmts()
67+
.iter()
68+
.any(|prev_stmt| match prev_stmt {
69+
pgt_query::NodeEnum::AlterTableStmt(prev_alter) => {
70+
if let Some(prev_relation) = &prev_alter.relation {
71+
let prev_schema_normalized =
72+
if prev_relation.schemaname.is_empty() {
73+
"public"
74+
} else {
75+
prev_relation.schemaname.as_str()
76+
};
77+
78+
// Match if same table and schema (treating empty schema as "public")
79+
prev_relation.relname == *current_table
80+
&& prev_schema_normalized == current_schema_normalized
81+
} else {
82+
false
83+
}
84+
}
85+
_ => false,
86+
});
87+
88+
if has_previous_alter {
89+
let schema_display = if current_schema.is_empty() {
90+
"public"
91+
} else {
92+
current_schema
93+
};
94+
95+
diagnostics.push(
96+
RuleDiagnostic::new(
97+
rule_category!(),
98+
None,
99+
markup! {
100+
"Multiple "<Emphasis>"ALTER TABLE"</Emphasis>" statements found for table "<Emphasis>{schema_display}"."{{current_table}}</Emphasis>"."
101+
},
102+
)
103+
.detail(
104+
None,
105+
"Multiple ALTER TABLE statements on the same table require scanning and potentially rewriting the table multiple times.",
106+
)
107+
.note("Combine the ALTER TABLE statements into a single statement with comma-separated actions to scan the table only once."),
108+
);
109+
}
110+
}
111+
}
112+
113+
diagnostics
114+
}
115+
}

crates/pgt_analyser/src/options.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub type ChangingColumnType =
2626
<lint::safety::changing_column_type::ChangingColumnType as pgt_analyse::Rule>::Options;
2727
pub type ConstraintMissingNotValid = < lint :: safety :: constraint_missing_not_valid :: ConstraintMissingNotValid as pgt_analyse :: Rule > :: Options ;
2828
pub type DisallowUniqueConstraint = < lint :: safety :: disallow_unique_constraint :: DisallowUniqueConstraint as pgt_analyse :: Rule > :: Options ;
29+
pub type MultipleAlterTable =
30+
<lint::safety::multiple_alter_table::MultipleAlterTable as pgt_analyse::Rule>::Options;
2931
pub type PreferBigInt = <lint::safety::prefer_big_int::PreferBigInt as pgt_analyse::Rule>::Options;
3032
pub type PreferBigintOverInt =
3133
<lint::safety::prefer_bigint_over_int::PreferBigintOverInt as pgt_analyse::Rule>::Options;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- expect_lint/safety/multipleAlterTable
2+
-- Test multiple ALTER TABLE statements on the same table
3+
ALTER TABLE authors ALTER COLUMN name SET NOT NULL;
4+
ALTER TABLE authors ALTER COLUMN email SET NOT NULL;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
source: crates/pgt_analyser/tests/rules_tests.rs
3+
expression: snapshot
4+
snapshot_kind: text
5+
---
6+
# Input
7+
```
8+
-- expect_lint/safety/multipleAlterTable
9+
-- Test multiple ALTER TABLE statements on the same table
10+
ALTER TABLE authors ALTER COLUMN name SET NOT NULL;
11+
ALTER TABLE authors ALTER COLUMN email SET NOT NULL;
12+
13+
```
14+
15+
# Diagnostics
16+
lint/safety/multipleAlterTable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17+
18+
× Multiple ALTER TABLE statements found for table public.authors.
19+
20+
i Multiple ALTER TABLE statements on the same table require scanning and potentially rewriting the table multiple times.
21+
22+
i Combine the ALTER TABLE statements into a single statement with comma-separated actions to scan the table only once.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- expect_lint/safety/multipleAlterTable
2+
-- Test mixing implicit and explicit public schema (should match)
3+
ALTER TABLE authors ADD COLUMN name text;
4+
ALTER TABLE public.authors ADD COLUMN email text;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
source: crates/pgt_analyser/tests/rules_tests.rs
3+
expression: snapshot
4+
snapshot_kind: text
5+
---
6+
# Input
7+
```
8+
-- expect_lint/safety/multipleAlterTable
9+
-- Test mixing implicit and explicit public schema (should match)
10+
ALTER TABLE authors ADD COLUMN name text;
11+
ALTER TABLE public.authors ADD COLUMN email text;
12+
13+
```
14+
15+
# Diagnostics
16+
lint/safety/multipleAlterTable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17+
18+
× Multiple ALTER TABLE statements found for table public.authors.
19+
20+
i Multiple ALTER TABLE statements on the same table require scanning and potentially rewriting the table multiple times.
21+
22+
i Combine the ALTER TABLE statements into a single statement with comma-separated actions to scan the table only once.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- expect_lint/safety/multipleAlterTable
2+
-- Test ALTER TABLE after other statements
3+
CREATE TABLE products (id serial PRIMARY KEY);
4+
ALTER TABLE products ADD COLUMN description text;
5+
ALTER TABLE products ADD COLUMN price numeric;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
source: crates/pgt_analyser/tests/rules_tests.rs
3+
expression: snapshot
4+
snapshot_kind: text
5+
---
6+
# Input
7+
```
8+
-- expect_lint/safety/multipleAlterTable
9+
-- Test ALTER TABLE after other statements
10+
CREATE TABLE products (id serial PRIMARY KEY);
11+
ALTER TABLE products ADD COLUMN description text;
12+
ALTER TABLE products ADD COLUMN price numeric;
13+
14+
```
15+
16+
# Diagnostics
17+
lint/safety/multipleAlterTable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18+
19+
× Multiple ALTER TABLE statements found for table public.products.
20+
21+
i Multiple ALTER TABLE statements on the same table require scanning and potentially rewriting the table multiple times.
22+
23+
i Combine the ALTER TABLE statements into a single statement with comma-separated actions to scan the table only once.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Test single ALTER TABLE with multiple actions (should be safe)
2+
-- expect_no_diagnostics
3+
ALTER TABLE authors
4+
ALTER COLUMN name SET NOT NULL,
5+
ALTER COLUMN email SET NOT NULL;

0 commit comments

Comments
 (0)