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

MethodSQL TypeDescriptionExample
increments(name)INTEGER AUTO_INCREMENTAuto-incrementing primary keytable.increments('id')
bigIncrements(name)BIGINT AUTO_INCREMENTLarge auto-incrementing keytable.bigIncrements('id')
uuid(name)CHAR(36)UUID columntable.uuid('id').primary()

String Types

MethodSQL TypeDescriptionExample
string(name, length)VARCHARText with max length (default 255)table.string('name', 100)
text(name)TEXTLong text (up to 65,535 chars)table.text('description')
mediumText(name)MEDIUMTEXTMedium text (up to 16MB)table.mediumText('content')
longText(name)LONGTEXTVery long text (up to 4GB)table.longText('article')
char(name, length)CHARFixed-length stringtable.char('country_code', 2)

Numeric Types

MethodSQL TypeDescriptionExample
integer(name)INTEGER32-bit integers (-2B to 2B)table.integer('age')
bigInteger(name)BIGINT64-bit integerstable.bigInteger('views')
smallInteger(name)SMALLINT16-bit integers (-32K to 32K)table.smallInteger('priority')
tinyInteger(name)TINYINT8-bit integers (-128 to 127)table.tinyInteger('status')
decimal(name, precision, scale)DECIMALExact decimal numberstable.decimal('price', 8, 2)
float(name, precision, scale)FLOATFloating point numberstable.float('rating', 3, 2)
double(name)DOUBLEDouble precision floatstable.double('coordinates')

Date/Time Types

MethodSQL TypeDescriptionExample
date(name)DATEDate only (YYYY-MM-DD)table.date('birth_date')
datetime(name)DATETIMEDate and timetable.datetime('created_at')
timestamp(name)TIMESTAMPUnix timestamptable.timestamp('updated_at')
time(name)TIMETime only (HH:MM:SS)table.time('start_time')
year(name)YEARYear only (1901-2155)table.year('graduation_year')

Other Types

MethodSQL TypeDescriptionExample
boolean(name)BOOLEAN/TINYINTTrue/false valuestable.boolean('is_active')
json(name)JSONJSON datatable.json('metadata')
jsonb(name)JSONBBinary JSON (PostgreSQL)table.jsonb('settings')
binary(name)BLOBBinary datatable.binary('file_data')
enum(name, values)ENUMPredefined valuestable.enum('status', ['active', 'inactive'])
geometry(name)GEOMETRYSpatial datatable.geometry('location')
point(name)POINTGeographic pointtable.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

ModifierDescriptionExample
.notNullable()Column cannot be nulltable.string('name').notNullable()
.nullable()Column can be null (default)table.string('bio').nullable()
.defaultTo(value)Set default valuetable.boolean('active').defaultTo(true)
.unique()Column values must be uniquetable.string('email').unique()
.primary()Set as primary keytable.uuid('id').primary()
.unsigned()Only positive numberstable.integer('age').unsigned()
.index()Add database indextable.string('slug').index()
.comment(text)Add column commenttable.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

ActionDescription
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

Next Steps