erimeilis/laravel-migrations-drift

Detect and fix migration table drift in Laravel apps. Sync filenames, compare schemas, rename migrations.

Installs: 4

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/erimeilis/laravel-migrations-drift

v0.3.3 2026-02-27 06:37 UTC

This package is auto-updated.

Last update: 2026-02-27 06:39:42 UTC


README

Keep your Laravel database in perfect sync with your migrations

Latest Version on Packagist Total Downloads Tests PHP Version Laravel 11+ License: MIT

๐Ÿ” Detect every kind of schema drift โ€” filenames, columns, indexes, foreign keys ๐Ÿ”ง Fix it all โ€” schema-aware record sync, corrective migrations, consolidation ๐Ÿ›ก๏ธ Safe by default โ€” dry-run mode, automatic backups, transactional operations ๐ŸŽฏ Interactive prompts โ€” powered by laravel/prompts for a beautiful CLI experience

โœจ Features

๐Ÿ” Schema-Aware Detection

  • โœ… 6-State Classification โ€” Every migration is classified as OK, Bogus Record, Missing File, Orphan Record, Lost Record, or New Migration
  • ๐Ÿ—„๏ธ Schema Verification โ€” Checks actual DB schema to determine if migrations truly ran, not just if records exist
  • ๐Ÿ”ฌ Code Quality โ€” AST-parses migrations to detect missing down() methods, empty migrations, and more
  • ๐Ÿ“Š JSON Output โ€” Machine-readable output for CI pipelines with exit code 0/1

๐Ÿ”ง Unified Fix Command

  • ๐Ÿง  Schema-Aware Sync โ€” Cross-references files, DB records, and actual schema to make correct decisions
  • ๐Ÿšง Schema Repair โ€” Generates corrective migration files for missing tables, columns, indexes, and FKs
  • ๐Ÿ”— Consolidation โ€” Merges redundant per-table migration chains into single clean migrations via AST replay

๐Ÿ›ก๏ธ Safety First

  • ๐Ÿ”’ Dry-Run Default โ€” Every command shows what would change before doing anything
  • ๐Ÿ’พ Automatic Backups โ€” JSON snapshots before any destructive operation
  • โ™ป๏ธ One-Command Restore โ€” Roll back to last backup instantly
  • ๐Ÿ”„ Transactional Operations โ€” DB changes wrapped in transactions with rollback on failure
  • ๐Ÿ“ Atomic File Operations โ€” Archive-based consolidation with full rollback on error

๐ŸŽฏ Developer Experience

  • ๐ŸŽจ Interactive Prompts โ€” Beautiful multi-select, confirmations, and spinners via laravel/prompts
  • ๐Ÿ”Œ Multi-Connection โ€” Works with any database connection, not just the default
  • โšก CI-Ready โ€” Non-interactive mode with JSON output for automated pipelines
  • ๐Ÿงช 230 Tests โ€” Comprehensive test suite with PHPStan level 6 static analysis

๐Ÿ“ฆ Installation

composer require erimeilis/laravel-migrations-drift --dev

The service provider is auto-discovered. To publish the config:

php artisan vendor:publish --tag=migration-drift-config

Requirements:

  • PHP 8.2 or higher
  • Laravel 11.x or 12.x
  • CREATE DATABASE permission for schema comparison (optional โ€” gracefully skipped if unavailable)

๐Ÿš€ Quick Start

# 1. Detect all drift in one pass
php artisan migrations:detect

# 2. Preview what fix would do (dry-run, safe)
php artisan migrations:fix

# 3. Apply fixes (creates backup first)
php artisan migrations:fix --force

# 4. Run any genuinely new migrations
php artisan migrate --force

# 5. Consolidate redundant migration chains
php artisan migrations:fix --consolidate --force

๐Ÿ“š Commands

๐Ÿ” migrations:detect

Performs a comprehensive three-layer analysis in one pass:

  1. State classification โ€” classifies every migration into one of 6 states by cross-referencing files, DB records, and actual schema
  2. Schema drift โ€” creates a temp database, runs all migrations, diffs the resulting schema against your actual database
  3. Code quality โ€” parses migration ASTs to detect structural issues
php artisan migrations:detect
php artisan migrations:detect --json              # machine-readable output
php artisan migrations:detect --connection=mysql2  # specific connection
Flag Description
--json Output results as JSON
--connection= Database connection to use
--path= Override migrations directory

Exit codes: 0 = no drift, 1 = drift detected.

Example output:

3 migration(s) OK.
1 new migration(s) pending (will run with `php artisan migrate`).

Bogus records (registered but never ran):
  2026_01_15_000001_create_widgets_table

Lost records (ran but not registered):
  2026_01_01_000003_add_bio_to_users_table

Schema comparison: no differences found.

DRIFT DETECTED

Migration States

State Meaning Action taken by fix --force
OK Record + file + schema all match None
Bogus Record Record + file exist, but schema says it never ran Delete record
Missing File Record + schema exist, but file is gone Warn (file can't be auto-regenerated)
Orphan Record Record exists, no file, no schema evidence Delete record
Lost Record File + schema exist, but no DB record Insert record
New Migration File exists, no record, not in schema yet Left alone โ€” php artisan migrate will run it

Note: Schema comparison requires CREATE DATABASE permission to create and drop a temporary database. If unavailable, it degrades gracefully with a warning.

๐Ÿ”ง migrations:fix

The unified repair command. Analyzes all migrations against the actual database schema, then fixes bookkeeping and generates corrective migrations.

php artisan migrations:fix                  # dry-run: shows classified states
php artisan migrations:fix --force          # apply fixes (creates backup first)
php artisan migrations:fix --consolidate    # dry-run: shows consolidation candidates
php artisan migrations:fix --restore        # restore from latest backup

How It Works

  1. Classify โ€” Every migration is classified into one of 6 states (see table above)
  2. Fix bookkeeping โ€” In a single transaction: delete bogus/orphan records, insert lost records
  3. Schema repair โ€” Compare actual schema against what migrations produce, generate corrective migrations for any remaining drift
  4. New migrations are never touched โ€” they're left for php artisan migrate

Dry-run output:

3 migration(s) OK.
1 new migration(s) pending (will run with `php artisan migrate`).

Bogus record (registered but never ran):
  2026_01_15_000001_create_widgets_table

Lost record (ran but not registered):
  2026_01_01_000003_add_bio_to_users_table

DRY RUN โ€” use --force to apply changes.

After --force:

Backup created: storage/migrations-drift/backup-2026-02-27-143022.json

Bookkeeping fixed:
  Removed 1 bogus record(s).
  Inserted 1 lost record(s).

Schema is in sync โ€” no corrective migrations needed.

Idempotent: Running again after fix outputs Everything in sync โ€” no fixes needed.

๐Ÿ”— Consolidation (--consolidate)

Parses migration ASTs with nikic/php-parser to find tables with multiple migrations that can be merged into a single clean migration.

php artisan migrations:fix --consolidate        # dry-run: shows candidates
php artisan migrations:fix --consolidate --force # consolidate selected tables

Handles column additions, drops, index changes, and foreign keys. Multi-table migrations are automatically skipped from consolidation to preserve safety.

โ™ป๏ธ Restore from Backup

php artisan migrations:fix --restore
Flag Description
--force Apply changes (default is dry-run)
--restore Restore migrations table from latest backup
--consolidate Consolidate redundant migrations per table
--connection= Database connection to use
--path= Override migrations directory

โœ๏ธ migrations:rename

Renames migration files to use a target date prefix with sequential numbering. Updates both files and the migrations table atomically โ€” DB records first (transactional), files second, with automatic rollback on failure.

php artisan migrations:rename                           # dry-run
php artisan migrations:rename --force --date=2026-02-25  # apply
Flag Description
--force Apply renames
--date=YYYY-MM-DD Target date prefix (default: today)
--connection= Database connection to use
--path= Override migrations directory

Example output:

 Current                                    | New
 0001_01_01_000000_create_users_table.php   | 2026_02_25_000001_create_users_table.php
 0001_01_01_000001_create_cache_table.php   | 2026_02_25_000002_create_cache_table.php

Would rename 2 files.
DRY RUN โ€” use --force to apply.

Files that already match the target pattern are skipped.

โš™๏ธ Configuration

php artisan vendor:publish --tag=migration-drift-config
// config/migration-drift.php

return [
    // Where to store migrations table backups
    'backup_path' => storage_path('migrations-drift'),

    // Maximum number of backup files to keep (oldest rotated out)
    'max_backups' => 5,

    // Path to migration files
    'migrations_path' => database_path('migrations'),

    // Database connection (null = default connection)
    'connection' => null,
];

๐Ÿšข Production Workflow

# 1. Deploy new code

# 2. Preview what fix will do (dry-run, safe)
php artisan migrations:fix

# 3. Review the output, then apply (creates backup first)
php artisan migrations:fix --force

# 4. Run any genuinely new migrations
php artisan migrate --force

# 5. Verify no drift remains
php artisan migrations:detect

If something goes wrong after step 3:

php artisan migrations:fix --restore

๐Ÿค– CI Integration

Add drift detection to your CI pipeline:

- name: Check migration drift
  run: php artisan migrations:detect --json

The command exits with code 1 when drift is detected, failing the pipeline. JSON output includes structured migration_states with per-migration classification, schema_drift, and quality_issues.

๐Ÿ›ก๏ธ Safety Matrix

Scenario Behavior
Any fix without --force Dry-run. Shows classified states and planned actions, changes nothing.
--force on fix Backs up migrations table, then fixes bookkeeping in a transaction + generates corrective migrations.
--force run again "Everything in sync" โ€” idempotent, no changes.
Fix broke something --restore loads the last backup.
detect Read-only. Main database is never modified.
Schema comparison Creates and drops a temp database. Main database is read-only.
rename --force Updates DB records first (transactional), then renames files. Rolls back DB on file failure.
Consolidation Archives original files atomically. Rolls back on any failure.
New migrations detected Left alone. php artisan migrate handles them normally.

Backups are stored as JSON in storage/migrations-drift/. The last 5 are kept by default.

๐Ÿ” How It Works

๐Ÿง  6-State Classification

The core innovation: every migration is classified by cross-referencing three data sources:

                    Has DB Record?
                   /              \
                 YES               NO
                /                    \
          Has File?              Has File?
         /        \             /        \
       YES         NO        YES         NO
        |           |          |          (impossible)
  Schema says   Schema has   Schema says
   applied?     evidence?     applied?
    /    \       /    \       /    \
  YES    NO    YES    NO    YES    NO
   |      |     |      |     |      |
  OK   BOGUS  MISS-  ORPHAN LOST   NEW
       RECORD  ING   RECORD RECORD MIGR.
               FILE

๐Ÿšง Architecture

migrations:detect / migrations:fix
       |
       v
 +------------------------+
 | MigrationStateAnalyzer |  <-- Cross-references all 3 sources
 +------------------------+
       |         |         |
       v         v         v
 +---------+ +--------+ +---------+
 | Diff    | | Parser | | Schema  |
 | Service | | (AST)  | | Intro-  |
 | (files  | |        | | spector |
 | vs DB)  | |        | | (actual |
 +---------+ +--------+ | schema) |
                         +---------+

migrations:fix --force
       |
       +-- fixBookkeeping --> DELETE/INSERT in transaction
       |
       +-- fixSchemaDrift --> SchemaComparator --> MigrationGenerator --> .php files
       |
       +-- --consolidate --> MigrationParser --> AST replay --> single migration

๐Ÿ”ง Key Components

  • MigrationStateAnalyzer โ€” The brain: classifies every migration into one of 6 states using files, DB records, and actual schema
  • SchemaComparator โ€” Creates a temporary database, runs all migrations on it, then diffs both schemas column-by-column
  • MigrationParser + MigrationVisitor โ€” Uses nikic/php-parser to extract structured column, index, and FK data from migration ASTs
  • SchemaIntrospector โ€” Queries actual database schema via INFORMATION_SCHEMA
  • TypeMapper โ€” Bidirectional mapping between SQL types and Laravel Blueprint methods
  • MigrationGenerator โ€” Generates properly formatted migration files with up() and down() from schema diff actions
  • ConsolidationService โ€” Replays migration operations in order to produce a single equivalent migration per table
  • BackupService โ€” JSON snapshots of the migrations table with automatic rotation

โš ๏ธ Limitations

  • Schema comparison requires CREATE DATABASE permission (gracefully skipped on SQLite or restricted permissions)
  • Consolidation skips multi-table migrations to preserve safety
  • Type normalization covers common MySQL/PostgreSQL/SQLite types โ€” exotic custom types may need manual review
  • Column modifiers (nullable, default) are detected in schema comparison but not fully replayed during consolidation
  • Partial analysis โ€” Migrations with raw SQL or conditional logic are flagged with warnings; schema checks cover only the parseable Blueprint parts

๐Ÿงช Testing

# Run the full test suite (230 tests, 497 assertions)
vendor/bin/phpunit

# Static analysis (PHPStan level 6 with Larastan)
vendor/bin/phpstan analyse

Tested across:

  • PHP 8.2, 8.3, 8.4, 8.5
  • Laravel 11.x, 12.x

๐Ÿ™ Acknowledgements

๐Ÿ“„ License

MIT License โ€” see LICENSE file

Made with ๐Ÿ’™๐Ÿ’› for the Laravel community