Skip to content

Commit 496c86f

Browse files
committed
Adds ban-truncate-cascade
1 parent 900bf10 commit 496c86f

10 files changed

Lines changed: 136 additions & 5 deletions

File tree

BUILDING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ docker run -d \
4747
-e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
4848
-p 9000:9000 \
4949
-v $(pwd)/target/sonar-postgres-plugin-1.4-SNAPSHOT.jar:/opt/sonarqube/extensions/plugins/sonar-postgres-plugin-1.4-SNAPSHOT.jar \
50-
sonarqube:lts-community
50+
sonarqube:24.12.0.100206-community
5151
xdg-open http://localhost:9000/
5252
docker logs -f sonarqube
5353
```

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Check [BUILDING.md](BUILDING.md)
1919
* [ban-drop-database](src/main/resources/com/premiumminds/sonar/postgres/ban-drop-database.md)
2020
* [ban-alter-domain-with-add-constraint](src/main/resources/com/premiumminds/sonar/postgres/ban-alter-domain-with-add-constraint.md)
2121
* [ban-create-domain-with-constraint](src/main/resources/com/premiumminds/sonar/postgres/ban-create-domain-with-constraint.md)
22+
* [ban-truncate-cascade](src/main/resources/com/premiumminds/sonar/postgres/ban-truncate-cascade.md)
2223
* [changing-column-type](src/main/resources/com/premiumminds/sonar/postgres/changing-column-type.md)
2324
* [cluster](src/main/resources/com/premiumminds/sonar/postgres/cluster.md)
2425
* [concurrently](src/main/resources/com/premiumminds/sonar/postgres/concurrently.md)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TRUNCATE a, b, c CASCADE;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TRUNCATE a, b, c;

src/main/java/com/premiumminds/sonar/postgres/PostgresSqlQualityProfile.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_BAN_CHAR_FIELD;
1111
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_BAN_CREATE_DOMAIN_WITH_CONSTRAINT;
1212
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_BAN_DROP_DATABASE;
13+
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_BAN_TRUNCATE_CASCADE;
1314
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_CHANGING_COLUMN_TYPE;
1415
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_CLUSTER;
1516
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_CONCURRENTLY;
@@ -45,6 +46,7 @@ public void define(Context context) {
4546
activateRule(profile, RULE_ADDING_SERIAL_PRIMARY_KEY_FIELD);
4647
activateRule(profile, RULE_BAN_CHAR_FIELD);
4748
activateRule(profile, RULE_BAN_ALTER_DOMAIN_WITH_CONSTRAINT);
49+
activateRule(profile, RULE_BAN_TRUNCATE_CASCADE);
4850
activateRule(profile, RULE_BAN_CREATE_DOMAIN_WITH_CONSTRAINT);
4951
activateRule(profile, RULE_BAN_DROP_DATABASE);
5052
activateRule(profile, RULE_CHANGING_COLUMN_TYPE);

src/main/java/com/premiumminds/sonar/postgres/PostgresSqlRulesDefinition.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.premiumminds.sonar.postgres.visitors.BanCharFieldVisitorCheck;
1111
import com.premiumminds.sonar.postgres.visitors.BanCreateDomainWithConstraintCheck;
1212
import com.premiumminds.sonar.postgres.visitors.BanDropDatabaseVisitorCheck;
13+
import com.premiumminds.sonar.postgres.visitors.BanTruncateCascade;
1314
import com.premiumminds.sonar.postgres.visitors.ChangingColumnTypeVisitorCheck;
1415
import com.premiumminds.sonar.postgres.visitors.ClusterVisitorCheck;
1516
import com.premiumminds.sonar.postgres.visitors.ConcurrentVisitorCheck;
@@ -46,6 +47,7 @@ public class PostgresSqlRulesDefinition implements RulesDefinition {
4647
public static final RuleKey RULE_BAN_CHAR_FIELD = RuleKey.of(REPOSITORY, "ban-char-field");
4748
public static final RuleKey RULE_BAN_CREATE_DOMAIN_WITH_CONSTRAINT = RuleKey.of(REPOSITORY, "ban-create-domain-with-constraint");
4849
public static final RuleKey RULE_BAN_ALTER_DOMAIN_WITH_CONSTRAINT = RuleKey.of(REPOSITORY, "ban-alter-domain-with-add-constraint");
50+
public static final RuleKey RULE_BAN_TRUNCATE_CASCADE = RuleKey.of(REPOSITORY, "ban-truncate-cascade");
4951
public static final RuleKey RULE_BAN_DROP_DATABASE = RuleKey.of(REPOSITORY, "ban-drop-database");
5052
public static final RuleKey RULE_CHANGING_COLUMN_TYPE = RuleKey.of(REPOSITORY, "changing-column-type");
5153
public static final RuleKey RULE_CONSTRAINT_MISSING_NOT_VALID = RuleKey.of(REPOSITORY, "constraint-missing-not-valid");
@@ -119,6 +121,11 @@ public void define(Context context) {
119121
.setType(RuleType.BUG)
120122
.setMarkdownDescription(getClass().getResource("ban-alter-domain-with-add-constraint.md"));
121123

124+
repository.createRule(RULE_BAN_TRUNCATE_CASCADE.rule())
125+
.setName("ban-truncate-cascade")
126+
.setType(RuleType.BUG)
127+
.setMarkdownDescription(getClass().getResource("ban-truncate-cascade.md"));
128+
122129
repository.createRule(RULE_BAN_DROP_DATABASE.rule())
123130
.setName("ban-drop-database rule")
124131
.setType(RuleType.BUG)
@@ -220,6 +227,7 @@ public static List<VisitorCheck> allChecks(){
220227
new BanCharFieldVisitorCheck(),
221228
new BanCreateDomainWithConstraintCheck(),
222229
new BanAlterDomainWithConstraintCheck(),
230+
new BanTruncateCascade(),
223231
new PreferTextFieldVisitorCheck(),
224232
new VacuumFullVisitorCheck(),
225233
new ClusterVisitorCheck(),
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.premiumminds.sonar.postgres.visitors;
2+
3+
import com.premiumminds.sonar.postgres.protobuf.DropBehavior;
4+
import com.premiumminds.sonar.postgres.protobuf.TruncateStmt;
5+
import org.sonar.api.rule.RuleKey;
6+
import org.sonar.check.Rule;
7+
8+
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_BAN_TRUNCATE_CASCADE;
9+
10+
@Rule(key = "ban-truncate-cascade")
11+
public class BanTruncateCascade extends AbstractVisitorCheck {
12+
13+
@Override
14+
public void visit(TruncateStmt truncateStmt) {
15+
16+
if (truncateStmt.getBehavior() == DropBehavior.DROP_CASCADE){
17+
newIssue("Truncate cascade will recursively truncate all related tables");
18+
}
19+
20+
super.visit(truncateStmt);
21+
}
22+
23+
@Override
24+
protected RuleKey getRule() {
25+
return RULE_BAN_TRUNCATE_CASCADE;
26+
}
27+
}

src/main/java/com/premiumminds/sonar/postgres/visitors/OnlySchemaMigrationsVisitorCheck.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
package com.premiumminds.sonar.postgres.visitors;
22

3-
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_ONLY_SCHEMA_MIGRATIONS;
4-
53
import com.premiumminds.sonar.postgres.protobuf.DeleteStmt;
6-
import com.premiumminds.sonar.postgres.protobuf.DoStmt;
7-
import com.premiumminds.sonar.postgres.protobuf.DropStmt;
84
import com.premiumminds.sonar.postgres.protobuf.InsertStmt;
95
import com.premiumminds.sonar.postgres.protobuf.TruncateStmt;
106
import com.premiumminds.sonar.postgres.protobuf.UpdateStmt;
117
import org.sonar.api.rule.RuleKey;
128
import org.sonar.check.Rule;
139

10+
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_ONLY_SCHEMA_MIGRATIONS;
11+
1412
@Rule(key = "only-schema-migrations")
1513
public class OnlySchemaMigrationsVisitorCheck extends AbstractVisitorCheck {
1614

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
== problem
2+
3+
Using `TRUNCATE`'s `CASCADE` option will truncate any tables that are also foreign-keyed to the specified tables.
4+
5+
So if you had tables with foreign-keys like:
6+
7+
``
8+
a <- b <- c
9+
``
10+
11+
and ran:
12+
13+
``sql
14+
truncate a cascade;
15+
``
16+
17+
You'd end up with `a`, `b`, & `c` all being truncated!
18+
19+
=== runnable example
20+
21+
Setup:
22+
23+
``sql
24+
create table a (
25+
a_id int primary key
26+
);
27+
create table b (
28+
b_id int primary key,
29+
a_id int,
30+
foreign key (a_id) references a(a_id)
31+
);
32+
create table c (
33+
c_id int primary key,
34+
b_id int,
35+
foreign key (b_id) references b(b_id)
36+
);
37+
insert into a (a_id) values (1), (2), (3);
38+
insert into b (b_id, a_id) values (101, 1), (102, 2), (103, 3);
39+
insert into c (c_id, b_id) values (1001, 101), (1002, 102), (1003, 103);
40+
``
41+
42+
Then you run:
43+
44+
``sql
45+
truncate a cascade;
46+
``
47+
48+
Which outputs:
49+
50+
``text
51+
NOTICE: truncate cascades to table "b"
52+
NOTICE: truncate cascades to table "c"
53+
54+
Query 1 OK: TRUNCATE TABLE
55+
``
56+
57+
And now tables `a`, `b`, & `c` are empty!
58+
59+
== solution
60+
61+
Don't use the `CASCADE` option, instead manually specify the tables you want.
62+
63+
So if you just wanted tables `a` and `b` from the example above:
64+
65+
``sql
66+
truncate a, b;
67+
``
68+
69+
== links
70+
71+
* https://www.postgresql.org/docs/current/sql-truncate.html
72+
* `CASCADE`'s recursive nature [caused Linear's 2024-01-24 incident](https://linear.app/blog/linear-incident-on-jan-24th-2024).

src/test/java/com/premiumminds/sonar/postgres/PostgresSqlSensorTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_BAN_CHAR_FIELD;
2828
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_BAN_CREATE_DOMAIN_WITH_CONSTRAINT;
2929
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_BAN_DROP_DATABASE;
30+
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_BAN_TRUNCATE_CASCADE;
3031
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_CHANGING_COLUMN_TYPE;
3132
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_CLUSTER;
3233
import static com.premiumminds.sonar.postgres.PostgresSqlRulesDefinition.RULE_CONCURRENTLY;
@@ -97,6 +98,7 @@ void testAllRules() {
9798
RULE_BAN_DROP_DATABASE,
9899
RULE_BAN_CREATE_DOMAIN_WITH_CONSTRAINT,
99100
RULE_BAN_ALTER_DOMAIN_WITH_CONSTRAINT,
101+
RULE_BAN_TRUNCATE_CASCADE,
100102
RULE_CHANGING_COLUMN_TYPE,
101103
RULE_CONSTRAINT_MISSING_NOT_VALID,
102104
RULE_DISALLOWED_UNIQUE_CONSTRAINT,
@@ -939,6 +941,25 @@ void banAlterDomainWithConstraints() {
939941
assertEquals(1, fileMap.size());
940942
}
941943

944+
@Test
945+
void banTruncateCascade() {
946+
createFile(contextTester, "file1.sql", "TRUNCATE a, b, c CASCADE;\n");
947+
createFile(contextTester, "file2-ok.sql", "TRUNCATE a, b, c;");
948+
949+
final RuleKey rule = RULE_BAN_TRUNCATE_CASCADE;
950+
PostgresSqlSensor sensor = getPostgresSqlSensor(rule);
951+
sensor.execute(contextTester);
952+
953+
final Map<RuleKey, Map<String, Issue>> issueMap = groupByRuleAndFile(contextTester.allIssues());
954+
955+
final Map<String, Issue> fileMap = issueMap.get(rule);
956+
957+
assertEquals("Truncate cascade will recursively truncate all related tables",
958+
fileMap.get(":file1.sql").primaryLocation().message());
959+
960+
assertEquals(1, fileMap.size());
961+
}
962+
942963
private PostgresSqlSensor getPostgresSqlSensor(RuleKey... ruleKey) {
943964
ActiveRulesBuilder activeRulesBuilder = new ActiveRulesBuilder();
944965

0 commit comments

Comments
 (0)