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
2 changes: 1 addition & 1 deletion crates/squawk_ide/src/binder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ fn bind_stmt(b: &mut Binder, stmt: ast::Stmt) {
ast::Stmt::Listen(listen) => bind_listen(b, listen),
ast::Stmt::Set(set) => bind_set(b, set),
ast::Stmt::CreatePolicy(create_policy) => bind_create_policy(b, create_policy),
_ => {}
_ => (),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/squawk_ide/src/document_symbols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ pub fn document_symbols(file: &ast::SourceFile) -> Vec<DocumentSymbol> {
}
}

_ => {}
_ => (),
}
}

Expand Down
7 changes: 4 additions & 3 deletions crates/squawk_linter/src/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ fn has_foreign_key_constraint(create_table: &ast::CreateTable) -> bool {
return true;
}
ast::TableArg::Column(column) => {
if let Some(ast::ColumnConstraint::ReferencesConstraint(_)) =
column.constraint()
if column
.constraints()
.any(|c| matches!(c, ast::ColumnConstraint::ReferencesConstraint(_)))
{
return true;
}
}
_ => {}
_ => (),
}
}
}
Expand Down
166 changes: 109 additions & 57 deletions crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,63 +15,103 @@ pub(crate) fn adding_foreign_key_constraint(ctx: &mut Linter, parse: &Parse<Sour
let tables_created = tables_created_in_transaction(ctx.settings.assume_in_transaction, &file);
// TODO: use match_ast! like in #api_walkthrough
for stmt in file.stmts() {
if let ast::Stmt::AlterTable(alter_table) = stmt {
if let Some(table_name) = alter_table
.relation_name()
.and_then(|x| x.path())
.and_then(|x| x.segment())
.and_then(|x| x.name_ref())
{
for action in alter_table.actions() {
match action {
ast::AlterTableAction::AddConstraint(add_constraint) => {
if add_constraint.not_valid().is_some()
|| tables_created.contains(&Identifier::new(&table_name.text()))
{
// Adding foreign key is okay when:
// - NOT VALID is specified.
// - The table is created in the same transaction
continue;
match stmt {
ast::Stmt::AlterTable(alter_table) => {
if let Some(table_name) = alter_table
.relation_name()
.and_then(|x| x.path())
.and_then(|x| x.segment())
.and_then(|x| x.name_ref())
{
for action in alter_table.actions() {
match action {
ast::AlterTableAction::AddConstraint(add_constraint) => {
if add_constraint.not_valid().is_some()
|| tables_created.contains(&Identifier::new(&table_name.text()))
{
// Adding foreign key is okay when:
// - NOT VALID is specified.
// - The table is created in the same transaction
continue;
}
if let Some(constraint) = add_constraint.constraint() {
if matches!(
constraint,
ast::Constraint::ForeignKeyConstraint(_)
| ast::Constraint::ReferencesConstraint(_)
) {
ctx.report(
Violation::for_node(
Rule::AddingForeignKeyConstraint,
message.into(),
constraint.syntax(),
)
.help(help),
)
}
}
}
if let Some(constraint) = add_constraint.constraint() {
if matches!(
constraint,
ast::Constraint::ForeignKeyConstraint(_)
| ast::Constraint::ReferencesConstraint(_)
) {
ctx.report(
Violation::for_node(
Rule::AddingForeignKeyConstraint,
message.into(),
constraint.syntax(),
ast::AlterTableAction::AddColumn(add_column) => {
for constraint in add_column.constraints() {
if matches!(
constraint,
ast::Constraint::ForeignKeyConstraint(_)
| ast::Constraint::ReferencesConstraint(_)
) {
ctx.report(
Violation::for_node(
Rule::AddingForeignKeyConstraint,
message.into(),
constraint.syntax(),
)
.help(help),
)
.help(help),
)
}
}
}
_ => continue,
}
ast::AlterTableAction::AddColumn(add_column) => {
for constraint in add_column.constraints() {
if matches!(
constraint,
ast::Constraint::ForeignKeyConstraint(_)
| ast::Constraint::ReferencesConstraint(_)
) {
ctx.report(
Violation::for_node(
Rule::AddingForeignKeyConstraint,
message.into(),
constraint.syntax(),
)
.help(help),
}
}
}
ast::Stmt::CreateTable(create_table) => {
if let Some(table_arg_list) = create_table.table_arg_list() {
for arg in table_arg_list.args() {
match arg {
ast::TableArg::TableConstraint(
ast::TableConstraint::ForeignKeyConstraint(fk),
) => {
ctx.report(
Violation::for_node(
Rule::AddingForeignKeyConstraint,
message.into(),
fk.syntax(),
)
.help(help),
);
}
ast::TableArg::Column(column) => {
for constraint in column.constraints() {
if let ast::ColumnConstraint::ReferencesConstraint(refs) =
constraint
{
ctx.report(
Violation::for_node(
Rule::AddingForeignKeyConstraint,
message.into(),
refs.syntax(),
)
.help(help),
);
}
}
}
_ => (),
}
_ => continue,
}
}
}
_ => (),
}
}
}
Expand All @@ -90,20 +130,32 @@ mod test {
#[test]
fn create_table_with_foreign_key_constraint() {
let sql = r#"
BEGIN;
CREATE TABLE email (
id BIGINT GENERATED ALWAYS AS IDENTITY,
user_id BIGINT,
email TEXT,
PRIMARY KEY(id),
CONSTRAINT fk_user
FOREIGN KEY ("user_id")
REFERENCES "user" ("id")
);
COMMIT;
BEGIN;
CREATE TABLE email (
id BIGINT GENERATED ALWAYS AS IDENTITY,
user_id BIGINT,
email TEXT,
PRIMARY KEY(id),
CONSTRAINT fk_user
FOREIGN KEY ("user_id")
REFERENCES "user" ("id")
);
COMMIT;
"#;

lint_ok(sql, Rule::AddingForeignKeyConstraint);
assert_snapshot!(lint_errors(sql, Rule::AddingForeignKeyConstraint));
}

#[test]
fn create_table_with_column_references() {
let sql = r#"
CREATE TABLE table_name (
id BIGSERIAL NOT NULL,
my_fk_id INTEGER NOT NULL REFERENCES other_table (id) ON DELETE CASCADE
);
"#;

assert_snapshot!(lint_errors(sql, Rule::AddingForeignKeyConstraint));
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/squawk_linter/src/rules/adding_not_null_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ pub(crate) fn adding_not_null_field(ctx: &mut Linter, parse: &Parse<SourceFile>)
.help("Make the field nullable and use a `CHECK` constraint instead."),
);
}
_ => {}
_ => (),
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs
expression: "lint_errors(sql, Rule::AddingForeignKeyConstraint)"
---
warning[adding-foreign-key-constraint]: Adding a foreign key constraint requires a table scan and a `SHARE ROW EXCLUSIVE` lock on both tables, which blocks writes to each table.
╭▸
4 │ my_fk_id INTEGER NOT NULL REFERENCES other_table (id) ON DELETE CASCADE
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
╰ help: Add `NOT VALID` to the constraint in one transaction and then VALIDATE the constraint in a separate transaction.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
source: crates/squawk_linter/src/rules/adding_foreign_key_constraint.rs
expression: "lint_errors(sql, Rule::AddingForeignKeyConstraint)"
---
warning[adding-foreign-key-constraint]: Adding a foreign key constraint requires a table scan and a `SHARE ROW EXCLUSIVE` lock on both tables, which blocks writes to each table.
╭▸
8 │ ┏ CONSTRAINT fk_user
9 │ ┃ FOREIGN KEY ("user_id")
10 │ ┃ REFERENCES "user" ("id")
│ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
╰ help: Add `NOT VALID` to the constraint in one transaction and then VALIDATE the constraint in a separate transaction.
4 changes: 2 additions & 2 deletions crates/squawk_syntax/src/ast/generated/nodes.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions crates/squawk_syntax/src/postgresql.ungram
Original file line number Diff line number Diff line change
Expand Up @@ -604,11 +604,11 @@ CompressionMethod =
Column =
'period'?
(
Name WithOptions? constraint:ColumnConstraint?
Name WithOptions? constraints:ColumnConstraint*
DeferrableConstraintOption? NotDeferrableConstraintOption?
InitiallyDeferredConstraintOption? InitiallyImmediateConstraintOption?
NotEnforced? Enforced?
| Name Type Storage? CompressionMethod? Collate? constraint:ColumnConstraint?
| Name Type Storage? CompressionMethod? Collate? constraints:ColumnConstraint*
DeferrableConstraintOption? NotDeferrableConstraintOption?
InitiallyDeferredConstraintOption? InitiallyImmediateConstraintOption?
NotEnforced? Enforced?
Expand Down
Loading