Skip to content

Commit a89de2a

Browse files
committed
progress
1 parent d7cf7e2 commit a89de2a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1774
-45
lines changed

PLAN.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ pub struct RuleContext<'a, R: Rule> {
1717
// the file context which contains other statements in that file in case you need them
1818
file_context: &'a AnalysedFileContext,
1919
}
20+
21+
pub struct AnalysedFileContext<'a> {
22+
// all other statements in this file
23+
pub all_stmts: &'a Vec<pgt_query::NodeEnum>,
24+
// total count of statements in this file
25+
pub stmt_count: usize,
26+
// all statements before the currently analysed one
27+
pub previous_stmts: Vec<&'a pgt_query::NodeEnum>,
28+
}
2029
```
2130

2231
In squawk, you will see:
@@ -41,27 +50,23 @@ LEARNINGS:
4150
- RuleDiagnostic methods: `detail(span, msg)` takes two parameters, `note(msg)` takes only one parameter
4251
- To check Postgres version: access `ctx.schema_cache().is_some_and(|sc| sc.version.major_version)` which gives e.g. 17
4352
- NEVER skip anything, or use a subset of something. ALWAYS do the full thing. For example, copy the entire non-volatile functions list from Squawk, not just a subset.
53+
- If you are missing features from our context to be able to properly implement a rule, DO NOT DO IT. Instead, add that rule to the NEEDS FEATURES list below.
4454
- Remember to run `just gen-lint` after creating a new rule to generate all necessary files
4555

4656
Please update the list below with the rules that we need to migrate, and the ones that are already migrated. Keep the list up-to-date.
4757

58+
NEEDS FEATURES:
59+
4860
TODO:
49-
- ban_concurrent_index_creation_in_transaction
50-
- changing_column_type
51-
- constraint_missing_not_valid
52-
- disallow_unique_constraint
53-
- prefer_big_int
54-
- prefer_bigint_over_int
55-
- prefer_bigint_over_smallint
56-
- prefer_identity
57-
- prefer_robust_stmts
5861
- prefer_text_field
5962
- prefer_timestamptz
6063
- renaming_column
6164
- renaming_table
6265
- require_concurrent_index_creation
6366
- require_concurrent_index_deletion
6467
- transaction_nesting
68+
- disallow_unique_constraint
69+
- prefer_robust_stmts
6570

6671
DONE:
6772
- adding_field_with_default ✓ (ported from Squawk)
@@ -70,9 +75,16 @@ DONE:
7075
- adding_primary_key_constraint ✓ (ported from Squawk)
7176
- adding_required_field (already exists in pgt_analyser)
7277
- ban_char_field ✓ (ported from Squawk)
78+
- ban_concurrent_index_creation_in_transaction ✓ (ported from Squawk)
7379
- ban_drop_column (already exists in pgt_analyser)
80+
- changing_column_type ✓ (ported from Squawk)
81+
- constraint_missing_not_valid ✓ (ported from Squawk)
7482
- ban_drop_database (already exists in pgt_analyser, as bad_drop_database in squawk)
7583
- ban_drop_not_null (already exists in pgt_analyser)
7684
- ban_drop_table (already exists in pgt_analyser)
85+
- prefer_big_int ✓ (ported from Squawk)
86+
- prefer_bigint_over_int ✓ (ported from Squawk)
87+
- prefer_bigint_over_smallint ✓ (ported from Squawk)
88+
- prefer_identity ✓ (ported from Squawk)
7789

7890

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1-
#[derive(Default)]
2-
pub struct AnalysedFileContext {}
1+
pub struct AnalysedFileContext<'a> {
2+
pub all_stmts: &'a Vec<pgt_query::NodeEnum>,
3+
pub stmt_count: usize,
4+
pub previous_stmts: Vec<&'a pgt_query::NodeEnum>,
5+
}
6+
7+
impl<'a> AnalysedFileContext<'a> {
8+
pub fn new(stmts: &'a Vec<pgt_query::NodeEnum>) -> Self {
9+
Self {
10+
all_stmts: stmts,
11+
stmt_count: stmts.len(),
12+
previous_stmts: Vec::new(),
13+
}
14+
}
315

4-
impl AnalysedFileContext {
5-
#[allow(unused)]
6-
pub fn update_from(&mut self, stmt_root: &pgt_query::NodeEnum) {}
16+
pub fn update_from(&mut self, stmt_root: &'a pgt_query::NodeEnum) {
17+
self.previous_stmts.push(stmt_root);
18+
}
719
}

crates/pgt_analyse/src/context.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub struct RuleContext<'a, R: Rule> {
1010
stmt: &'a pgt_query::NodeEnum,
1111
options: &'a R::Options,
1212
schema_cache: Option<&'a SchemaCache>,
13-
file_context: &'a AnalysedFileContext,
13+
file_context: &'a AnalysedFileContext<'a>,
1414
}
1515

1616
impl<'a, R> RuleContext<'a, R>

crates/pgt_analyse/src/registry.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ impl RuleRegistry {
159159
pub struct RegistryRuleParams<'a> {
160160
pub root: &'a pgt_query::NodeEnum,
161161
pub options: &'a AnalyserOptions,
162-
pub analysed_file_context: &'a AnalysedFileContext,
162+
pub analysed_file_context: &'a AnalysedFileContext<'a>,
163163
pub schema_cache: Option<&'a pgt_schema_cache::SchemaCache>,
164164
}
165165

crates/pgt_analyser/src/lib.rs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,29 @@ impl<'a> Analyser<'a> {
6262
pub fn run(&self, params: AnalyserParams) -> Vec<RuleDiagnostic> {
6363
let mut diagnostics = vec![];
6464

65-
let mut file_context = AnalysedFileContext::default();
65+
let roots: Vec<pgt_query::NodeEnum> = params.stmts.iter().map(|s| s.root.clone()).collect();
66+
let mut file_context = AnalysedFileContext::new(&roots);
67+
68+
for (i, stmt) in params.stmts.into_iter().enumerate() {
69+
let stmt_diagnostics: Vec<_> = {
70+
let rule_params = RegistryRuleParams {
71+
root: &roots[i],
72+
options: self.options,
73+
analysed_file_context: &file_context,
74+
schema_cache: params.schema_cache,
75+
};
6676

67-
for stmt in params.stmts {
68-
let rule_params = RegistryRuleParams {
69-
root: &stmt.root,
70-
options: self.options,
71-
analysed_file_context: &file_context,
72-
schema_cache: params.schema_cache,
73-
};
74-
75-
diagnostics.extend(
7677
self.registry
7778
.rules
7879
.iter()
7980
.flat_map(|rule| (rule.run)(&rule_params))
80-
.map(|r| r.span(stmt.range)),
81-
);
81+
.map(|r| r.span(stmt.range))
82+
.collect()
83+
}; // end immutable borrow
84+
85+
diagnostics.extend(stmt_diagnostics);
8286

83-
file_context.update_from(&stmt.root);
87+
file_context.update_from(&roots[i]);
8488
}
8589

8690
diagnostics

crates/pgt_analyser/src/lint/safety.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@ pub mod adding_not_null_field;
77
pub mod adding_primary_key_constraint;
88
pub mod adding_required_field;
99
pub mod ban_char_field;
10+
pub mod ban_concurrent_index_creation_in_transaction;
1011
pub mod ban_drop_column;
1112
pub mod ban_drop_database;
1213
pub mod ban_drop_not_null;
1314
pub mod ban_drop_table;
1415
pub mod ban_truncate_cascade;
15-
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_drop_column :: BanDropColumn , self :: ban_drop_database :: BanDropDatabase , self :: ban_drop_not_null :: BanDropNotNull , self :: ban_drop_table :: BanDropTable , self :: ban_truncate_cascade :: BanTruncateCascade ,] } }
16+
pub mod changing_column_type;
17+
pub mod constraint_missing_not_valid;
18+
pub mod prefer_big_int;
19+
pub mod prefer_bigint_over_int;
20+
pub mod prefer_bigint_over_smallint;
21+
pub mod prefer_identity;
22+
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 :: prefer_big_int :: PreferBigInt , self :: prefer_bigint_over_int :: PreferBigintOverInt , self :: prefer_bigint_over_smallint :: PreferBigintOverSmallint , self :: prefer_identity :: PreferIdentity , ] } }
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
/// Concurrent index creation is not allowed within a transaction.
7+
///
8+
/// `CREATE INDEX CONCURRENTLY` cannot be used within a transaction block. This will cause an error in Postgres.
9+
///
10+
/// Migration tools usually run each migration in a transaction, so using `CREATE INDEX CONCURRENTLY` will fail in such tools.
11+
///
12+
/// ## Examples
13+
///
14+
/// ### Invalid
15+
///
16+
/// ```sql,expect_diagnostic
17+
/// CREATE INDEX CONCURRENTLY "field_name_idx" ON "table_name" ("field_name");
18+
/// ```
19+
///
20+
pub BanConcurrentIndexCreationInTransaction {
21+
version: "next",
22+
name: "banConcurrentIndexCreationInTransaction",
23+
severity: Severity::Error,
24+
recommended: true,
25+
sources: &[RuleSource::Squawk("ban-concurrent-index-creation-in-transaction")],
26+
}
27+
}
28+
29+
impl Rule for BanConcurrentIndexCreationInTransaction {
30+
type Options = ();
31+
32+
fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
33+
let mut diagnostics = Vec::new();
34+
35+
// check if the current statement is CREATE INDEX CONCURRENTLY and there is at least one
36+
// other statement in the same context (indicating a transaction block)
37+
//
38+
// since our analyser assumes we're always in a transaction context, we always flag concurrent indexes
39+
if let pgt_query::NodeEnum::IndexStmt(stmt) = ctx.stmt() {
40+
if stmt.concurrent && ctx.file_context().stmt_count > 1 {
41+
diagnostics.push(RuleDiagnostic::new(
42+
rule_category!(),
43+
None,
44+
markup! {
45+
"CREATE INDEX CONCURRENTLY cannot be used inside a transaction block."
46+
}
47+
).detail(None, "Run CREATE INDEX CONCURRENTLY outside of a transaction. Migration tools usually run in transactions, so you may need to run this statement in its own migration or manually."));
48+
}
49+
}
50+
51+
diagnostics
52+
}
53+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
/// Changing a column type may break existing clients.
7+
///
8+
/// Changing a column's data type requires an exclusive lock on the table while the entire table is rewritten.
9+
/// This can take a long time for large tables and will block reads and writes.
10+
///
11+
/// Instead of changing the type directly, consider creating a new column with the desired type,
12+
/// migrating the data, and then dropping the old column.
13+
///
14+
/// ## Examples
15+
///
16+
/// ### Invalid
17+
///
18+
/// ```sql,expect_diagnostic
19+
/// ALTER TABLE "core_recipe" ALTER COLUMN "edits" TYPE text USING "edits"::text;
20+
/// ```
21+
///
22+
pub ChangingColumnType {
23+
version: "next",
24+
name: "changingColumnType",
25+
severity: Severity::Warning,
26+
recommended: false,
27+
sources: &[RuleSource::Squawk("changing-column-type")],
28+
}
29+
}
30+
31+
impl Rule for ChangingColumnType {
32+
type Options = ();
33+
34+
fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
35+
let mut diagnostics = Vec::new();
36+
37+
if let pgt_query::NodeEnum::AlterTableStmt(stmt) = ctx.stmt() {
38+
for cmd in &stmt.cmds {
39+
if let Some(pgt_query::NodeEnum::AlterTableCmd(cmd)) = &cmd.node {
40+
if cmd.subtype() == pgt_query::protobuf::AlterTableType::AtAlterColumnType {
41+
diagnostics.push(RuleDiagnostic::new(
42+
rule_category!(),
43+
None,
44+
markup! {
45+
"Changing a column type requires a table rewrite and blocks reads and writes."
46+
}
47+
).detail(None, "Consider creating a new column with the desired type, migrating data, and then dropping the old column."));
48+
}
49+
}
50+
}
51+
}
52+
53+
diagnostics
54+
}
55+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
/// Adding constraints without NOT VALID blocks all reads and writes.
7+
///
8+
/// When adding a CHECK or FOREIGN KEY constraint, PostgreSQL must validate all existing rows,
9+
/// which requires a full table scan. This blocks reads and writes for the duration.
10+
///
11+
/// Instead, add the constraint with NOT VALID first, then VALIDATE CONSTRAINT in a separate
12+
/// transaction. This allows reads and writes to continue while validation happens.
13+
///
14+
/// ## Examples
15+
///
16+
/// ### Invalid
17+
///
18+
/// ```sql,expect_diagnostic
19+
/// ALTER TABLE distributors ADD CONSTRAINT distfk FOREIGN KEY (address) REFERENCES addresses (address);
20+
/// ```
21+
///
22+
/// ### Valid
23+
///
24+
/// ```sql
25+
/// ALTER TABLE distributors ADD CONSTRAINT distfk FOREIGN KEY (address) REFERENCES addresses (address) NOT VALID;
26+
/// ```
27+
///
28+
pub ConstraintMissingNotValid {
29+
version: "next",
30+
name: "constraintMissingNotValid",
31+
severity: Severity::Warning,
32+
recommended: false,
33+
sources: &[RuleSource::Squawk("constraint-missing-not-valid")],
34+
}
35+
}
36+
37+
impl Rule for ConstraintMissingNotValid {
38+
type Options = ();
39+
40+
fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
41+
let mut diagnostics = Vec::new();
42+
43+
if let pgt_query::NodeEnum::AlterTableStmt(stmt) = ctx.stmt() {
44+
for cmd in &stmt.cmds {
45+
if let Some(pgt_query::NodeEnum::AlterTableCmd(cmd)) = &cmd.node {
46+
// Check if we're adding a constraint
47+
if let Some(pgt_query::NodeEnum::Constraint(constraint)) =
48+
cmd.def.as_ref().and_then(|d| d.node.as_ref())
49+
{
50+
// Skip if the constraint has NOT VALID
51+
if constraint.initially_valid {
52+
// Only warn for CHECK and FOREIGN KEY constraints
53+
match constraint.contype() {
54+
pgt_query::protobuf::ConstrType::ConstrCheck
55+
| pgt_query::protobuf::ConstrType::ConstrForeign => {
56+
diagnostics.push(RuleDiagnostic::new(
57+
rule_category!(),
58+
None,
59+
markup! {
60+
"Adding a constraint without NOT VALID will block reads and writes while validating existing rows."
61+
}
62+
).detail(None, "Add the constraint as NOT VALID in one transaction, then run VALIDATE CONSTRAINT in a separate transaction."));
63+
}
64+
_ => {}
65+
}
66+
}
67+
}
68+
}
69+
}
70+
}
71+
72+
diagnostics
73+
}
74+
}

0 commit comments

Comments
 (0)