masitings / laravel-migration-squash
Combine multiple Laravel migration files into consolidated migrations per table with schema verification
Package info
github.com/masitings/laravel-migration-squash
pkg:composer/masitings/laravel-migration-squash
Requires
- php: ^8.1
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- nikic/php-parser: ^5.0
Requires (Dev)
- laravel/pint: ^1.13
- orchestra/testbench: ^8.0|^9.0|^10.0
- pestphp/pest: ^2.0|^3.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0
- phpunit/phpunit: ^10.0|^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-13 09:11:01 UTC
README
Combine multiple Laravel migration files into a single consolidated migration per table, with automated schema verification to ensure nothing is lost.
⚠️ v1.0.0 and v1.0.1 are non-functional and should not be used. Both fatal-error before touching the database. Use v1.1.0 or later.
How It Works
- Scan — discovers all migration files in
database/migrations/ - Guard — detects migrations with raw SQL or data seeding and excludes them
- Sandbox — runs your migrations in an isolated sandbox on the same engine your app uses (never touches your real database)
- Introspect — reads the resulting schema (tables, columns, indexes, foreign keys)
- Generate — creates one clean migration per table, plus a separate FK migration
- Verify — runs the generated migrations and compares the schema to ensure they match
- Archive — moves original migrations to an archive directory with a manifest for rollback
Requirements
- PHP 8.1+
- Laravel 10, 11, 12, or 13
Installation
composer require masitings/laravel-migration-squash
The service provider is auto-discovered. To publish the config:
php artisan vendor:publish --tag=migrationsquash-config
Usage
Dry Run (recommended first)
php artisan migrate:squash --dry-run
Generates and verifies squashed migrations without modifying any files.
Full Squash
php artisan migrate:squash
Generates squashed migrations, verifies them, and archives the originals.
Check Mode
php artisan migrate:squash --check
Only checks for migrations that contain raw SQL or data seeding — does not squash.
Undo a Squash
php artisan migrate:squash:restore
Puts the archived migrations back using the manifest written during the squash, and removes the generated files. Every archived file is checked against its recorded hash first: if one has been altered, nothing is restored.
php artisan migrate:squash:restore --list # show available archives php artisan migrate:squash:restore --archive=PATH # restore a specific one php artisan migrate:squash:restore --keep-squashed # restore without removing generated files
Options
migrate:squash
| Option | Description |
|---|---|
--dry-run |
Generate and verify without archiving |
--check |
Only check for guarded migrations |
--driver=sqlite |
Force sandbox driver (sqlite, mysql, pgsql, or sqlsrv) |
--table=users |
Squash only specific tables (repeatable) |
--table matches on the migration filename, not on the tables a migration
actually touches. If the resulting set holds a foreign key onto a table left
outside it, the squash is refused rather than generating a migration that
references something the set never creates.
migrate:squash:restore
| Option | Description |
|---|---|
--list |
List available archives and exit |
--archive=PATH |
Restore a specific archive instead of the newest |
--force |
Overwrite files already present in the migration folder |
--keep-squashed |
Keep the generated migrations instead of removing them |
Safety Features
Sandbox Isolation
All migration execution happens in an isolated sandbox connection (squash-sandbox-*). A runtime guard verifies the connection name prefix before any write operation. Your real database is never touched.
The Sandbox Matches Your Database Engine
The sandbox runs the same engine your application runs on. This is not a performance choice, it is a correctness one.
SQLite has no unsigned bigint. Squash a MySQL app through a SQLite sandbox and
$table->id() comes back as $table->increments('id'), turning every primary
key from bigint unsigned into int, and every foreign key onto it fails with
errno 150. Verification would still report success, because it compared
SQLite against SQLite.
So the driver is chosen from database.default:
| Your app runs on | Sandbox |
|---|---|
mysql / mariadb |
MySQL (temporary laravel_squash_* database) |
pgsql |
PostgreSQL (temporary laravel_squash_* database) |
sqlsrv |
SQL Server (temporary laravel_squash_* database) |
sqlite |
SQLite in-memory |
| anything else | refused, rather than verified against the wrong engine |
--driver overrides this. If the override does not match your application
driver, the command says so and explains what the verification will and will
not prove.
Schema Verification
After generating squashed migrations, the package runs them in a fresh sandbox and compares the resulting schema against the original. If any differences are found, the squash is aborted.
Guard System
Migrations containing DB::statement(), DB::unprepared(), DB::table()->insert(), ->update(), or ->delete() are automatically detected and excluded from the squash. They are reported but do not cause the entire process to fail.
DB::raw() and DB::select() are deliberately not flagged: DB::raw() is
routine inside legitimate schema migrations, and excluding those migrations
would quietly shrink the schema being verified.
Each guard can be switched off, or downgraded to a warning, in the config:
| Key | Effect |
|---|---|
guards.block_raw_sql |
false stops raw SQL from excluding a migration |
guards.block_data_seeding |
false stops data seeding from excluding a migration |
guards.warn_on_detection |
true reports findings but squashes them anyway |
--check always reports what it finds, whatever these flags say.
What Verification Compares
Per column: name, type, nullability, default, length, unsigned, collation,
comment and enum allowed values. Per table: indexes (matched by type and
column set, not by generated name) and foreign keys (matched by column and
target, with onUpdate / onDelete compared).
Every one of these has a test that builds two schemas differing in exactly that attribute and asserts the difference is reported, so "verification passed" means something.
Not compared: column order (columns are matched by name), check constraints
other than the one Laravel uses for enum, generated and virtual columns,
table engine and charset, and partial indexes.
One limitation worth knowing: on SQLite, varchar length is not verifiable.
Laravel's SQLite grammar writes "name" varchar not null with no length for
any string size, so nothing downstream can recover it. This does not affect a
MySQL application, which is introspected on MySQL where length is compared.
Archive Manifest
Before archiving, an archive-manifest.json is written containing the original file list, hashes, and timestamps. migrate:squash:restore reads it to undo a squash, and refuses to restore a file whose hash no longer matches.
Foreign Key Handling
Foreign keys are generated in a separate migration file (*_add_foreign_keys.php) that runs after all table creation migrations. This eliminates circular dependency issues without needing topological sort.
Configuration
// config/migrationsquash.php return [ 'sandbox' => [ // null follows your application's driver, which is what keeps // verification meaningful. Only override deliberately. 'driver' => env('MIGRATION_SQUASH_DRIVER'), ], 'guards' => [ 'block_raw_sql' => true, 'block_data_seeding' => true, 'warn_on_detection' => false, ], 'archiving' => [ // null archives next to your migration folder. A path inside // database/migrations is ignored, since Laravel would pick the // archived files back up as live migrations. 'archive_directory' => null, ], 'verification' => [ 'strict_mode' => true, 'ignore_differences' => [], ], ];
Supported Databases
| Driver | Status |
|---|---|
| SQLite | ✅ Full support, covered by the test suite (default sandbox) |
| MySQL | ✅ Supported, covered end-to-end by the test suite |
| PostgreSQL | ✅ Supported, covered by the test suite |
| SQL Server | ✅ Supported, covered by the test suite |
The temporary laravel_squash_* sandbox database requires specific user privileges per driver:
- MySQL:
CREATE DATABASEprivileges. - PostgreSQL:
CREATEDBprivileges. - SQL Server:
CREATE ANY DATABASEordbcreatorrole.
Testing
composer install
composer test
The MySQL, PostgreSQL, and SQL Server tests skip themselves unless a matching database server is reachable, so the suite is green on a machine that only has SQLite. Point them at a server to run them:
DB_HOST=127.0.0.1 DB_USERNAME=root DB_PASSWORD= composer test PGSQL_HOST=127.0.0.1 PGSQL_USERNAME=postgres PGSQL_PASSWORD= composer test SQLSRV_HOST=127.0.0.1 SQLSRV_USERNAME=sa SQLSRV_PASSWORD= composer test
Coverage needs pcov or xdebug installed:
composer test:coverage # fails below 70%
Code style is enforced with Pint:
composer lint # check composer lint:fix # fix
CI runs the suite across PHP 8.1 to 8.4 and Laravel 10 to 12, with a MySQL 8.0 service so the MySQL path is exercised on every push.
Changelog
See CHANGELOG.md for release history.
License
MIT License. See LICENSE for details.