Skip to content

Commit 6077ff5

Browse files
authored
linter: add fix for prefer-timestamptz (#617)
1 parent 4a5591d commit 6077ff5

6 files changed

+188
-52
lines changed

crates/squawk_linter/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,12 @@ impl Edit {
246246
text: Some(text.into()),
247247
}
248248
}
249+
pub fn replace<T: Into<String>>(text_range: TextRange, text: T) -> Self {
250+
Self {
251+
text_range,
252+
text: Some(text.into()),
253+
}
254+
}
249255
}
250256

251257
#[derive(Debug, Clone, PartialEq, Eq)]

crates/squawk_linter/src/rules/prefer_robust_stmts.rs

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -242,48 +242,12 @@ mod test {
242242
use insta::{assert_debug_snapshot, assert_snapshot};
243243

244244
use crate::{
245-
Edit, Linter, Rule,
246-
test_utils::{lint, lint_with_assume_in_transaction},
245+
Rule,
246+
test_utils::{fix_sql, lint, lint_with_assume_in_transaction},
247247
};
248248

249249
fn fix(sql: &str) -> String {
250-
let file = squawk_syntax::SourceFile::parse(sql);
251-
assert_eq!(file.errors().len(), 0);
252-
assert_eq!(file.errors().len(), 0, "Shouldn't start with syntax errors");
253-
let mut linter = Linter::from([Rule::PreferRobustStmts]);
254-
let errors = linter.lint(&file, sql);
255-
assert!(!errors.is_empty(), "Should start with linter errors");
256-
257-
let fixes = errors.into_iter().flat_map(|x| x.fix).collect::<Vec<_>>();
258-
259-
let mut result = sql.to_string();
260-
261-
let mut all_edits: Vec<&Edit> = fixes.iter().flat_map(|fix| &fix.edits).collect();
262-
263-
all_edits.sort_by(|a, b| b.text_range.start().cmp(&a.text_range.start()));
264-
265-
for edit in all_edits {
266-
let start: usize = edit.text_range.start().into();
267-
let end: usize = edit.text_range.end().into();
268-
let text = edit.text.as_ref().map_or("", |v| v);
269-
result.replace_range(start..end, text);
270-
}
271-
272-
let file = squawk_syntax::SourceFile::parse(&result);
273-
assert_eq!(
274-
file.errors().len(),
275-
0,
276-
"Shouldn't introduce any syntax errors"
277-
);
278-
let mut linter = Linter::from([Rule::PreferRobustStmts]);
279-
let errors = linter.lint(&file, &result);
280-
assert_eq!(
281-
errors.len(),
282-
0,
283-
"Fixes should remove all the linter errors."
284-
);
285-
286-
result
250+
fix_sql(sql, Rule::PreferRobustStmts)
287251
}
288252

289253
#[test]

crates/squawk_linter/src/rules/prefer_timestamptz.rs

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use squawk_syntax::{
33
ast::{self, AstNode},
44
};
55

6-
use crate::{Linter, Rule, Violation};
6+
use crate::{Edit, Fix, Linter, Rule, Violation};
77
use crate::{identifier::Identifier, visitors::check_not_allowed_types};
88

99
pub fn is_not_allowed_timestamp(ty: &ast::Type) -> bool {
@@ -44,14 +44,33 @@ pub fn is_not_allowed_timestamp(ty: &ast::Type) -> bool {
4444
}
4545
}
4646

47+
fn fix_timestamp(ty: &ast::Type) -> Option<Fix> {
48+
match ty {
49+
ast::Type::TimeType(_) => {
50+
let range = ty.syntax().text_range();
51+
let edit = Edit::replace(range, "timestamptz");
52+
Some(Fix::new("Replace with `timestamptz`", vec![edit]))
53+
}
54+
ast::Type::ArrayType(array_type) => {
55+
if let Some(inner_ty) = array_type.ty() {
56+
fix_timestamp(&inner_ty)
57+
} else {
58+
None
59+
}
60+
}
61+
_ => None,
62+
}
63+
}
64+
4765
fn check_ty_for_timestamp(ctx: &mut Linter, ty: Option<ast::Type>) {
4866
if let Some(ty) = ty {
4967
if is_not_allowed_timestamp(&ty) {
68+
let fix = fix_timestamp(&ty);
5069
ctx.report(Violation::for_node(
5170
Rule::PreferTimestampTz,
5271
"When Postgres stores a datetime in a `timestamp` field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time.".into(),
5372
ty.syntax(),
54-
).help("Use timestamptz instead of timestamp for your column type."));
73+
).help("Use `timestamptz` instead of `timestamp` for your column type.").fix(fix));
5574
};
5675
}
5776
}
@@ -63,10 +82,70 @@ pub(crate) fn prefer_timestamptz(ctx: &mut Linter, parse: &Parse<SourceFile>) {
6382

6483
#[cfg(test)]
6584
mod test {
66-
use insta::assert_debug_snapshot;
85+
use insta::{assert_debug_snapshot, assert_snapshot};
6786

6887
use crate::Rule;
69-
use crate::test_utils::lint;
88+
use crate::test_utils::{fix_sql, lint};
89+
90+
fn fix(sql: &str) -> String {
91+
fix_sql(sql, Rule::PreferTimestampTz)
92+
}
93+
94+
#[test]
95+
fn fix_timestamp_to_timestamptz() {
96+
assert_snapshot!(fix("
97+
create table app.users
98+
(
99+
created_ts timestamp
100+
);
101+
"), @r"
102+
create table app.users
103+
(
104+
created_ts timestamptz
105+
);
106+
");
107+
}
108+
109+
#[test]
110+
fn fix_timestamp_without_time_zone() {
111+
assert_snapshot!(fix("
112+
create table app.accounts
113+
(
114+
created_ts timestamp without time zone
115+
);
116+
"), @r"
117+
create table app.accounts
118+
(
119+
created_ts timestamptz
120+
);
121+
");
122+
}
123+
124+
#[test]
125+
fn fix_alter_table_timestamp() {
126+
assert_snapshot!(fix("
127+
alter table app.users
128+
alter column created_ts type timestamp;
129+
"), @r"
130+
alter table app.users
131+
alter column created_ts type timestamptz;
132+
");
133+
}
134+
135+
#[test]
136+
fn fix_timestamp_array() {
137+
assert_snapshot!(fix("
138+
create table app.events
139+
(
140+
timestamps timestamp[]
141+
);
142+
"), @r"
143+
create table app.events
144+
(
145+
timestamps timestamptz[]
146+
);
147+
");
148+
}
70149

71150
#[test]
72151
fn create_table_with_timestamp_err() {

crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_timestamptz__test__alter_table_with_timestamp_err.snap

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,41 @@ expression: errors
88
message: "When Postgres stores a datetime in a `timestamp` field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time.",
99
text_range: 56..65,
1010
help: Some(
11-
"Use timestamptz instead of timestamp for your column type.",
11+
"Use `timestamptz` instead of `timestamp` for your column type.",
12+
),
13+
fix: Some(
14+
Fix {
15+
title: "Replace with `timestamptz`",
16+
edits: [
17+
Edit {
18+
text_range: 56..65,
19+
text: Some(
20+
"timestamptz",
21+
),
22+
},
23+
],
24+
},
1225
),
13-
fix: None,
1426
},
1527
Violation {
1628
code: PreferTimestampTz,
1729
message: "When Postgres stores a datetime in a `timestamp` field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time.",
1830
text_range: 125..152,
1931
help: Some(
20-
"Use timestamptz instead of timestamp for your column type.",
32+
"Use `timestamptz` instead of `timestamp` for your column type.",
33+
),
34+
fix: Some(
35+
Fix {
36+
title: "Replace with `timestamptz`",
37+
edits: [
38+
Edit {
39+
text_range: 125..152,
40+
text: Some(
41+
"timestamptz",
42+
),
43+
},
44+
],
45+
},
2146
),
22-
fix: None,
2347
},
2448
]

crates/squawk_linter/src/rules/snapshots/squawk_linter__rules__prefer_timestamptz__test__create_table_with_timestamp_err.snap

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,41 @@ expression: errors
88
message: "When Postgres stores a datetime in a `timestamp` field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time.",
99
text_range: 43..52,
1010
help: Some(
11-
"Use timestamptz instead of timestamp for your column type.",
11+
"Use `timestamptz` instead of `timestamp` for your column type.",
12+
),
13+
fix: Some(
14+
Fix {
15+
title: "Replace with `timestamptz`",
16+
edits: [
17+
Edit {
18+
text_range: 43..52,
19+
text: Some(
20+
"timestamptz",
21+
),
22+
},
23+
],
24+
},
1225
),
13-
fix: None,
1426
},
1527
Violation {
1628
code: PreferTimestampTz,
1729
message: "When Postgres stores a datetime in a `timestamp` field, Postgres drops the UTC offset. This means 2019-10-11 21:11:24+02 and 2019-10-11 21:11:24-06 will both be stored as 2019-10-11 21:11:24 in the database, even though they are eight hours apart in time.",
1830
text_range: 99..126,
1931
help: Some(
20-
"Use timestamptz instead of timestamp for your column type.",
32+
"Use `timestamptz` instead of `timestamp` for your column type.",
33+
),
34+
fix: Some(
35+
Fix {
36+
title: "Replace with `timestamptz`",
37+
edits: [
38+
Edit {
39+
text_range: 99..126,
40+
text: Some(
41+
"timestamptz",
42+
),
43+
},
44+
],
45+
},
2146
),
22-
fix: None,
2347
},
2448
]

crates/squawk_linter/src/test_utils.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Linter, Rule, Violation};
1+
use crate::{Edit, Linter, Rule, Violation};
22

33
pub(crate) fn lint(sql: &str, rule: Rule) -> Vec<Violation> {
44
let file = squawk_syntax::SourceFile::parse(sql);
@@ -14,3 +14,42 @@ pub(crate) fn lint_with_assume_in_transaction(sql: &str, rule: Rule) -> Vec<Viol
1414
linter.settings.assume_in_transaction = true;
1515
linter.lint(&file, sql)
1616
}
17+
18+
pub(crate) fn fix_sql(sql: &str, rule: Rule) -> String {
19+
let file = squawk_syntax::SourceFile::parse(sql);
20+
assert_eq!(file.errors().len(), 0, "Shouldn't start with syntax errors");
21+
let mut linter = Linter::from([rule]);
22+
let errors = linter.lint(&file, sql);
23+
assert!(!errors.is_empty(), "Should start with linter errors");
24+
25+
let fixes = errors.into_iter().flat_map(|x| x.fix).collect::<Vec<_>>();
26+
27+
let mut result = sql.to_string();
28+
29+
let mut all_edits: Vec<&Edit> = fixes.iter().flat_map(|fix| &fix.edits).collect();
30+
31+
all_edits.sort_by(|a, b| b.text_range.start().cmp(&a.text_range.start()));
32+
33+
for edit in all_edits {
34+
let start: usize = edit.text_range.start().into();
35+
let end: usize = edit.text_range.end().into();
36+
let text = edit.text.as_ref().map_or("", |v| v);
37+
result.replace_range(start..end, text);
38+
}
39+
40+
let file = squawk_syntax::SourceFile::parse(&result);
41+
assert_eq!(
42+
file.errors().len(),
43+
0,
44+
"Shouldn't introduce any syntax errors"
45+
);
46+
let mut linter = Linter::from([rule]);
47+
let errors = linter.lint(&file, &result);
48+
assert_eq!(
49+
errors.len(),
50+
0,
51+
"Fixes should remove all the linter errors."
52+
);
53+
54+
result
55+
}

0 commit comments

Comments
 (0)