Tangible Bytes

A Web Developer’s Blog

Laravel Migrate - Exclude Tables

Laravel’s database migrations is a great system and makes it easy for the development team to stay in sync with schema changes as well as ensuring tests can run against a defined database state.

It also makes great use of transactions to efficiently roll back changes after each test

But what if you have some large tables of fairly static data that you don’t want to reload on every test run …

The issue is that

./artisan migrate:fresh

Drops all tables and starts again.

https://github.com/laravel/framework/blob/9.x/src/Illuminate/Database/Console/Migrations/FreshCommand.php#L44

$this->components->task('Dropping all tables', fn () => $this->callSilent('db:wipe', array_filter([
    '--database' => $database,
    '--drop-views' => $this->option('drop-views'),
    '--drop-types' => $this->option('drop-types'),
    '--force' => true,
])) == 0);

If all your tables are small this is fine - they get rebuilt fast enough.

But if you are integrating with an external data source with millions of rows this can be painful.

In my case I want to work with UK postcode data - there is a lot of it and while I will get updates - as far as the application is concerned this is read-only data and the tests do not benefit from a refresh.

Fresh vs Refresh

First of all I can use

./artisan migrate:refresh

This does not drop all the tables.

Instead it runs all the down() functions from my migrations and then all the up() ones

My large tables are not in my migrations and so don’t get dropped.

If I wanted to put the schema in a migration - I could make it an early one and just run the subsequent migrations

./artisan migrate:refresh --step=5 

(runs the last five migrations)

A bonus to all this - I get some testing of my rollbacks.

Testing

Laravel’s testing also resets the database and by default drops all tables.

protected function refreshTestDatabase()
{
if (! RefreshDatabaseState::$migrated) {
    $this->artisan('migrate:fresh', $this->migrateFreshUsing());
    $this->app[Kernel::class]->setArtisan(null);
        RefreshDatabaseState::$migrated = true;
    }
    $this->beginDatabaseTransaction();
}

https://github.com/laravel/framework/blob/9.x/src/Illuminate/Foundation/Testing/RefreshDatabase.php#L70

To avoid this we can override the refreshTestDatabase in our base test class


<?php
namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\RefreshDatabaseState;
use Illuminate\Contracts\Console\Kernel;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;
    use RefreshDatabase;

    protected function refreshTestDatabase()
    {
        if (! RefreshDatabaseState::$migrated) {
            $this->artisan('migrate:refresh', ['--seed' => true]);
            $this->app[Kernel::class]->setArtisan(null);
            RefreshDatabaseState::$migrated = true;
        }

        $this->beginDatabaseTransaction();
    }

}

Now all tests will use the customised refreshTestDatabase method and our external tables will be left alone.

This does have some scope to go wrong if there is an error in a migration rollback.

But at that point you can either go direct to the database to fix it - or manually run a full migration and rebuild the big table.

For small tables drop all is a safe bet and will probably be faster.

If you have some large datasets though - this could save a lot of time.