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:
php celestial make:migration create_products_tableSlenix automatically detects the intent from the migration name and generates the appropriate stub:
| Name pattern | Stub generated |
|---|---|
create_*_table | Schema::create() with up() and down() |
add_*_to_* | Schema::table() to add columns |
remove_*_from_* | Schema::table() to drop columns |
drop_*_table | Schema::dropIfExists() |
| anything else | blank stub |
The generated file will be placed in database/migrations/ with a timestamp prefix:
database/migrations/2025_06_01_143022_create_products_table.phpNote: 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
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
php celestial migrateThis 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:
php celestial migrate:statusOutput example:
---------------------------------------------------------------------------------
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
php celestial migrate:rollbackRolling Back Multiple Batches
Use the --step option to roll back more than one batch at a time:
php celestial migrate:rollback --step=3Rolling Back All Migrations
php celestial migrate:resetFresh 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:
php celestial migrate:freshWarning:
migrate:freshwill 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
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});Modifying an Existing Table
Schema::table('users', function (Blueprint $table) {
$table->string('phone', 20)->nullable()->after('email');
});Dropping a Table
// 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
if (Schema::hasTable('users')) {
// table exists
}
if (Schema::hasColumn('users', 'email')) {
// column exists
}Other Utilities
Schema::rename('old_table', 'new_table');
Schema::truncate('logs');Blueprint — Column Reference
Numeric Columns
$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
$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']); // SETDate and Time Columns
$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 deleteSpecial Columns
$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:
$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();| Modifier | Description |
|---|---|
->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
// 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
Using foreignId + constrained (recommended)
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_id → categories):
// 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)
$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:
php celestial make:migration add_discount_to_products_tablepublic 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
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('old_name', 'new_name');
});Dropping Multiple Columns
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.
Batch 1: create_users_table, create_categories_table
Batch 2: create_products_table
Batch 3: add_discount_to_products_tableRunning migrate:rollback reverts Batch 3. Running it again reverts Batch 2, and so on.
Celestial Migration Command Reference
| Command | Description |
|---|---|
make:migration <name> | Creates a new migration file with timestamp prefix |
migrate | Runs all pending migrations |
migrate:rollback | Reverts the last batch |
migrate:rollback --step=N | Reverts the last N batches |
migrate:reset | Reverts all migrations |
migrate:fresh | Resets and re-runs all migrations |
migrate:status | Shows the status (Ran / Pending) of all migrations |