Skip to content

feat: destructive change linter with --allow-destructive flag#174

Open
jasdeepkhalsa wants to merge 2 commits into
masterfrom
feature/destructive-change-linter
Open

feat: destructive change linter with --allow-destructive flag#174
jasdeepkhalsa wants to merge 2 commits into
masterfrom
feature/destructive-change-linter

Conversation

@jasdeepkhalsa

Copy link
Copy Markdown
Member

Summary

Adds a safety linting pass between DiffCalculator and SQLGenerator that catches destructive schema changes before any SQL is written to disk. This is the missing feature that tools like Atlas and pg-delta advertise as "safety-first migration generation" — and the #1 developer complaint about database diff tools on StackOverflow/Reddit.

What it does

  • Blocks DROP TABLE and DROP COLUMN by throwing DestructiveChangeException (exit non-zero) unless --allow-destructive is passed
  • Warns on DROP ENUM, DROP ROUTINE, DROP TRIGGER, DROP VIEW (generation still proceeds but stderr shows warnings)
  • Detects probable column renames: when a DROP COLUMN + ADD COLUMN on the same table share the same data type, it flags as a possible-rename warning instead of a hard error

New files

File Purpose
src/Linter/LintViolation.php Value object: level, type, object, sql, suggestion
src/Linter/LintResult.php Aggregate of violations; hasErrors(), hasWarnings(), getErrors(), getWarnings()
src/Linter/DestructiveLinter.php Core logic — inspects $diff['schema'], indexes by table for rename detection
src/Exceptions/DestructiveChangeException.php Extends BaseException, formats a human-readable block listing all violations
tests/Unit/Linter/DestructiveLinterTest.php 14 PHPUnit tests covering every violation type, clean diffs, and rename heuristic edge cases

Modified files

File Change
src/DBDiff.php Calls DestructiveLinter::lint() after getDiff(), before SQLGenerator. Returns lint key in result array.
src/Params/DefaultParams.php Adds public bool $allowDestructive = false
src/Params/CLIGetter.php Registers 'allow-destructive::' in getopt, sets $params->allowDestructive

CLI usage

# Default — throws on DROP TABLE / DROP COLUMN:
dbdiff db1:db2

# Override the safety gate:
dbdiff db1:db2 --allow-destructive

Why this matters competitively

  • Atlas charges for its lint/destructive-change detection in CI. DBDiff now offers this free.
  • pg-delta (Supabase's alpha tool) describes itself as "safety-first" but only for PG. DBDiff's linter works on MySQL, PostgreSQL, and SQLite.
  • The top StackOverflow/Reddit complaint: "diff tool ran DROP TABLE in production by accident." This PR closes that gap.

Test plan

  • DropTableerror level, drop-table type
  • AlterTableDropColumnerror level, drop-column type
  • DROP + ADD same table, same type → warning possible-rename
  • DROP + ADD same table, different type → still error
  • DROP + ADD different tables → still error
  • DropEnumwarning
  • DropRoutinewarning
  • DropTriggerwarning
  • DropViewwarning
  • AddTable → no violations
  • Empty diff → no violations
  • Mixed diff → all violations reported independently
  • LintResult::getErrors() / getWarnings() filter correctly
  • Array-format diff metadata works (not just object format)

Generated by Claude Code

Adds a lint pass between DiffCalculator and SQLGenerator that detects
DROP TABLE, DROP COLUMN, DROP ENUM, DROP ROUTINE, DROP TRIGGER, and
DROP VIEW operations. By default an error is thrown so that accidental
data-loss migrations are blocked in CI. Pass --allow-destructive on
the CLI to override.

Also detects probable column renames (DROP + ADD on same table/type)
and emits a warning rather than a hard error for those cases.
@github-actions github-actions Bot added bug enhancement mysql Related to Mysql php Pull requests that update php code postgres Related to Postgres sqlite Related to Sqlite labels Jun 19, 2026
The new DestructiveLinter throws DestructiveChangeException by default
when DROP TABLE / DROP COLUMN is detected. The existing integration
tests exercise fixtures that intentionally include such operations,
causing 27/27 CI jobs to fail.

- DiffCommand: register --allow-destructive option and populate
  $params->allowDestructive so the CLI flag actually takes effect
- binary-check.yml: pass --allow-destructive in all integration diff
  steps (MySQL, Postgres, npm wrapper) so the smoke test fixtures
  (which include DROP TABLE zz and DROP COLUMN aa.zx) can proceed
- AbstractComprehensiveTest: append --allow-destructive to every
  runDBDiff() call so existing diff-output tests bypass the linter;
  linter behaviour is covered by DestructiveLinterTest unit tests

Co-Authored-By: Claude <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug enhancement mysql Related to Mysql php Pull requests that update php code postgres Related to Postgres sqlite Related to Sqlite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants