dragon-code/migrate-db
Laravel package for migrating data between different database systems. Move tables and records from MySQL, PostgreSQL, SQLite, SQL Server and more using configurable mappings, artisan commands and safe, resumable transfers.
composer require dragon-code/migrate-db
php artisan vendor:publish --provider="DragonCode\MigrateDb\ServiceProvider"
config/migrate-db.php. It defines the source and target database connections (names from config/database.php, not credentials). Point them at the two connections you want to move data between — often legacy → mysql, or the same connection twice when you are only reshaping tables.database/migrate-db/, configurable in the published config). It is a normal timestamped class extending DragonCode\MigrateDb\Migration, with up() / down() just like a Laravel migration.php artisan migrate:db. Use php artisan migrate:db --help on day one to see the real flags (--path, --database, rollback, etc.) your installed version exposes — the command is the authoritative reference.Where to look first
$table, up(), down()) better than the source does.vendor/dragon-code/migrate-db/config/migrate-db.php — the pristine config, useful when you have already edited yours.Migration class itself — it is thin, so reading it tells you exactly which helpers exist for copying, mapping columns, and chunking.First real use case: a legacy application's database needs to become a fresh Laravel schema. You run php artisan migrate to build the empty target schema, then write one transfer class per table (or per logical group) and run migrate:db. This is also the pattern for a schema refactor — e.g. splitting users.name into first_name / last_name — where you cannot express the change as pure DDL.
One migration per table, ordered by dependency. Parents before children. The package runs files in timestamp order like Laravel, so the file names are your execution plan. Do not put an entire database in one class; a failure halfway through is much easier to reason about when each unit is one table.
Raw copy vs. transformed copy. For a straight lift-and-shift, the migration body is essentially "select from source table, insert into target table" with an explicit column list (never SELECT * — column order and extras differ). For transformations, do the mapping in PHP inside up() and insert row by row or in chunks. Keep the transformation logic in a dedicated service/action class and let the migration call it — migrations are hard to unit test, services are not.
ID preservation vs. remapping. Decide this before writing anything:
AUTO_INCREMENT / sequences on the target afterwards.old_id => new_id lookup map per table and rewrite dependent foreign keys through it. Do this in dependency order and persist the map (a temp table or a JSON file) so a crashed run can resume.Chunk large tables. ->chunk(1000) / ->cursor() inside up() keeps memory flat and lets you log progress. Never get() a multi-million-row table into a collection; the package will happily try and your worker will die.
Wrap the target in a known state. In each migration: disable foreign key checks, optionally truncate the target, transfer, then re-enable. Prefer truncate over delete for reset semantics, and make the migration re-runnable on staging so you can iterate without a fresh database each time.
Separate connections per phase. If your source and target are different servers, make sure both connections are defined in config/database.php and referenced by name in config/migrate-db.php. For multi-tenant or per-environment cutovers, override the config at runtime (config(['migrate-db.source' => ...])) in a custom artisan command that wraps migrate:db — this is the main legitimate extension point.
down() is a no-op or a truncate. Data migrations are effectively irreversible. Write down() as "truncate the target table" so a rollback leaves a clean slate, and treat the real safety net as a database snapshot taken before the run.
Schedule it as a one-shot, not a sync. This is a cutover tool, not replication. If you find yourself running it nightly, you want a proper ETL or logical replication instead.
composer.json, and budget for forking or vendoring it if a future PHP/Laravel release breaks it. With zero dependents and no maintenance, you are the maintainer of last resort. Read the source before relying on any behaviour you cannot reproduce in a test.config:cache will bite you. After changing source/target connections, run php artisan config:clear. A cached config pointing at the old connection is the single most common "it transferred nothing" cause.0000-00-00, booleans becoming 1/'1', JSON strings becoming escaped strings, SQLite dropping column types. Always finish a run with a validation pass: row counts per table and a checksum or spot-check of a handful of rows on both sides.AUTO_INCREMENT (MySQL) or the sequence (Postgres) past MAX(id), or the next insert collides.--path against a single file on a copy of the data before running the full set. Add a Log::info() of "table X: read N, wrote M" at the end of each up() — a read/write mismatch is the fastest signal that filtering or a constraint silently dropped rows.Migration, and wrapping the artisan command. Don't try to hook into internals; if you need more, write your own chunked transfer with Laravel's query builder and use this package only as the runner.How can I help you explore Laravel packages today?