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
Requires
- php: ^8.2
- illuminate/console: ^11.0|^12.0
- illuminate/database: ^11.0|^12.0
- illuminate/filesystem: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
- nikic/php-parser: ^5.0
Requires (Dev)
- larastan/larastan: ^2.0|^3.0
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^11.0
README
Keep your Laravel database in perfect sync with your migrations
๐ 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/promptsfor 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 DATABASEpermission 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:
- State classification โ classifies every migration into one of 6 states by cross-referencing files, DB records, and actual schema
- Schema drift โ creates a temp database, runs all migrations, diffs the resulting schema against your actual database
- 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 DATABASEpermission 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
- Classify โ Every migration is classified into one of 6 states (see table above)
- Fix bookkeeping โ In a single transaction: delete bogus/orphan records, insert lost records
- Schema repair โ Compare actual schema against what migrations produce, generate corrective migrations for any remaining drift
- 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()anddown()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 DATABASEpermission (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
- roslov/laravel-migration-checker โ inspiration for this package
๐ License
MIT License โ see LICENSE file
Made with ๐๐ for the Laravel community