Skip to content

Commit 546ae73

Browse files
authored
fix warnings for generated columns (#397)
fixes #396 reverts 0917fe4, which broke the build
1 parent 0917fe4 commit 546ae73

14 files changed

+153
-17
lines changed

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
## v1.4.1
10+
## v1.5.0 - 2025-01-09
11+
12+
### Added
13+
14+
- Added warning to `adding-field-with-default` to warn about generated columns (#397).
15+
16+
### Fixed
17+
18+
- Fixed bug in `adding-required-field` where generated columns were incorrectly flagged (#397).
19+
20+
## v1.4.1 - 2024-10-10
1121

1222
### Fixed
1323

Cargo.lock

Lines changed: 12 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "squawk"
3-
version = "1.4.0"
3+
version = "1.5.0"
44
authors = ["Steve Dignam <[email protected]>"]
55
edition = "2018"
66
license = "GPL-3.0"

cli/src/reporter.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -481,10 +481,10 @@ fn get_sql_file_content(violation: &ViolationContent) -> Result<String, std::io:
481481

482482
let violation_content = if violation_count > 0 {
483483
format!(
484-
r#"
484+
r"
485485
```
486486
{}
487-
```"#,
487+
```",
488488
violations_text.trim_matches('\n')
489489
)
490490
} else {
@@ -494,7 +494,7 @@ fn get_sql_file_content(violation: &ViolationContent) -> Result<String, std::io:
494494
let violations_emoji = get_violations_emoji(violation_count);
495495

496496
Ok(format!(
497-
r#"
497+
r"
498498
<h3><code>{filename}</code></h3>
499499
500500
```sql
@@ -506,7 +506,7 @@ fn get_sql_file_content(violation: &ViolationContent) -> Result<String, std::io:
506506
{violation_content}
507507
508508
---
509-
"#,
509+
",
510510
violations_emoji = violations_emoji,
511511
filename = violation.filename,
512512
sql = sql,
@@ -521,7 +521,7 @@ pub fn get_comment_body(files: &[ViolationContent], version: &str) -> String {
521521
let violations_emoji = get_violations_emoji(violations_count);
522522

523523
format!(
524-
r#"
524+
r"
525525
# Squawk Report
526526
527527
### **{violations_emoji} {violation_count}** violations across **{file_count}** file(s)
@@ -532,7 +532,7 @@ pub fn get_comment_body(files: &[ViolationContent], version: &str) -> String {
532532
[📚 More info on rules](https://github.com/sbdchd/squawk#rules)
533533
534534
⚡️ Powered by [`Squawk`](https://github.com/sbdchd/squawk) ({version}), a linter for PostgreSQL, focused on migrations
535-
"#,
535+
",
536536
violations_emoji = violations_emoji,
537537
violation_count = violations_count,
538538
file_count = files.len(),

docs/docs/adding-field-with-default.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,40 @@ We add our column as nullable, set a default for new rows, backfill our column (
4545
See ["How not valid constraints work"](constraint-missing-not-valid.md#how-not-valid-validate-works) for more information on adding constraints as `NOT VALID`.
4646

4747

48+
### Adding a generated column
49+
50+
Adding a generated column in Postgres will cause a table rewrite, locking the table while the update completes.
51+
52+
Instead of using a generated column, use a trigger.
53+
54+
55+
Instead of:
56+
57+
```sql
58+
ALTER TABLE "core_recipe" ADD COLUMN "foo" integer GENERATED ALWAAYS as ("bar" + "baz") STORED;
59+
```
60+
61+
Use:
62+
63+
```sql
64+
ALTER TABLE "core_recipe" ADD COLUMN "foo" integer;
65+
66+
-- backfill column in batches
67+
68+
-- add a trigger to match the generated column behavior
69+
CREATE FUNCTION foo_update_trigger()
70+
RETURNS trigger AS '
71+
BEGIN
72+
NEW.foo := NEW.bar + NEW.baz;
73+
RETURN NEW;
74+
END' LANGUAGE 'plpgsql';
75+
76+
CREATE TRIGGER update_foo_column
77+
BEFORE INSERT ON core_recipe
78+
FOR EACH ROW
79+
EXECUTE PROCEDURE foo_update_trigger();
80+
```
81+
4882
## solution for alembic and sqlalchemy
4983

5084
Instead of:

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
{
1919
squawk = final.rustPlatform.buildRustPackage {
2020
pname = "squawk";
21-
version = "1.4.0";
21+
version = "1.5.0";
2222

2323
cargoLock = {
2424
lockFile = ./Cargo.lock;

linter/src/rules/adding_field_with_default.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::HashSet;
22

33
use crate::{
44
versions::Version,
5-
violations::{RuleViolation, RuleViolationKind},
5+
violations::{RuleViolation, RuleViolationKind, ViolationMessage},
66
};
77

88
use serde_json::{json, Value};
@@ -73,6 +73,20 @@ pub fn adding_field_with_default(
7373
None,
7474
));
7575
}
76+
if constraint.contype == ConstrType::Generated {
77+
errs.push(RuleViolation::new(
78+
RuleViolationKind::AddingFieldWithDefault,
79+
raw_stmt.into(),
80+
Some(vec![
81+
ViolationMessage::Note(
82+
"Adding a generated column requires a table rewrite with an ACCESS EXCLUSIVE lock.".into(),
83+
),
84+
ViolationMessage::Help(
85+
"Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead.".into(),
86+
),
87+
]),
88+
));
89+
}
7690
}
7791
}
7892
_ => continue,
@@ -240,4 +254,14 @@ alter table account_metadata add column blah integer default 2 + 2;
240254
let pg_version_11 = Some(Version::from_str("11.0.0").unwrap());
241255
assert_debug_snapshot!(lint_sql(ok_sql, pg_version_11));
242256
}
257+
258+
#[test]
259+
fn test_generated_stored() {
260+
let bad_sql = r"
261+
ALTER TABLE foo
262+
ADD COLUMN bar numeric GENERATED ALWAYS AS (bar + baz) STORED;
263+
";
264+
let pg_version_11 = Some(Version::from_str("11.0.0").unwrap());
265+
assert_debug_snapshot!(lint_sql(bad_sql, pg_version_11));
266+
}
243267
}

linter/src/rules/adding_required_field.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ use crate::violations::{RuleViolation, RuleViolationKind};
44
use squawk_parser::ast::{
55
AlterTableCmds, AlterTableDef, AlterTableType, ColumnDefConstraint, ConstrType, RawStmt, Stmt,
66
};
7+
fn has_generated_constraint(constraints: &[ColumnDefConstraint]) -> bool {
8+
for ColumnDefConstraint::Constraint(constraint) in constraints {
9+
if constraint.contype == ConstrType::Generated {
10+
return true;
11+
}
12+
}
13+
false
14+
}
715

816
fn has_not_null_and_no_default_constraint(constraints: &[ColumnDefConstraint]) -> bool {
917
let mut has_not_null = false;
@@ -33,6 +41,9 @@ pub fn adding_required_field(
3341
for AlterTableCmds::AlterTableCmd(cmd) in &stmt.cmds {
3442
if cmd.subtype == AlterTableType::AddColumn {
3543
if let Some(AlterTableDef::ColumnDef(column_def)) = &cmd.def {
44+
if has_generated_constraint(&column_def.constraints) {
45+
continue;
46+
}
3647
if has_not_null_and_no_default_constraint(&column_def.constraints) {
3748
errs.push(RuleViolation::new(
3849
RuleViolationKind::AddingRequiredField,
@@ -86,4 +97,20 @@ ALTER TABLE "recipe" ADD COLUMN "public" boolean NOT NULL;
8697
"#;
8798
assert_debug_snapshot!(lint_sql(bad_sql));
8899
}
100+
#[test]
101+
fn test_generated_stored_not_null() {
102+
let ok_sql = r"
103+
ALTER TABLE foo
104+
ADD COLUMN bar numeric GENERATED ALWAYS AS (bar + baz) STORED NOT NULL;
105+
";
106+
assert_debug_snapshot!(lint_sql(ok_sql));
107+
}
108+
#[test]
109+
fn test_generated_stored() {
110+
let ok_sql = r"
111+
ALTER TABLE foo
112+
ADD COLUMN bar numeric GENERATED ALWAYS AS (bar + baz) STORED ;
113+
";
114+
assert_debug_snapshot!(lint_sql(ok_sql));
115+
}
89116
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
source: linter/src/rules/adding_field_with_default.rs
3+
expression: "lint_sql(bad_sql, pg_version_11)"
4+
---
5+
[
6+
RuleViolation {
7+
kind: AddingFieldWithDefault,
8+
span: Span {
9+
start: 0,
10+
len: Some(
11+
90,
12+
),
13+
},
14+
messages: [
15+
Note(
16+
"Adding a generated column requires a table rewrite with an ACCESS EXCLUSIVE lock.",
17+
),
18+
Help(
19+
"Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead.",
20+
),
21+
],
22+
},
23+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
source: linter/src/rules/adding_required_field.rs
3+
expression: lint_sql(ok_sql)
4+
---
5+
[]

0 commit comments

Comments
 (0)