
You delete a migration by deleting its file, but only if it has never run. Check with php artisan migrate:status: if it shows Ran, first run php artisan migrate:rollback --step=1, then delete the file. To add a column to an existing table, never edit the old migration: create a new one with --table.
Migrations version your database schema the way Git versions your code: every change is a file, replayable on any environment. Three operations cover most of the day-to-day work: adding a column to a table that already exists, rolling back a migration, and deleting a file that should never have been created.
Part of the Web development reading path. Every command and every output below was run on Laravel 13.30.1 with PHP 8.4.25.
Creating a migration
The command follows a naming convention that decides what ends up in the generated file:
php artisan make:migration create_tasks_tableThe name starts with create, followed by the plural table name, followed by table. Laravel recognises this pattern and pre-fills Schema::create() with id() and timestamps(), the title column is added here for the example, and the generated comments have been removed:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tasks');
}
};Since Laravel 8, migrations are anonymous classes returned by return new class extends Migration. Older named migrations, of the class CreateTasksTable extends Migration kind, still work but are no longer generated. In the same way, $table->bigIncrements('id') has given way to $table->id(), and the up() and down() methods now declare a void return type.
If the table needs creation and update dates, $table->timestamps() adds them: leaving them out is what causes the “Unknown column ‘updated_at’” error.
Running the migration creates the table:
php artisan migrateAdding a column to an existing table
Once the schema is in place, the everyday Eloquent queries do the rest. Never edit a migration that has already run: your colleagues and your servers have played it, and they will not play it again. Create a new migration instead, naming the table with --table:
php artisan make:migration add_notes_to_tasks_table --table=tasksThe generated file holds an empty Schema::table() in both directions. Fill them in: the column in up(), dropping it in down().
public function up(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->text('notes')->nullable()->after('title');
});
}
public function down(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->dropColumn('notes');
});
}php artisan migrate 2026_09_02_194036_add_notes_to_tasks_table .................... 47.43ms DONETwo precautions are worth repeating here.
Make the column nullable, or give it a default value. On a table that already holds rows, a NOT NULL column with no default makes the migration fail: the database has nothing to put in the existing rows.
Always write the down(). That is what makes the migration reversible. A migration with no down() turns the smallest step backwards into a manual job.
Positioning a column with after() is specific to MySQL and MariaDB. On SQLite the instruction is accepted without error but ignored: the column is added at the end of the table. Checked on SQLite 3.46.1, where notes ends up after updated_at despite the after('title'). Column order has no functional effect.
Rolling back a migration that has already run
Start by looking at where you stand. migrate:status lists every migration with its batch and its state:
php artisan migrate:status 2026_09_02_100000_create_tasks_table ............................... [1] Ran
2026_09_02_194036_add_notes_to_tasks_table ......................... PendingA Pending migration has never been played: its file can be deleted straight away. A Ran migration has to be rolled back first.
# rolls back the last migration only
php artisan migrate:rollback --step=1
# rolls back the whole last batch
php artisan migrate:rollbackThe rollback runs the down() method. In the example above, the notes column does disappear from the table and the migration goes back to Pending. That is the moment, and not before, when you can delete the file.
Two neighbouring commands exist, to be kept for development:
# rolls back everything, then replays everything
php artisan migrate:refresh
# drops every table, then replays everything
php artisan migrate:freshmigrate:fresh drops the tables, including those no migration manages. On a production server the command destroys the database, with no way of confirming once it has started. Keep it strictly for local development.
Deleting a migration
There is no make:migration --delete: you delete a migration by deleting its file, in database/migrations, the folder returned by database_path() among the application paths. The only question is whether it has already run.
It has never run (Pending state): delete the file, nothing else.
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.phpIt has already run (Ran state): roll it back first, then delete the file.
php artisan migrate:rollback --step=1
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.phpDeleting the file does not remove the matching row from the migrations table of the databases where it has already run. Those environments keep the column, with no migration left to explain it. In that case, delete nothing: write a new migration that undoes the change. The principle is the same as with Git, where a revert commit beats rewriting history that has already been published.
Changing an existing column
To change a type, a length or nullability, redeclare the column and add change():
public function up(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->string('title', 500)->change();
});
}Good news for anyone coming back from an old version: the doctrine/dbal package, required for this operation for years, has not been needed since Laravel 11. Checked on Laravel 13.30.1, where change() works with no extra dependency.
Any attribute you do not repeat is lost. A ->nullable()->default('x') column altered by a plain ->string('title', 500)->change() becomes non-nullable again, with no default value. Redeclare the full definition every time.
Common errors
nullable() or default().

