kerroldj / laravel-migrate-fresh-table
Fresh-migrate a specific table (or a chosen set of tables) in Laravel โ like migrate:fresh, but scoped, foreign-key aware, multi-connection and multitenant-friendly.
Package info
github.com/KerroldJ/laravel-migrate-fresh-table
pkg:composer/kerroldj/laravel-migrate-fresh-table
Requires
- php: ^8.2
- illuminate/console: ^12.0|^13.0
- illuminate/container: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/filesystem: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.5
- pestphp/pest-plugin-laravel: ^3.0
This package is auto-updated.
Last update: 2026-06-16 19:54:03 UTC
README
migrate:fresh-table, but scoped to one table (or a chosen, ordered set) instead of wiping the whole database.
Sometimes you need to rebuild a single table from scratch โ reset a corrupted pivot, reapply a changed schema during development, or re-provision one table per tenant โ without nuking everything else. This package does exactly that, safely:
- ๐ฏ Scoped โ drop & recreate one table or an ordered list, not the whole DB.
- ๐ Foreign-key aware โ inspects the live schema, reports every parent and child relationship with the rows that would be orphaned, and asks first.
- ๐ Cascade or data-only โ
--with-relatedfreshes a table plus its whole dependent tree;--data-onlyempties the rows and keeps the schema. - ๐งฉ Pluggable โ choose how a table is rebuilt: re-run its migration, or
rebuild from an explicit
Blueprint. Register your own strategy too. - ๐ข Multi-connection / multitenant โ target one connection, iterate a list, or
resolve tenant connections dynamically. PostgreSQL
search_pathaware. - ๐งช Safe by default โ confirms before running in protected environments
(configurable), plus
--dry-run,--pretend, and a transaction so a failed run rolls back instead of half-dropping. - ๐ช Hookable โ lifecycle events and global before/after callbacks.
Works across MySQL/MariaDB, PostgreSQL, SQLite and SQL Server on Laravel 12+ and PHP 8.2+.
Example โ a
--with-related --data-only --dry-runpreview showing the foreign-key impact report and the resolved plan before anything is touched:
Installation
composer require kerroldj/laravel-migrate-fresh-table
The service provider is auto-discovered. Publish the config file:
php artisan vendor:publish --tag=migrate-fresh-table-config
This creates config/migrate-fresh-table.php.
Quick start
# Fresh a single table (auto-detects the migration that creates it) php artisan migrate:fresh-table users # Fresh several tables in a safe, dependency-aware order (parents first) php artisan migrate:fresh-table --tables=users,posts,comments # Fresh a table AND every table that references it (the full dependent tree) php artisan migrate:fresh-table users --with-related # Just empty the data โ keep the schema, drop nothing php artisan migrate:fresh-table users --with-related --data-only # Re-seed afterwards php artisan migrate:fresh-table posts --seed --seeder="Database\Seeders\PostSeeder" # See exactly what would happen, change nothing php artisan migrate:fresh-table users --dry-run # Print the SQL that would run php artisan migrate:fresh-table users --pretend
How it works
For each target table the command:
- Inspects the live schema on the resolved connection to find every foreign key that touches the table โ both the parents it references and the children that reference it.
- Prints a foreign-key impact report (always).
- Asks what to do (unless
--force,--dry-run, or--pretend) โ when dependents exist you can fresh only the target, or cascade to its dependents. - Drops the target table(s) โ children first โ detaching any foreign keys that block the drop, then recreates them โ parents first โ using the selected strategy, and restores constraints. On PostgreSQL and SQL Server the whole drop/recreate runs in a transaction, so a failure rolls everything back instead of leaving the database half-dropped.
- Optionally re-seeds.
Drop/recreate ordering is dependency-safe: tables are dropped children-first and
recreated parents-first. (With --tables=a,b,c you supply the order yourself โ
a is a parent of b is a parent of c.)
The foreign-key impact report & prompt
- The table lists every dependent that references the target, AโZ, with the number of rows that actually point at it (FK not null) โ the rows a fresh would orphan. Parent tables the target references are shown separately and are not modified.
- When dependents exist you get the three-way choice above; picking [1] is
the same as
--with-related. With no dependents it's a simple yes/no. - In a protected environment (default
production) you are first asked โDo you really wish to run this command?โ; other environments skip straight to the foreign-key prompt. See Safety notes. --forceskips every prompt (and is required to run non-interactively in a protected environment).--dry-runprints the report and plan, then stops.--pretendprints the SQL without running it. The report is recomputed per connection / tenant.
Freshing dependent tables (--with-related)
By default only the table(s) you name are freshed; tables that reference them
keep their rows, which then point at records that no longer exist. Pass
--with-related to also fresh every table that (transitively) references the
target, in dependency-safe order:
php artisan migrate:fresh-table users --with-related
# Non-interactive (e.g. CI) โ required to skip the prompt:
php artisan migrate:fresh-table users --with-related --force
The command resolves the full dependency closure, drops children-first and recreates parents-first, detaching and restoring foreign keys as needed โ including circular references. On PostgreSQL and SQL Server the whole run is wrapped in a transaction, so any failure rolls everything back rather than leaving the database half-dropped.
A cascade can touch a lot of tables and wipes their data. Preview with
--dry-runfirst and take a backup.
If one migration file creates several tables (e.g. users +
password_reset_tokens + sessions), the command treats the migration as
the unit: it drops those sibling tables together and re-runs the file once, so
recreation never collides with a leftover table.
Clearing data only (--data-only)
Sometimes you don't want to rebuild structure at all โ you just want to empty
the tables. --data-only deletes rows instead of dropping/recreating, leaving
every table, column, index and constraint exactly in place:
# Empty users and everything that depends on it, keeping all schema php artisan migrate:fresh-table users --with-related --data-only # Preview (nothing is deleted) php artisan migrate:fresh-table users --with-related --data-only --dry-run
Rows are deleted children-first (so no foreign key is violated) inside a
transaction. Because it never drops a table or re-runs a migration, --data-only
works cleanly even on schemas with bundled migrations, circular foreign keys, or
columns added by later ALTER migrations โ cases where a structural
drop/recreate is hard or impossible.
| drop/recreate (default) | --data-only |
|
|---|---|---|
| Table structure | rebuilt from its migration | left untouched |
| Re-runs migrations | yes | never |
| Clears data | yes | yes |
| Use when | you need to rebuild the schema | you just want to wipe the data |
Strategies (customizable rebuild logic)
How a table is recreated after it's dropped is decided by a strategy. Two ship out of the box, and you can register your own.
1. migration strategy (default)
Re-runs the migration(s) that build the table. It auto-detects the owning
migration by scanning your migration paths for Schema::create('<table>', โฆ). If
auto-detection isn't reliable for a table, add a manual override:
// config/migrate-fresh-table.php 'overrides' => [ 'users' => [ 'database/migrations/2014_10_12_000000_create_users_table.php', ], ],
2. schema strategy
Recreates the table from an explicit Blueprint callback โ handy when no single
migration cleanly owns a table:
use Illuminate\Database\Schema\Blueprint; // config/migrate-fresh-table.php 'schema' => [ 'sessions' => function (Blueprint $table) { $table->string('id')->primary(); $table->foreignId('user_id')->nullable()->index(); $table->string('ip_address', 45)->nullable(); $table->text('payload'); $table->integer('last_activity')->index(); }, ],
Select it per run or per table:
php artisan migrate:fresh-table sessions --strategy=schema
'table_strategies' => [ 'sessions' => 'schema', ],
3. Your own strategy
Implement the contract and register it:
use Kerroldj\MigrateFreshTable\Contracts\FreshStrategy; use Kerroldj\MigrateFreshTable\Support\FreshContext; class ParquetImportStrategy implements FreshStrategy { public function name(): string { return 'parquet'; } public function recreate(FreshContext $context): void { // $context->table, $context->connection, $context->schema, // $context->schemaBuilder, $context->pretend, $context->options $context->schemaBuilder->create($context->table, function ($table) { // ... }); } }
// config/migrate-fresh-table.php 'strategies' => [ 'migration' => \Kerroldj\MigrateFreshTable\Strategies\MigrationStrategy::class, 'schema' => \Kerroldj\MigrateFreshTable\Strategies\SchemaStrategy::class, 'parquet' => \App\Fresh\ParquetImportStrategy::class, ],
php artisan migrate:fresh-table events --strategy=parquet
Custom strategies are resolved through the container, so you may type-hint
dependencies. You can also swap the migration resolver itself by binding
Kerroldj\MigrateFreshTable\Contracts\TableResolver.
Multi-connection & multitenancy
# Target a specific connection php artisan migrate:fresh-table users --connection=tenant_42 # --database is an accepted alias php artisan migrate:fresh-table users --database=tenant_42 # Run across every configured connection / tenant in one call php artisan migrate:fresh-table users --all-connections --force
--all-connections iterates the static list plus anything returned by a
dynamic tenant resolver:
// config/migrate-fresh-table.php 'connections' => ['tenant_one', 'tenant_two'], 'tenant_resolver' => fn () => \App\Models\Tenant::query()->pluck('connection')->all(),
The resolved connection is always used explicitly โ the package never assumes the
default connection โ and the foreign-key report runs independently per
connection.
PostgreSQL schema awareness
# Fresh the same table within a specific schema / search_path
php artisan migrate:fresh-table audit_log --connection=pgsql --schema=reporting
The search_path is set for the duration of the run and restored afterward, so
the same table name in different schemas can be freshed independently.
Command reference
Events & hooks
Listen to lifecycle events (each carries connection, table, schema):
| Event | Fired |
|---|---|
TableFreshing |
before a table is freshed |
TableDropping / TableDropped |
around the drop |
TableRecreating / TableRecreated |
around the recreate |
TableFreshed |
after a table is freshed |
use Kerroldj\MigrateFreshTable\Events\TableFreshed; Event::listen(TableFreshed::class, function (TableFreshed $event) { logger()->info("Freshed {$event->table} on {$event->connection}"); });
Or use global callbacks, fired once per connection:
// config/migrate-fresh-table.php 'hooks' => [ 'before' => function (string $connection, array $tables) { /* ... */ }, 'after' => function (string $connection, array $tables) { /* ... */ }, ],
Safety notes
-
Protected environments (by default
production) ask you to confirm before anything is dropped โ an interactive run printsApplication is in the [production] environment.then asks โDo you really wish to run this command?โ. In any other environment (e.g.local) it runs without that question. Configure the list inconfig/migrate-fresh-table.php:// Which APP_ENV values require confirmation. Wildcards (e.g. "prod*") work. 'protected_environments' => ['production'],
-
--forceskips the confirmation, and is required to run non-interactively (e.g. in CI) within a protected environment โ otherwise the command refuses. -
--dry-runand--pretendare always allowed since they never mutate. -
The foreign-key impact report is always printed, and you are always prompted in interactive mode unless
--force. -
If a migration can't be resolved, the command fails loudly with a message telling you to add an
overridesentry or switch the table to theschemastrategy.
Testing
composer test # Pest composer analyse # PHPStan / Larastan composer format # Laravel Pint
The suite defaults to an in-memory SQLite database. Because every developer has
a different local setup, it also reads a .env from the package root โ copy the
example and point it at your database:
cp .env.example .env
# edit .env (DB_CONNECTION, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD)
vendor/bin/pest
DB_CONNECTION accepts sqlite, mysql, mariadb, pgsql, or sqlsrv
(postgres/postgresql and mssql/sqlserver work as aliases). Real
environment variables override .env, so a one-off run is also fine:
DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=laravel_test \ DB_USERNAME=postgres DB_PASSWORD=secret vendor/bin/pest
License
The MIT License (MIT). See LICENSE.


