Database Migrations

Introduction

Migrations are like version control for your database, allowing your team to define and share the application's database schema definition. They keep the database structure synchronized across all development environments and deployment stages.

The Slenix migration system uses a fluent Blueprint API to describe tables and columns, and a Schema facade to execute DDL operations — all without writing raw SQL.

Each migration file lives in the database/migrations/ directory and is automatically ordered by the timestamp prefix in its filename.


Generating Migrations

To create a new migration, use the make:migration Celestial command:

bash
php celestial make:migration create_products_table

Slenix automatically detects the intent from the migration name and generates the appropriate stub:

Name patternStub generated
create_*_tableSchema::create() with up() and down()
add_*_to_*Schema::table() to add columns
remove_*_from_*Schema::table() to drop columns
drop_*_tableSchema::dropIfExists()
anything elseblank stub

The generated file will be placed in database/migrations/ with a timestamp prefix:

plaintext
database/migrations/2025_06_01_143022_create_products_table.php

Note: Migrations are executed in alphabetical order, and the timestamp prefix guarantees the correct order automatically.


Migration Structure

Each migration file must return an anonymous class that extends Migration and implements two methods: up() to apply the changes and down() to revert them.

php
<?php

declare(strict_types=1);

use Slenix\Supports\Database\Migrations\Migration;
use Slenix\Supports\Database\Migrations\Schema;
use Slenix\Supports\Database\Migrations\Blueprint;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();                                                         // BIGINT UNSIGNED AUTO_INCREMENT PK
            $table->string('name', 150);                                          // VARCHAR(150) NOT NULL
            $table->string('slug')->unique();                                     // VARCHAR(255) UNIQUE
            $table->text('description')->nullable();                              // TEXT NULL
            $table->decimal('price', 10, 2);                                      // DECIMAL(10,2)
            $table->integer('stock')->default(0);                                 // INT DEFAULT 0
            $table->boolean('is_active')->default(true);                          // TINYINT(1) DEFAULT 1
            $table->foreignId('category_id')->constrained()->onDelete('cascade'); // FK → categories.id
            $table->timestamps();                                                  // created_at + updated_at
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};

Running Migrations

Running All Pending Migrations

bash
php celestial migrate

This executes every migration that has not yet been run, in timestamp order, and records each one in the migrations table.

Checking Status

To see which migrations have been run and which are still pending:

bash
php celestial migrate:status

Output example:

plaintext
---------------------------------------------------------------------------------
STATUS   BATCH    MIGRATION
---------------------------------------------------------------------------------
Ran      1        2025_06_01_143000_create_categories_table
Ran      1        2025_06_01_143022_create_products_table
Pending  -        2025_06_02_090000_add_discount_to_products_table
---------------------------------------------------------------------------------

Rolling Back Migrations

Rolling Back the Last Batch

bash
php celestial migrate:rollback

Rolling Back Multiple Batches

Use the --step option to roll back more than one batch at a time:

bash
php celestial migrate:rollback --step=3

Rolling Back All Migrations

bash
php celestial migrate:reset

Fresh Start (Reset + Migrate)

The migrate:fresh command drops all tables by rolling back every migration and then re-runs them from scratch. This is useful during development to rebuild a clean database:

bash
php celestial migrate:fresh

Warning: migrate:fresh will destroy all data in the database. Never run it in a production environment.


Schema Facade

The Schema facade provides static methods for all DDL operations.

Creating a Table

php
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamps();
});

Modifying an Existing Table

php
Schema::table('users', function (Blueprint $table) {
    $table->string('phone', 20)->nullable()->after('email');
});

Dropping a Table

php
// Silently ignores if the table does not exist
Schema::dropIfExists('users');

// Throws an error if the table does not exist
Schema::drop('users');

Checking Existence

php
if (Schema::hasTable('users')) {
    // table exists
}

if (Schema::hasColumn('users', 'email')) {
    // column exists
}

Other Utilities

php
Schema::rename('old_table', 'new_table');
Schema::truncate('logs');

Blueprint — Column Reference

Numeric Columns

php
$table->id();                          // BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
$table->tinyInteger('rating');         // TINYINT
$table->smallInteger('code');          // SMALLINT
$table->integer('stock');              // INT
$table->bigInteger('views');           // BIGINT
$table->float('latitude', 10, 6);      // FLOAT(10,6)
$table->double('amount', 15, 8);       // DOUBLE(15,8)
$table->decimal('price', 10, 2);       // DECIMAL(10,2) — recommended for money
$table->boolean('is_active');          // TINYINT(1)

String Columns

php
$table->char('code', 6);              // CHAR(6)
$table->string('name', 150);          // VARCHAR(150) — default 255
$table->text('description');          // TEXT
$table->tinyText('excerpt');          // TINYTEXT
$table->mediumText('body');           // MEDIUMTEXT
$table->longText('content');          // LONGTEXT
$table->json('meta');                 // JSON
$table->enum('status', ['active', 'inactive', 'pending']); // ENUM
$table->set('permissions', ['read', 'write', 'delete']);    // SET

Date and Time Columns

php
$table->date('birth_date');            // DATE
$table->time('opens_at');              // TIME
$table->dateTime('published_at');      // DATETIME
$table->timestamp('verified_at');      // TIMESTAMP NULL
$table->year('founded');               // YEAR
$table->timestamps();                  // created_at + updated_at (TIMESTAMP NULL)
$table->softDeletes();                 // deleted_at (TIMESTAMP NULL) — for soft delete

Special Columns

php
$table->uuid('uuid');                  // VARCHAR(36)
$table->foreignId('user_id');          // BIGINT UNSIGNED NOT NULL (FK definition below)
$table->ipAddress('last_ip');          // VARCHAR(45) — supports IPv6
$table->macAddress('device_mac');      // VARCHAR(17)

Blueprint — Column Modifiers

Modifiers are chained directly after the column definition:

php
$table->string('nickname')->nullable();
$table->integer('score')->default(0);
$table->string('status')->default('pending');
$table->boolean('active')->default(true);
$table->text('notes')->nullable()->comment('Internal notes only');
$table->string('phone')->nullable()->after('email');
$table->integer('position')->unsigned();
ModifierDescription
->nullable()Allows NULL values
->default($value)Sets a default value
->unsigned()Makes numeric columns unsigned
->comment('text')Adds a column comment
->after('column')Places the column after another (ALTER TABLE)
->first()Places the column first (ALTER TABLE)

Blueprint — Indexes

php
// Unique constraint on a single column (chainable)
$table->string('email')->unique();

// Unique constraint on multiple columns
$table->unique(['email', 'tenant_id']);

// Standard index for query performance
$table->index('category_id');
$table->index(['user_id', 'created_at'], 'idx_user_date');

// Full-text index for text search
$table->fullText(['title', 'content']);

Blueprint — Foreign Keys

The foreignId method creates a BIGINT UNSIGNED NOT NULL column. Chaining constrained() automatically infers the referenced table name from the column name (e.g., category_idcategories):

php
// Infers: REFERENCES categories(id)
$table->foreignId('category_id')->constrained();

// Explicit table name
$table->foreignId('author_id')->constrained('users');

// With cascade actions
$table->foreignId('user_id')->constrained()->onDelete('cascade')->onUpdate('cascade');

Available onDelete / onUpdate actions: CASCADE, SET NULL, RESTRICT, NO ACTION.

Using foreign (explicit syntax)

php
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');

Modifying Tables

To add or remove columns from an existing table, generate an alter migration:

bash
php celestial make:migration add_discount_to_products_table
php
public function up(): void
{
    Schema::table('products', function (Blueprint $table) {
        $table->decimal('discount', 5, 2)->nullable()->after('price');
        $table->index('discount');
    });
}

public function down(): void
{
    Schema::table('products', function (Blueprint $table) {
        $table->dropColumn('discount');
    });
}

Renaming a Column

php
Schema::table('users', function (Blueprint $table) {
    $table->renameColumn('old_name', 'new_name');
});

Dropping Multiple Columns

php
Schema::table('users', function (Blueprint $table) {
    $table->dropColumn(['bio', 'website', 'twitter']);
});

Migration Batches

Every time you run php celestial migrate, all migrations executed in that run are grouped into a batch. Rolling back removes the most recent batch first, then the previous one, and so on.

plaintext
Batch 1: create_users_table, create_categories_table
Batch 2: create_products_table
Batch 3: add_discount_to_products_table

Running migrate:rollback reverts Batch 3. Running it again reverts Batch 2, and so on.


Celestial Migration Command Reference

CommandDescription
make:migration <name>Creates a new migration file with timestamp prefix
migrateRuns all pending migrations
migrate:rollbackReverts the last batch
migrate:rollback --step=NReverts the last N batches
migrate:resetReverts all migrations
migrate:freshResets and re-runs all migrations
migrate:statusShows the status (Ran / Pending) of all migrations