Skip to content

add foreign key check#132

Merged
ayarotsky merged 2 commits intoayarotsky:mainfrom
valkrypton:add-foreign-key-check
Mar 18, 2026
Merged

add foreign key check#132
ayarotsky merged 2 commits intoayarotsky:mainfrom
valkrypton:add-foreign-key-check

Conversation

@valkrypton
Copy link
Contributor

@valkrypton valkrypton commented Mar 16, 2026

What does this PR do?

Fixes #87
Summary:

  • Adds a new AddForeignKey check that checks, foreign keys being added using a ALTER TABLE statement must use a NOT VALID clause.
  • Add unit tests and new safe/unsafe fixture coverage, including fixture-suite expectation updates for file and violation totals.
  • Add safe and unsafe examples in docs/src/checks/add_foreign_key.md and linked them in the summary.
  • Moved code used to find foreign key columns, tables and columns referenced in the foreign key constraint, from unnamed_constraint_check.rs to pg_helpers.rs

Checklist

  • Relevant issue assigned under the Development section
  • Docs updated (docs/src/checks/<check>.md + docs/src/SUMMARY.md)
  • One check per PR
  • No unrelated changes

Summary by CodeRabbit

  • New Features

    • Detects unsafe additions of foreign keys that can block writes on large tables and recommends a safer two-step migration.
  • Documentation

    • Added guidance with bad/good SQL examples and updated summary to reflect the new check (now 25 built-in checks).
  • Tests

    • Added and updated fixtures and tests to cover safe/unsafe foreign-key scenarios and adjusted expected violation counts.

@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

Adds a new AddForeignKeyCheck that detects ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY statements added without NOT VALID, plus accompanying docs, pg helper functions, fixtures, tests, and registry/export integration.

Changes

Cohort / File(s) Summary
Documentation
docs/src/SUMMARY.md, docs/src/checks/add_foreign_key.md
Added sidebar entry and a new guidance doc describing unsafe ADD FOREIGN KEY without NOT VALID and the recommended two-step (NOT VALID → VALIDATE) with SQL examples.
New Check
src/checks/add_foreign_key.rs
New public AddForeignKeyCheck implementing Check; parses ALTER TABLE commands, detects FK additions lacking NOT VALID, and emits detailed violations with remediation snippets.
Registry / Exports
src/checks/mod.rs
Added mod add_foreign_key, pub use add_foreign_key::AddForeignKeyCheck, and register instantiation in the enabled-checks list.
PG helpers
src/checks/pg_helpers.rs
Added fk_cols_constraint(), ref_table_constraint(), and ref_columns_constraint() helpers to extract FK details from Constraint AST nodes.
Refactor: unnamed constraints
src/checks/unnamed_constraint.rs
Replaced inline FK-extraction logic with the new pg_helpers functions and adjusted imports; behavior preserved.
Fixtures
tests/fixtures/add_foreign_key_safe/up.sql, tests/fixtures/add_foreign_key_unsafe/up.sql, tests/fixtures/unnamed_constraint_safe/up.sql
Added safe two-step and unsafe fixtures; updated unnamed-constraint fixture to use NOT VALID.
Tests
tests/fixtures_test.rs
Added safe fixture to list, added test_add_foreign_key_unsafe_detected, updated expected violations/positions and aggregate counts (files with violations: 29, total violations: 38).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • ayarotsky

Poem

🐰 I hopped through migrations at midnight bright,
Spotted FKs that make the writers fight.
Say "NOT VALID" first, then validate in line,
Tables hum softly — no long locks this time. 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title "add foreign key check" is concise and clearly describes the main change—adding a new check for foreign key constraints.
Description check ✅ Passed The PR description covers all required sections: objective (fixes #87), implementation summary, documentation updates, and a completed checklist matching the template.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #87: detects unsafe foreign key additions without NOT VALID, enforces the two-step safe pattern, and includes documentation and test coverage.
Out of Scope Changes check ✅ Passed The PR refactors foreign key extraction logic from unnamed_constraint_check.rs to pg_helpers.rs as a supporting change; all modifications are directly related to implementing the AddForeignKeyCheck feature.
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can enforce grammar and style rules using `languagetool`.

Configure the reviews.tools.languagetool setting to enable/disable rules and categories. Refer to the LanguageTool Community to learn more.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/src/checks/add_foreign_key.md (1)

19-19: Minor clarification: "no lock" is slightly imprecise.

ADD CONSTRAINT ... NOT VALID still acquires a ShareRowExclusiveLock, but only briefly (to update catalog metadata), rather than holding it for the duration of a full table scan. Consider clarifying to "no table scan, brief lock" or "no validation lock" to be more precise.

📝 Suggested clarification
 -- Safe
--- Step 1 (no table scan, no lock)
+-- Step 1 (no table scan, brief metadata lock only)
 ALTER TABLE orders ADD CONSTRAINT fk_user_id
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/src/checks/add_foreign_key.md` at line 19, Update the wording in the
"Step 1 (no table scan, no lock)" heading/line in add_foreign_key.md to clarify
that ADD CONSTRAINT ... NOT VALID still takes a brief catalog lock: replace "no
lock" with a precise phrase such as "brief lock" or "no table scan, brief lock"
(update the heading "Step 1 (no table scan, no lock)" to e.g. "Step 1 (no table
scan, brief lock)") so the doc accurately reflects the ShareRowExclusiveLock
taken briefly to update metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/checks/add_foreign_key.rs`:
- Line 46: The remediation text in add_foreign_key.rs currently says "with any
validation" which is misleading; update the remediation string to read "without
immediate validation (NOT VALID)" so it instructs creating the foreign key
without immediate validation. Locate the remediation message in the
add_foreign_key.rs code (the string containing "with any validation") and
replace it with the corrected wording, ensuring the phrase "NOT VALID" is
included exactly as shown.
- Around line 33-51: The generated remediation SQL misformats the FK clause and
breaks for unnamed constraints: ensure the FK column list is wrapped as "FOREIGN
KEY ({columns})" (replace FOREIGN KEY {columns}) and handle constraint.conname
== "" by not emitting "CONSTRAINT <unnamed>" (or by generating a safe
deterministic name) — i.e., when constraint.conname is empty produce "ALTER
TABLE {table} ADD FOREIGN KEY ({columns}) REFERENCES {ref_table} ({ref_cols})
NOT VALID;" and only emit "ALTER TABLE {table} VALIDATE CONSTRAINT
{constraint_name};" when a real name exists; update the code that builds the
Violation message (variables constraint.conname / constraint_name, table_name,
fk_cols / columns, ref_table, ref_cols) inside add_foreign_key.rs so the SQL is
syntactically valid in both named and unnamed cases.

---

Nitpick comments:
In `@docs/src/checks/add_foreign_key.md`:
- Line 19: Update the wording in the "Step 1 (no table scan, no lock)"
heading/line in add_foreign_key.md to clarify that ADD CONSTRAINT ... NOT VALID
still takes a brief catalog lock: replace "no lock" with a precise phrase such
as "brief lock" or "no table scan, brief lock" (update the heading "Step 1 (no
table scan, no lock)" to e.g. "Step 1 (no table scan, brief lock)") so the doc
accurately reflects the ShareRowExclusiveLock taken briefly to update metadata.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ed21ee86-5c2b-489f-87dd-09dd82c6d4d2

📥 Commits

Reviewing files that changed from the base of the PR and between bcd72e7 and 06d737a.

📒 Files selected for processing (10)
  • docs/src/SUMMARY.md
  • docs/src/checks/add_foreign_key.md
  • src/checks/add_foreign_key.rs
  • src/checks/mod.rs
  • src/checks/pg_helpers.rs
  • src/checks/unnamed_constraint.rs
  • tests/fixtures/add_foreign_key_safe/up.sql
  • tests/fixtures/add_foreign_key_unsafe/up.sql
  • tests/fixtures/unnamed_constraint_safe/up.sql
  • tests/fixtures_test.rs

format!(
r#"For a safer foreign key addition on large tables:

1. Create a foreign key with any validation:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Wording is misleading in the remediation step.

Line 46 says “with any validation”; it should state “without immediate validation” (NOT VALID) to match the intended safe flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/checks/add_foreign_key.rs` at line 46, The remediation text in
add_foreign_key.rs currently says "with any validation" which is misleading;
update the remediation string to read "without immediate validation (NOT VALID)"
so it instructs creating the foreign key without immediate validation. Locate
the remediation message in the add_foreign_key.rs code (the string containing
"with any validation") and replace it with the corrected wording, ensuring the
phrase "NOT VALID" is included exactly as shown.

@valkrypton valkrypton force-pushed the add-foreign-key-check branch from 06d737a to 9bee12a Compare March 16, 2026 18:26
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/checks/add_foreign_key.rs (1)

47-51: Inconsistent placeholder naming in remediation template.

Line 47 uses {table} while line 51 uses {table_name}. Both resolve correctly, but using the same placeholder consistently would improve readability.

Suggested fix
 1. Create a foreign key without any immediate validation:
    ALTER TABLE {table} ADD CONSTRAINT {constraint_name}
     FOREIGN KEY ({columns}) REFERENCES {ref_table} ({ref_cols}) NOT VALID;

 2. Step 2 (separate migration, acquires ShareUpdateExclusiveLock only)
-  ALTER TABLE {table_name} VALIDATE CONSTRAINT {constraint_name};
+  ALTER TABLE {table} VALIDATE CONSTRAINT {constraint_name};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/checks/add_foreign_key.rs` around lines 47 - 51, Remediation template in
src/checks/add_foreign_key.rs uses inconsistent placeholders ({table} vs
{table_name}); locate the template string (look for the remediation/SQL template
in the add_foreign_key check, e.g., a constant or function named like
remediation_template, REMEDIATION, or in AddForeignKey check code) and make the
placeholder names identical (choose either {table_name} or {table} and replace
the other occurrence to match), then ensure any code that interpolates values
uses the chosen placeholder name consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/checks/add_foreign_key.rs`:
- Around line 47-51: Remediation template in src/checks/add_foreign_key.rs uses
inconsistent placeholders ({table} vs {table_name}); locate the template string
(look for the remediation/SQL template in the add_foreign_key check, e.g., a
constant or function named like remediation_template, REMEDIATION, or in
AddForeignKey check code) and make the placeholder names identical (choose
either {table_name} or {table} and replace the other occurrence to match), then
ensure any code that interpolates values uses the chosen placeholder name
consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b14e61fc-cbc2-481f-9dbd-9d64409cdfc4

📥 Commits

Reviewing files that changed from the base of the PR and between 06d737a and 9bee12a.

📒 Files selected for processing (10)
  • docs/src/SUMMARY.md
  • docs/src/checks/add_foreign_key.md
  • src/checks/add_foreign_key.rs
  • src/checks/mod.rs
  • src/checks/pg_helpers.rs
  • src/checks/unnamed_constraint.rs
  • tests/fixtures/add_foreign_key_safe/up.sql
  • tests/fixtures/add_foreign_key_unsafe/up.sql
  • tests/fixtures/unnamed_constraint_safe/up.sql
  • tests/fixtures_test.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/checks/pg_helpers.rs
  • src/checks/mod.rs
  • docs/src/SUMMARY.md
  • src/checks/unnamed_constraint.rs
  • tests/fixtures/unnamed_constraint_safe/up.sql
  • tests/fixtures/add_foreign_key_unsafe/up.sql

@codecov
Copy link

codecov bot commented Mar 17, 2026

Codecov Report

❌ Patch coverage is 94.28571% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.38%. Comparing base (71ce83e) to head (9eb4ed5).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
src/checks/pg_helpers.rs 83.33% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #132      +/-   ##
==========================================
+ Coverage   94.18%   94.38%   +0.19%     
==========================================
  Files          39       40       +1     
  Lines        1221     1264      +43     
==========================================
+ Hits         1150     1193      +43     
  Misses         71       71              
Files with missing lines Coverage Δ
src/checks/add_foreign_key.rs 100.00% <100.00%> (ø)
src/checks/mod.rs 94.44% <100.00%> (-0.68%) ⬇️
src/checks/unnamed_constraint.rs 96.15% <100.00%> (+8.34%) ⬆️
src/checks/pg_helpers.rs 90.62% <83.33%> (-1.05%) ⬇️

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ayarotsky ayarotsky added the new check New SQL check label Mar 17, 2026
Copy link
Owner

@ayarotsky ayarotsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job! A few minor comments and we're good to merge.


## Description

The `add_foreign_key` check flags foreign keys added without the `NOT VALID` option. Adding a foreign key with validation requires a `ShareRowExclusiveLock`, which blocks writes on the table. On large tables, this can cause outages.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow the format of other checks' docs. Example: https://ayarotsky.github.io/diesel-guard/checks/adding-unique-constraint.html.

- [Framework Adapters](frameworks.md)
- [Checks](checks/overview.md)
- [ADD COLUMN with DEFAULT](checks/add-column-default.md)
- [Add Foreign Key](checks/add_foreign_key.md)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also need to mention this check in the table in README.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/src/checks/add_foreign_key.md`:
- Line 19: Fix the typo in the user-facing docs by replacing the misspelled word
"foregin" with "foreign" in the sentence "Add the foregin key first without
validation..." so it reads "Add the foreign key first without validation using
the `NOT VALID` clause. Validate the foreign key later in a separate". Locate
the occurrence of "foregin" and correct it to "foreign".
- Around line 23-24: Update the Step 1 description for the ALTER TABLE orders
ADD CONSTRAINT fk_user_id guidance to remove the inaccurate “no lock” phrasing
and replace it with precise wording such as “no validation scan; short metadata
lock” (or equivalent), so the guidance reflects the actual lock behavior (short
metadata lock / ShareRowExclusiveLock implication) rather than claiming no lock.

In `@README.md`:
- Line 96: The README line describing the lock for "ADD FOREIGN KEY without NOT
VALID" should use the precise PostgreSQL lock name; replace the current phrase
"SHARE lock" with "ShareRowExclusiveLock" (or state "ShareRowExclusiveLock
(blocks writes)") to match terminology used elsewhere and avoid confusion;
update the table cell text where "ADD FOREIGN KEY without NOT VALID | Blocks
writes (SHARE lock)" appears so it consistently references
ShareRowExclusiveLock.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: de08f930-684c-46ba-8ace-173cd32c5808

📥 Commits

Reviewing files that changed from the base of the PR and between 9bee12a and c27b443.

📒 Files selected for processing (2)
  • README.md
  • docs/src/checks/add_foreign_key.md


### Good

Add the foregin key first without validation using the `NOT VALID` clause. Validate the foreign key later in a separate
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Correct typo in user-facing docs.

Line 19 has foreginforeign.

Proposed typo fix
-Add the foregin key first without validation using the `NOT VALID` clause. Validate the foreign key later in a separate
+Add the foreign key first without validation using the `NOT VALID` clause. Validate the foreign key later in a separate
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Add the foregin key first without validation using the `NOT VALID` clause. Validate the foreign key later in a separate
Add the foreign key first without validation using the `NOT VALID` clause. Validate the foreign key later in a separate
🧰 Tools
🪛 LanguageTool

[grammar] ~19-~19: Ensure spelling is correct
Context: ...ENCES users(id); ``` ### Good Add the foregin key first without validation using the ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/src/checks/add_foreign_key.md` at line 19, Fix the typo in the
user-facing docs by replacing the misspelled word "foregin" with "foreign" in
the sentence "Add the foregin key first without validation..." so it reads "Add
the foreign key first without validation using the `NOT VALID` clause. Validate
the foreign key later in a separate". Locate the occurrence of "foregin" and
correct it to "foreign".

README.md Outdated
| Check | Risk |
|-----------------------------------|---------------------------------------------------|
| ADD COLUMN with DEFAULT | Table rewrite on Postgres < 11 (ACCESS EXCLUSIVE) |
| ADD FOREIGN KEY without NOT VALID | Blocks writes (SHARE lock) |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use the precise PostgreSQL lock name for this row.

Line 96 says SHARE lock, but this check/documentation elsewhere refers to ShareRowExclusiveLock. Please align wording to avoid misleading lock semantics.

Proposed wording tweak
-| ADD FOREIGN KEY without NOT VALID | Blocks writes (SHARE lock)                        |
+| ADD FOREIGN KEY without NOT VALID | Blocks writes (ShareRowExclusiveLock)             |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| ADD FOREIGN KEY without NOT VALID | Blocks writes (SHARE lock) |
| ADD FOREIGN KEY without NOT VALID | Blocks writes (ShareRowExclusiveLock) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 96, The README line describing the lock for "ADD FOREIGN
KEY without NOT VALID" should use the precise PostgreSQL lock name; replace the
current phrase "SHARE lock" with "ShareRowExclusiveLock" (or state
"ShareRowExclusiveLock (blocks writes)") to match terminology used elsewhere and
avoid confusion; update the table cell text where "ADD FOREIGN KEY without NOT
VALID | Blocks writes (SHARE lock)" appears so it consistently references
ShareRowExclusiveLock.

@valkrypton valkrypton force-pushed the add-foreign-key-check branch from c27b443 to 9eb4ed5 Compare March 18, 2026 09:11
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
docs/src/checks/add_foreign_key.md (1)

17-17: Use consistent heading levels for sibling sections.

Bad and Good are peer sections, so using the same heading level improves readability and doc structure consistency.

Proposed doc tweak
-### Good
+## Good
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/src/checks/add_foreign_key.md` at line 17, The "Good" heading is using a
different level ("### Good") than its sibling "Bad"; update the "Good" heading
to match the sibling (use the same heading level as "Bad", e.g., change "###
Good" to "## Good") so both sections are consistent in
docs/src/checks/add_foreign_key.md.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/src/checks/add_foreign_key.md`:
- Line 17: The "Good" heading is using a different level ("### Good") than its
sibling "Bad"; update the "Good" heading to match the sibling (use the same
heading level as "Bad", e.g., change "### Good" to "## Good") so both sections
are consistent in docs/src/checks/add_foreign_key.md.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be3b0364-4a92-4e31-9619-01ee404c39a0

📥 Commits

Reviewing files that changed from the base of the PR and between c27b443 and 9eb4ed5.

📒 Files selected for processing (2)
  • README.md
  • docs/src/checks/add_foreign_key.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md

Copy link
Owner

@ayarotsky ayarotsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 🎉 🎉

@ayarotsky ayarotsky merged commit bf7a52a into ayarotsky:main Mar 18, 2026
5 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Mar 18, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new check New SQL check

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADD FOREIGN KEY without NOT VALID

2 participants