What are Migrations?
Migrations are like version control for your database. They’re scripts that create, modify, or delete database tables and columns in a structured way.
Real-world analogy: Migrations are like renovation blueprints for a house. Each migration is a set of instructions to change the structure - add a room, remove a wall, install new plumbing. You can apply them in order or roll them back if needed.
Creating Migrations
Using the CLI
# Create a new table
npx ilana make:migration create_users_table
# Modify existing table
npx ilana make:migration add_email_to_users --table=users
# Create table with specific name
npx ilana make:migration create_posts_table --create=posts
Migration File Structure
// database/migrations/20231201_120000_create_users_table.js
import { SchemaBuilder } from "ilana-orm" ;
export default class CreateUsersTable {
async up ( schema ) {
// What to do when running the migration
await schema . createTable ( 'users' , function ( table ) {
table . increments ( 'id' ); // Auto-incrementing primary key
table . string ( 'name' ). notNullable ();
table . string ( 'email' ). unique (). notNullable ();
table . timestamps ( true , true ); // created_at, updated_at
});
}
async down ( schema ) {
// How to undo the migration
await schema . dropTable ( 'users' );
}
}
Migration Naming: IlanaORM automatically adds timestamps to migration names to ensure they run in the correct order.
Creating Tables
Basic Table Creation
export default class CreateUsersTable {
async up ( schema ) {
await schema . createTable ( 'users' , function ( table ) {
// Primary key
table . increments ( 'id' ); // Auto-incrementing integer primary key
// String columns
table . string ( 'name' ). notNullable ();
table . string ( 'email' , 100 ). unique (). notNullable (); // Max length 100
table . text ( 'bio' ). nullable ();
// Number columns
table . integer ( 'age' ). unsigned (); // Positive integers only
table . decimal ( 'salary' , 10 , 2 ); // 10 total digits, 2 decimal places
table . float ( 'rating' ); // Floating point numbers
table . bigInteger ( 'views' ); // Large integers
// Boolean column
table . boolean ( 'is_active' ). defaultTo ( true );
// Date/Time columns
table . date ( 'birth_date' ); // Date only (YYYY-MM-DD)
table . datetime ( 'last_login' ); // Date and time
table . timestamp ( 'email_verified_at' ). nullable (); // Unix timestamp
table . time ( 'preferred_time' ); // Time only (HH:MM:SS)
// JSON column (PostgreSQL, MySQL 5.7+, SQLite 3.38+)
table . json ( 'preferences' );
// Binary data
table . binary ( 'avatar' ); // Binary data
// UUID column
table . uuid ( 'external_id' );
// Timestamps (created_at, updated_at)
table . timestamps ( true , true ); // Adds both columns automatically
});
}
}
Column Types Reference
Primary Keys
Method SQL Type Description Example increments(name)INTEGER AUTO_INCREMENT Auto-incrementing primary key table.increments('id')bigIncrements(name)BIGINT AUTO_INCREMENT Large auto-incrementing key table.bigIncrements('id')uuid(name)CHAR(36) UUID column table.uuid('id').primary()
String Types
Method SQL Type Description Example string(name, length)VARCHAR Text with max length (default 255) table.string('name', 100)text(name)TEXT Long text (up to 65,535 chars) table.text('description')mediumText(name)MEDIUMTEXT Medium text (up to 16MB) table.mediumText('content')longText(name)LONGTEXT Very long text (up to 4GB) table.longText('article')char(name, length)CHAR Fixed-length string table.char('country_code', 2)
Numeric Types
Method SQL Type Description Example integer(name)INTEGER 32-bit integers (-2B to 2B) table.integer('age')bigInteger(name)BIGINT 64-bit integers table.bigInteger('views')smallInteger(name)SMALLINT 16-bit integers (-32K to 32K) table.smallInteger('priority')tinyInteger(name)TINYINT 8-bit integers (-128 to 127) table.tinyInteger('status')decimal(name, precision, scale)DECIMAL Exact decimal numbers table.decimal('price', 8, 2)float(name, precision, scale)FLOAT Floating point numbers table.float('rating', 3, 2)double(name)DOUBLE Double precision floats table.double('coordinates')
Date/Time Types
Method SQL Type Description Example date(name)DATE Date only (YYYY-MM-DD) table.date('birth_date')datetime(name)DATETIME Date and time table.datetime('created_at')timestamp(name)TIMESTAMP Unix timestamp table.timestamp('updated_at')time(name)TIME Time only (HH:MM:SS) table.time('start_time')year(name)YEAR Year only (1901-2155) table.year('graduation_year')
Other Types
Method SQL Type Description Example boolean(name)BOOLEAN/TINYINT True/false values table.boolean('is_active')json(name)JSON JSON data table.json('metadata')jsonb(name)JSONB Binary JSON (PostgreSQL) table.jsonb('settings')binary(name)BLOB Binary data table.binary('file_data')enum(name, values)ENUM Predefined values table.enum('status', ['active', 'inactive'])geometry(name)GEOMETRY Spatial data table.geometry('location')point(name)POINT Geographic point table.point('coordinates')
Column Modifiers
table . string ( 'email' )
. notNullable () // Cannot be null
. unique () // Must be unique
. defaultTo ( 'guest' ) // Default value
. comment ( 'User email address' ); // Add comment
table . integer ( 'age' )
. unsigned () // Only positive numbers
. nullable (); // Can be null (default)
table . string ( 'status' )
. defaultTo ( 'active' )
. index (); // Add database index
// Chaining multiple modifiers
table . decimal ( 'price' , 10 , 2 )
. unsigned ()
. notNullable ()
. defaultTo ( 0.00 )
. comment ( 'Product price in USD' );
Available Modifiers
Modifier Description Example .notNullable()Column cannot be null table.string('name').notNullable().nullable()Column can be null (default) table.string('bio').nullable().defaultTo(value)Set default value table.boolean('active').defaultTo(true).unique()Column values must be unique table.string('email').unique().primary()Set as primary key table.uuid('id').primary().unsigned()Only positive numbers table.integer('age').unsigned().index()Add database index table.string('slug').index().comment(text)Add column comment table.string('name').comment('Full name').after(column)Position after column (MySQL) table.string('middle_name').after('first_name').first()Position as first column (MySQL) table.string('id').first()
Modifying Tables
Adding Columns
// Migration: add_columns_to_users_table.js
export default class AddColumnsToUsersTable {
async up ( schema ) {
await schema . table ( 'users' , function ( table ) {
table . string ( 'phone' ). nullable ();
table . text ( 'address' ). nullable ();
table . boolean ( 'is_verified' ). defaultTo ( false );
});
}
async down ( schema ) {
await schema . table ( 'users' , function ( table ) {
table . dropColumn ([ 'phone' , 'address' , 'is_verified' ]);
});
}
}
Modifying Columns
export default class ModifyUsersTable {
async up ( schema ) {
await schema . table ( 'users' , function ( table ) {
// Change column type
table . text ( 'bio' ). alter ();
// Rename column
table . renameColumn ( 'full_name' , 'name' );
// Modify column properties
table . string ( 'email' , 150 ). notNullable (). alter ();
});
}
}
Dropping Columns
export default class DropColumnsFromUsersTable {
async up ( schema ) {
await schema . table ( 'users' , function ( table ) {
table . dropColumn ( 'old_field' );
table . dropColumn ([ 'field1' , 'field2' ]); // Drop multiple
});
}
}
Indexes and Constraints
Creating Indexes
export default class CreatePostsTable {
async up ( schema ) {
await schema . createTable ( 'posts' , function ( table ) {
table . increments ( 'id' );
table . string ( 'title' );
table . text ( 'content' );
table . integer ( 'user_id' ). unsigned ();
table . string ( 'status' );
table . timestamps ( true , true );
// Single column index
table . index ( 'user_id' );
table . index ( 'status' );
// Composite index
table . index ([ 'user_id' , 'status' ], 'posts_user_status_index' );
// Unique index
table . unique ([ 'user_id' , 'title' ]);
});
}
}
Foreign Key Constraints
export default class CreatePostsTable {
async up ( schema ) {
await schema . createTable ( 'posts' , function ( table ) {
table . increments ( 'id' );
table . string ( 'title' );
table . integer ( 'user_id' ). unsigned (). notNullable ();
table . integer ( 'category_id' ). unsigned (). nullable ();
// Foreign key constraints
table . foreign ( 'user_id' ). references ( 'id' ). inTable ( 'users' ). onDelete ( 'CASCADE' );
table . foreign ( 'category_id' ). references ( 'id' ). inTable ( 'categories' ). onDelete ( 'SET NULL' );
});
}
}
Constraint Actions
Action Description CASCADEDelete related records SET NULLSet foreign key to null RESTRICTPrevent deletion if related records exist NO ACTIONDo nothing (database default)
UUID Primary Keys
export default class CreateUsersTable {
async up ( schema ) {
await schema . createTable ( 'users' , function ( table ) {
table . uuid ( 'id' ). primary (). defaultTo ( schema . raw ( 'gen_random_uuid()' )); // PostgreSQL
// table.uuid('id').primary(); // For other databases with UUID generation in app
table . string ( 'name' );
table . string ( 'email' ). unique ();
table . timestamps ( true , true );
});
}
}
Pivot Tables for Many-to-Many
// Migration: create_post_tags_table.js
export default class CreatePostTagsTable {
async up ( schema ) {
await schema . createTable ( 'post_tags' , function ( table ) {
table . increments ( 'id' );
table . integer ( 'post_id' ). unsigned (). notNullable ();
table . integer ( 'tag_id' ). unsigned (). notNullable ();
table . timestamps ( true , true );
// Foreign keys
table . foreign ( 'post_id' ). references ( 'id' ). inTable ( 'posts' ). onDelete ( 'CASCADE' );
table . foreign ( 'tag_id' ). references ( 'id' ). inTable ( 'tags' ). onDelete ( 'CASCADE' );
// Prevent duplicate relationships
table . unique ([ 'post_id' , 'tag_id' ]);
});
}
}
Polymorphic Tables
// Migration: create_comments_table.js
export default class CreateCommentsTable {
async up ( schema ) {
await schema . createTable ( 'comments' , function ( table ) {
table . increments ( 'id' );
table . text ( 'content' ). notNullable ();
table . integer ( 'user_id' ). unsigned (). notNullable ();
// Polymorphic columns
table . integer ( 'commentable_id' ). unsigned (). notNullable ();
table . string ( 'commentable_type' ). notNullable ();
table . timestamps ( true , true );
// Foreign key for user
table . foreign ( 'user_id' ). references ( 'id' ). inTable ( 'users' ). onDelete ( 'CASCADE' );
// Index for polymorphic relationship
table . index ([ 'commentable_id' , 'commentable_type' ]);
});
}
}
Running Migrations
Basic Commands
# Run all pending migrations
npx ilana migrate
# Run migrations on specific connection
npx ilana migrate --connection=postgres
# Run specific migration file
npx ilana migrate --only=20231201_create_users_table.js
# Run migrations up to specific batch
npx ilana migrate --to=20231201_120000
Rollback Migrations
# Rollback last batch
npx ilana migrate:rollback
# Rollback specific number of batches
npx ilana migrate:rollback --step=2
# Rollback to specific migration
npx ilana migrate:rollback --to=20231201_120000
# Rollback all migrations
npx ilana migrate:reset
Fresh Migrations
# Drop all tables and re-run migrations
npx ilana migrate:fresh
# Fresh migration with seeding
npx ilana migrate:fresh --seed
Migration Status
# Check migration status
npx ilana migrate:status
# List completed migrations
npx ilana migrate:list
# Unlock migrations (if stuck)
npx ilana migrate:unlock
Advanced Migration Patterns
Conditional Migrations
export default class AddEmailVerificationToUsers {
async up ( schema ) {
const hasColumn = await schema . hasColumn ( 'users' , 'email_verified_at' );
if ( ! hasColumn ) {
await schema . table ( 'users' , function ( table ) {
table . timestamp ( 'email_verified_at' ). nullable ();
});
}
}
}
Data Migrations
export default class AddFullNameToUsers {
async up ( schema ) {
// First, add the new column
await schema . table ( 'users' , function ( table ) {
table . string ( 'full_name' ). nullable ();
});
// Then, populate it with existing data
const users = await schema . raw ( 'SELECT id, first_name, last_name FROM users' );
for ( const user of users . rows || users ) {
await schema . raw ( 'UPDATE users SET full_name = ? WHERE id = ?' , [
` ${ user . first_name } ${ user . last_name } ` ,
user . id
]);
}
// Finally, make it not nullable
await schema . table ( 'users' , function ( table ) {
table . string ( 'full_name' ). notNullable (). alter ();
});
}
}
Raw SQL in Migrations
export default class CreateEmailLowerIndex {
async up ( schema ) {
await schema . raw ( `
CREATE INDEX CONCURRENTLY idx_users_email_lower
ON users (LOWER(email))
` );
}
async down ( schema ) {
await schema . raw ( 'DROP INDEX IF EXISTS idx_users_email_lower' );
}
}
Database-Specific Features
PostgreSQL
export default class CreateUsersTable {
async up ( schema ) {
await schema . createTable ( 'users' , function ( table ) {
table . uuid ( 'id' ). primary (). defaultTo ( schema . raw ( 'gen_random_uuid()' ));
table . string ( 'email' ). unique ();
table . jsonb ( 'preferences' ); // JSONB for better performance
table . specificType ( 'tags' , 'text[]' ); // Array type
table . timestamps ( true , true );
});
}
}
MySQL
export default class CreateUsersTable {
async up ( schema ) {
await schema . createTable ( 'users' , function ( table ) {
table . increments ( 'id' );
table . string ( 'email' ). unique ();
table . json ( 'preferences' );
table . timestamps ( true , true );
}). charset ( 'utf8mb4' ). collate ( 'utf8mb4_unicode_ci' ); // Support emojis
}
}
Migration Best Practices
1. Always Write Down Methods
// ✅ Good - can be rolled back
export default class CreateUsersTable {
async up ( schema ) {
await schema . createTable ( 'users' , function ( table ) {
// ... table definition
});
}
async down ( schema ) {
await schema . dropTable ( 'users' );
}
}
// ❌ Bad - can't be rolled back
export default class CreateUsersTable {
async up ( schema ) {
// ... table definition
}
async down ( schema ) {
// Empty or missing
}
}
2. Use Descriptive Names
// ✅ Good
create_users_table . js
add_email_verification_to_users . js
create_post_tags_pivot_table . js
// ❌ Bad
migration1 . js
update_users . js
new_table . js
3. Test Migrations Both Ways
# Test up
npx ilana migrate
# Test down
npx ilana migrate:rollback
# Test up again
npx ilana migrate
4. Keep Migrations Small
// ✅ Good - one change per migration
// Migration 1: create_users_table.js
// Migration 2: add_email_to_users.js
// Migration 3: create_posts_table.js
// ❌ Bad - too many changes
// Migration 1: create_all_tables.js (creates 10 tables)
5. Don’t Modify Old Migrations
// ❌ Don't edit existing migrations that have been run
// Instead, create a new migration to make changes
// ✅ Create new migration
npx ilana make : migration add_phone_to_users -- table = users
Troubleshooting
Problem: Error saying migration already existsSolutions:
Check if migration was already run: npx ilana migrate:status
Use different migration name
Roll back if needed: npx ilana migrate:rollback
Foreign Key Constraint Error
Problem: Cannot create foreign key constraintSolutions:
Ensure referenced table exists first
Check column types match (both should be same type)
Verify referenced column exists
Run migrations in correct order
Problem: Column already exists errorSolutions:
Check if column exists: schema.hasColumn()
Use conditional migration
Check migration history: npx ilana migrate:list
Next Steps
Database Seeders Learn to populate your database with sample data
Model Factories Generate realistic test data automatically