Skip to content

Commit f0d8565

Browse files
committed
progress
1 parent 6895399 commit f0d8565

File tree

27 files changed

+1084
-13
lines changed

27 files changed

+1084
-13
lines changed

PLAN.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,6 @@ Please update the list below with the rules that we need to migrate, and the one
5858
NEEDS FEATURES:
5959

6060
TODO:
61-
- renaming_column
62-
- renaming_table
63-
- require_concurrent_index_creation
64-
- require_concurrent_index_deletion
65-
- transaction_nesting
66-
- prefer_robust_stmts
6761

6862
DONE:
6963
- adding_field_with_default ✓ (ported from Squawk)
@@ -86,5 +80,11 @@ DONE:
8680
- prefer_text_field ✓ (ported from Squawk)
8781
- prefer_timestamptz ✓ (ported from Squawk)
8882
- disallow_unique_constraint ✓ (ported from Squawk)
83+
- renaming_column ✓ (ported from Squawk)
84+
- renaming_table ✓ (ported from Squawk)
85+
- require_concurrent_index_creation ✓ (ported from Squawk)
86+
- require_concurrent_index_deletion ✓ (ported from Squawk)
87+
- transaction_nesting ✓ (ported from Squawk)
88+
- prefer_robust_stmts ✓ (ported from Squawk - simplified version)
8989

9090

crates/pgt_analyser/src/lint/safety.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ pub mod prefer_big_int;
2020
pub mod prefer_bigint_over_int;
2121
pub mod prefer_bigint_over_smallint;
2222
pub mod prefer_identity;
23+
pub mod prefer_robust_stmts;
2324
pub mod prefer_text_field;
2425
pub mod prefer_timestamptz;
25-
declare_lint_group! { pub Safety { name : "safety" , rules : [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_text_field :: PreferTextField , self :: prefer_timestamptz :: PreferTimestamptz ,] } }
26+
pub mod renaming_column;
27+
pub mod renaming_table;
28+
pub mod require_concurrent_index_creation;
29+
pub mod require_concurrent_index_deletion;
30+
pub mod transaction_nesting;
31+
declare_lint_group! { pub Safety { name : "safety" , rules : [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_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: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
/// Prefer statements with guards for robustness in migrations.
7+
///
8+
/// When running migrations outside of transactions (e.g., CREATE INDEX CONCURRENTLY),
9+
/// statements should be made robust by using guards like IF NOT EXISTS or IF EXISTS.
10+
/// This allows migrations to be safely re-run if they fail partway through.
11+
///
12+
/// ## Examples
13+
///
14+
/// ### Invalid
15+
///
16+
/// ```sql,expect_diagnostic
17+
/// CREATE INDEX CONCURRENTLY users_email_idx ON users (email);
18+
/// ```
19+
///
20+
/// ```sql,expect_diagnostic
21+
/// DROP INDEX CONCURRENTLY users_email_idx;
22+
/// ```
23+
///
24+
/// ### Valid
25+
///
26+
/// ```sql
27+
/// CREATE INDEX CONCURRENTLY IF NOT EXISTS users_email_idx ON users (email);
28+
/// ```
29+
///
30+
/// ```sql
31+
/// DROP INDEX CONCURRENTLY IF EXISTS users_email_idx;
32+
/// ```
33+
///
34+
pub PreferRobustStmts {
35+
version: "next",
36+
name: "preferRobustStmts",
37+
severity: Severity::Warning,
38+
recommended: false,
39+
sources: &[RuleSource::Squawk("prefer-robust-stmts")],
40+
}
41+
}
42+
43+
impl Rule for PreferRobustStmts {
44+
type Options = ();
45+
46+
fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
47+
let mut diagnostics = Vec::new();
48+
49+
// Skip if we only have one statement in the file
50+
if ctx.file_context().stmt_count <= 1 {
51+
return diagnostics;
52+
}
53+
54+
// Since we assume we're always in a transaction, we only check for
55+
// statements that explicitly run outside transactions
56+
match &ctx.stmt() {
57+
pgt_query::NodeEnum::IndexStmt(stmt) => {
58+
// Concurrent index creation runs outside transaction
59+
if stmt.concurrent {
60+
// Check for unnamed index
61+
if stmt.idxname.is_empty() {
62+
diagnostics.push(RuleDiagnostic::new(
63+
rule_category!(),
64+
None,
65+
markup! {
66+
"Concurrent index should have an explicit name."
67+
},
68+
).detail(None, "Use an explicit name for a concurrently created index to make migrations more robust."));
69+
}
70+
// Check for IF NOT EXISTS
71+
if !stmt.if_not_exists {
72+
diagnostics.push(
73+
RuleDiagnostic::new(
74+
rule_category!(),
75+
None,
76+
markup! {
77+
"Concurrent index creation should use IF NOT EXISTS."
78+
},
79+
)
80+
.detail(
81+
None,
82+
"Add IF NOT EXISTS to make the migration re-runnable if it fails.",
83+
),
84+
);
85+
}
86+
}
87+
}
88+
pgt_query::NodeEnum::DropStmt(stmt) => {
89+
// Concurrent drop runs outside transaction
90+
if stmt.concurrent && !stmt.missing_ok {
91+
diagnostics.push(
92+
RuleDiagnostic::new(
93+
rule_category!(),
94+
None,
95+
markup! {
96+
"Concurrent drop should use IF EXISTS."
97+
},
98+
)
99+
.detail(
100+
None,
101+
"Add IF EXISTS to make the migration re-runnable if it fails.",
102+
),
103+
);
104+
}
105+
}
106+
_ => {}
107+
}
108+
109+
diagnostics
110+
}
111+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
/// Renaming columns may break existing queries and application code.
7+
///
8+
/// Renaming a column that is being used by an existing application or query can cause unexpected downtime.
9+
/// Consider creating a new column instead and migrating the data, then dropping the old column after ensuring
10+
/// no dependencies exist.
11+
///
12+
/// ## Examples
13+
///
14+
/// ### Invalid
15+
///
16+
/// ```sql,expect_diagnostic
17+
/// ALTER TABLE users RENAME COLUMN email TO email_address;
18+
/// ```
19+
///
20+
pub RenamingColumn {
21+
version: "next",
22+
name: "renamingColumn",
23+
severity: Severity::Warning,
24+
recommended: false,
25+
sources: &[RuleSource::Squawk("renaming-column")],
26+
}
27+
}
28+
29+
impl Rule for RenamingColumn {
30+
type Options = ();
31+
32+
fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
33+
let mut diagnostics = Vec::new();
34+
35+
if let pgt_query::NodeEnum::RenameStmt(stmt) = &ctx.stmt() {
36+
if stmt.rename_type() == pgt_query::protobuf::ObjectType::ObjectColumn {
37+
diagnostics.push(RuleDiagnostic::new(
38+
rule_category!(),
39+
None,
40+
markup! {
41+
"Renaming a column may break existing clients."
42+
},
43+
).detail(None, "Consider creating a new column with the desired name and migrating data instead."));
44+
}
45+
}
46+
47+
diagnostics
48+
}
49+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
/// Renaming tables may break existing queries and application code.
7+
///
8+
/// Renaming a table that is being referenced by existing applications, views, functions, or foreign keys
9+
/// can cause unexpected downtime. Consider creating a view with the old table name pointing to the new table,
10+
/// or carefully coordinate the rename with application deployments.
11+
///
12+
/// ## Examples
13+
///
14+
/// ### Invalid
15+
///
16+
/// ```sql,expect_diagnostic
17+
/// ALTER TABLE users RENAME TO app_users;
18+
/// ```
19+
///
20+
pub RenamingTable {
21+
version: "next",
22+
name: "renamingTable",
23+
severity: Severity::Warning,
24+
recommended: false,
25+
sources: &[RuleSource::Squawk("renaming-table")],
26+
}
27+
}
28+
29+
impl Rule for RenamingTable {
30+
type Options = ();
31+
32+
fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
33+
let mut diagnostics = Vec::new();
34+
35+
if let pgt_query::NodeEnum::RenameStmt(stmt) = &ctx.stmt() {
36+
if stmt.rename_type() == pgt_query::protobuf::ObjectType::ObjectTable {
37+
diagnostics.push(RuleDiagnostic::new(
38+
rule_category!(),
39+
None,
40+
markup! {
41+
"Renaming a table may break existing clients."
42+
},
43+
).detail(None, "Consider creating a view with the old table name instead, or coordinate the rename carefully with application deployments."));
44+
}
45+
}
46+
47+
diagnostics
48+
}
49+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
/// Creating indexes non-concurrently can lock the table for writes.
7+
///
8+
/// When creating an index on an existing table, using CREATE INDEX without CONCURRENTLY will lock the table
9+
/// against writes for the duration of the index build. This can cause downtime in production systems.
10+
/// Use CREATE INDEX CONCURRENTLY to build the index without blocking concurrent operations.
11+
///
12+
/// ## Examples
13+
///
14+
/// ### Invalid
15+
///
16+
/// ```sql,expect_diagnostic
17+
/// CREATE INDEX users_email_idx ON users (email);
18+
/// ```
19+
///
20+
/// ### Valid
21+
///
22+
/// ```sql
23+
/// CREATE INDEX CONCURRENTLY users_email_idx ON users (email);
24+
/// ```
25+
///
26+
pub RequireConcurrentIndexCreation {
27+
version: "next",
28+
name: "requireConcurrentIndexCreation",
29+
severity: Severity::Warning,
30+
recommended: false,
31+
sources: &[RuleSource::Squawk("require-concurrent-index-creation")],
32+
}
33+
}
34+
35+
impl Rule for RequireConcurrentIndexCreation {
36+
type Options = ();
37+
38+
fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
39+
let mut diagnostics = Vec::new();
40+
41+
if let pgt_query::NodeEnum::IndexStmt(stmt) = &ctx.stmt() {
42+
if !stmt.concurrent {
43+
// Check if this table was created in the same transaction/file
44+
let table_name = stmt
45+
.relation
46+
.as_ref()
47+
.map(|r| r.relname.as_str())
48+
.unwrap_or("");
49+
50+
if !table_name.is_empty()
51+
&& !is_table_created_in_file(ctx.file_context(), table_name)
52+
{
53+
diagnostics.push(RuleDiagnostic::new(
54+
rule_category!(),
55+
None,
56+
markup! {
57+
"Creating an index non-concurrently blocks writes to the table."
58+
},
59+
).detail(None, "Use CREATE INDEX CONCURRENTLY to avoid blocking concurrent operations on the table."));
60+
}
61+
}
62+
}
63+
64+
diagnostics
65+
}
66+
}
67+
68+
fn is_table_created_in_file(
69+
file_context: &pgt_analyse::AnalysedFileContext,
70+
table_name: &str,
71+
) -> bool {
72+
// Check all statements in the file to see if this table was created
73+
for stmt in file_context.all_stmts {
74+
if let pgt_query::NodeEnum::CreateStmt(create_stmt) = stmt {
75+
if let Some(relation) = &create_stmt.relation {
76+
if relation.relname == table_name {
77+
return true;
78+
}
79+
}
80+
}
81+
}
82+
false
83+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
/// Dropping indexes non-concurrently can lock the table for reads.
7+
///
8+
/// When dropping an index, using DROP INDEX without CONCURRENTLY will lock the table
9+
/// preventing reads and writes for the duration of the drop. This can cause downtime in production systems.
10+
/// Use DROP INDEX CONCURRENTLY to drop the index without blocking concurrent operations.
11+
///
12+
/// ## Examples
13+
///
14+
/// ### Invalid
15+
///
16+
/// ```sql,expect_diagnostic
17+
/// DROP INDEX IF EXISTS users_email_idx;
18+
/// ```
19+
///
20+
/// ### Valid
21+
///
22+
/// ```sql
23+
/// DROP INDEX CONCURRENTLY IF EXISTS users_email_idx;
24+
/// ```
25+
///
26+
pub RequireConcurrentIndexDeletion {
27+
version: "next",
28+
name: "requireConcurrentIndexDeletion",
29+
severity: Severity::Warning,
30+
recommended: false,
31+
sources: &[RuleSource::Squawk("require-concurrent-index-deletion")],
32+
}
33+
}
34+
35+
impl Rule for RequireConcurrentIndexDeletion {
36+
type Options = ();
37+
38+
fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
39+
let mut diagnostics = Vec::new();
40+
41+
if let pgt_query::NodeEnum::DropStmt(stmt) = &ctx.stmt() {
42+
if !stmt.concurrent
43+
&& stmt.remove_type() == pgt_query::protobuf::ObjectType::ObjectIndex
44+
{
45+
diagnostics.push(RuleDiagnostic::new(
46+
rule_category!(),
47+
None,
48+
markup! {
49+
"Dropping an index non-concurrently blocks reads and writes to the table."
50+
},
51+
).detail(None, "Use DROP INDEX CONCURRENTLY to avoid blocking concurrent operations on the table."));
52+
}
53+
}
54+
55+
diagnostics
56+
}
57+
}

0 commit comments

Comments
 (0)