Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/squawk_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use rules::ban_drop_column;
use rules::ban_drop_database;
use rules::ban_drop_not_null;
use rules::ban_drop_table;
use rules::ban_truncate_cascade;
use rules::changing_column_type;
use rules::constraint_missing_not_valid;
use rules::disallow_unique_constraint;
Expand Down Expand Up @@ -108,6 +109,8 @@ pub enum Rule {
BanCreateDomainWithConstraint,
#[serde(rename = "ban-alter-domain-with-add-constraint")]
BanAlterDomainWithAddConstraint,
#[serde(rename = "ban-truncate-cascade")]
BanTruncateCascade,
// xtask:new-rule:error-name
}

Expand Down Expand Up @@ -145,6 +148,7 @@ impl TryFrom<&str> for Rule {
}
"ban-create-domain-with-constraint" => Ok(Rule::BanCreateDomainWithConstraint),
"ban-alter-domain-with-add-constraint" => Ok(Rule::BanAlterDomainWithAddConstraint),
"ban-truncate-cascade" => Ok(Rule::BanTruncateCascade),
// xtask:new-rule:str-name
_ => Err(format!("Unknown violation name: {}", s)),
}
Expand Down Expand Up @@ -202,6 +206,7 @@ impl fmt::Display for Rule {
Rule::BanCreateDomainWithConstraint => "ban-create-domain-with-constraint",
Rule::UnusedIgnore => "unused-ignore",
Rule::BanAlterDomainWithAddConstraint => "ban-alter-domain-with-add-constraint",
Rule::BanTruncateCascade => "ban-truncate-cascade",
// xtask:new-rule:variant-to-name
};
write!(f, "{}", val)
Expand Down Expand Up @@ -345,6 +350,9 @@ impl Linter {
if self.rules.contains(&Rule::TransactionNesting) {
transaction_nesting(self, &file);
}
if self.rules.contains(&Rule::BanTruncateCascade) {
ban_truncate_cascade(self, &file);
}
// xtask:new-rule:rule-call

// locate any ignores in the file
Expand Down
60 changes: 60 additions & 0 deletions crates/squawk_linter/src/rules/ban_truncate_cascade.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use squawk_syntax::{
ast::{self, HasModuleItem},
Parse, SourceFile,
};

use crate::{Linter, Rule, Violation};

pub(crate) fn ban_truncate_cascade(ctx: &mut Linter, parse: &Parse<SourceFile>) {
let file = parse.tree();
for item in file.items() {
match item {
ast::Item::Truncate(truncate) => {
if let Some(cascade) = truncate.cascade_token() {
// TODO: if we had knowledge about the entire schema, we
// could be more precise here and actually navigate the
// foreign keys.
ctx.report(Violation::new(
Rule::BanTruncateCascade,
format!("Using `CASCADE` will recursively truncate any tables that foreign key to the referenced tables! So if you had foreign keys setup as `a <- b <- c` and truncated `a`, then `b` & `c` would also be truncated!"),
cascade.text_range(),
"Remove the `CASCADE` and specify exactly which tables you want to truncate.".to_string(),
));
}
}
_ => (),
}
}
}

#[cfg(test)]
mod test {
use insta::assert_debug_snapshot;

use crate::{Linter, Rule};
use squawk_syntax::SourceFile;

#[test]
fn err() {
let sql = r#"
truncate a, b, c cascade;
"#;
let file = SourceFile::parse(sql);
let mut linter = Linter::from([Rule::BanTruncateCascade]);
let errors = linter.lint(file, sql);
assert_ne!(errors.len(), 0);
assert_debug_snapshot!(errors);
}

#[test]
fn ok() {
let sql = r#"
truncate a, b, c;
truncate a;
"#;
let file = SourceFile::parse(sql);
let mut linter = Linter::from([Rule::BanTruncateCascade]);
let errors = linter.lint(file, sql);
assert_eq!(errors.len(), 0);
}
}
2 changes: 2 additions & 0 deletions crates/squawk_linter/src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub(crate) mod ban_drop_column;
pub(crate) mod ban_drop_database;
pub(crate) mod ban_drop_not_null;
pub(crate) mod ban_drop_table;
pub(crate) mod ban_truncate_cascade;
pub(crate) mod changing_column_type;
pub(crate) mod constraint_missing_not_valid;
pub(crate) mod disallow_unique_constraint;
Expand Down Expand Up @@ -40,6 +41,7 @@ pub(crate) use ban_drop_column::ban_drop_column;
pub(crate) use ban_drop_database::ban_drop_database;
pub(crate) use ban_drop_not_null::ban_drop_not_null;
pub(crate) use ban_drop_table::ban_drop_table;
pub(crate) use ban_truncate_cascade::ban_truncate_cascade;
pub(crate) use changing_column_type::changing_column_type;
pub(crate) use constraint_missing_not_valid::constraint_missing_not_valid;
pub(crate) use disallow_unique_constraint::disallow_unique_constraint;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: crates/squawk_linter/src/rules/ban_truncate_cascade.rs
expression: errors
---
[
Violation {
code: BanTruncateCascade,
message: "Using `CASCADE` will recursively truncate any tables that foreign key to the referenced tables! So if you had foreign keys setup as `a <- b <- c` and truncated `a`, then `b` & `c` would also be truncated!",
text_range: 26..33,
help: Some(
"Remove the `CASCADE` and specify exactly which tables you want to truncate.",
),
},
]
23 changes: 12 additions & 11 deletions crates/squawk_parser/src/grammar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9781,11 +9781,7 @@ fn lock_stmt(p: &mut Parser<'_>) -> CompletedMarker {
p.bump(LOCK_KW);
// [ TABLE ]
p.eat(TABLE_KW);
// [ ONLY ] name [ * ] [, ...]
relation_name(p);
while !p.at(EOF) && p.eat(COMMA) {
relation_name(p);
}
table_list(p);
// [ IN lockmode MODE ]
if p.eat(IN_KW) {
match (p.current(), p.nth(1)) {
Expand Down Expand Up @@ -9828,6 +9824,16 @@ fn lock_stmt(p: &mut Parser<'_>) -> CompletedMarker {
m.complete(p, LOCK_STMT)
}

// [ ONLY ] name [ * ] [, ... ]
fn table_list(p: &mut Parser<'_>) {
let m = p.start();
relation_name(p);
while !p.at(EOF) && p.eat(COMMA) {
relation_name(p);
}
m.complete(p, TABLE_LIST);
}

// [ WITH with_query [, ...] ]
// MERGE INTO [ ONLY ] target_table_name [ * ] [ [ AS ] target_alias ]
// USING data_source ON join_condition
Expand Down Expand Up @@ -11098,12 +11104,7 @@ fn truncate_stmt(p: &mut Parser<'_>) -> CompletedMarker {
let m = p.start();
p.bump(TRUNCATE_KW);
p.eat(TABLE_KW);
while !p.at(EOF) {
relation_name(p);
if !p.eat(COMMA) {
break;
}
}
table_list(p);
if p.eat(RESTART_KW) {
p.expect(IDENTITY_KW);
}
Expand Down
Loading
Loading