skills/doanchienthangdev/omgkit/managing-database-migrations

managing-database-migrations

SKILL.md

Managing Database Migrations

Purpose

Implement safe, reversible database migrations for production environments:

  • Apply expand-contract pattern for zero-downtime changes
  • Design rollback strategies for every migration
  • Handle large table alterations safely
  • Integrate migrations with CI/CD pipelines
  • Test migrations before production deployment

Quick Start

# Prisma
npx prisma migrate dev --name add_user_roles
npx prisma migrate deploy  # Production

# TypeORM
npm run typeorm migration:generate -- -n AddUserRoles
npm run typeorm migration:run

# Raw SQL with naming convention
# migrations/20241230_001_add_user_roles.sql
// Prisma migration example
// prisma/migrations/20241230_add_user_roles/migration.sql
ALTER TABLE "users" ADD COLUMN "role" VARCHAR(50) DEFAULT 'user';
CREATE INDEX "idx_users_role" ON "users"("role");

Features

Feature Strategy When to Use
Add Column Add nullable, then backfill, then NOT NULL Always safe
Remove Column Stop using, deploy, then remove Expand-contract
Rename Column Add new, copy data, remove old Zero-downtime required
Change Type Add new column, migrate, swap Data transformation
Add Index CREATE CONCURRENTLY Large tables (>1M rows)
Drop Table Rename first, drop after verification Reversible delete

Common Patterns

Expand-Contract Pattern

-- EXPAND: Add new structure (backward compatible)
-- Migration 1: Add new column
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);

-- Application: Write to BOTH columns
UPDATE users SET full_name = first_name || ' ' || last_name;

-- Deploy application that reads from new column

-- CONTRACT: Remove old structure
-- Migration 2: (after app deployed)
ALTER TABLE users DROP COLUMN first_name;
ALTER TABLE users DROP COLUMN last_name;

Safe Column Addition

-- Step 1: Add nullable column
ALTER TABLE orders ADD COLUMN shipping_method VARCHAR(50);

-- Step 2: Backfill in batches (avoid locking)
UPDATE orders SET shipping_method = 'standard'
WHERE id IN (SELECT id FROM orders WHERE shipping_method IS NULL LIMIT 10000);

-- Step 3: Add NOT NULL constraint
ALTER TABLE orders ALTER COLUMN shipping_method SET NOT NULL;
ALTER TABLE orders ALTER COLUMN shipping_method SET DEFAULT 'standard';

Safe Column Removal

-- Step 1: Stop application from using column
-- (Deploy code that no longer reads/writes to column)

-- Step 2: Drop default and constraints
ALTER TABLE users ALTER COLUMN legacy_field DROP DEFAULT;
ALTER TABLE users ALTER COLUMN legacy_field DROP NOT NULL;

-- Step 3: Remove column (after verification period)
ALTER TABLE users DROP COLUMN legacy_field;

Safe Index Creation (Large Tables)

-- WRONG: Locks table for duration
CREATE INDEX idx_orders_status ON orders(status);

-- CORRECT: Non-blocking for PostgreSQL
CREATE INDEX CONCURRENTLY idx_orders_status ON orders(status);

-- Note: CONCURRENTLY cannot run in transaction
-- For Prisma, use raw SQL migration:
-- prisma/migrations/xxx/migration.sql
-- /* Disable transaction for this migration */
CREATE INDEX CONCURRENTLY idx_orders_status ON orders(status);

Prisma Migration Workflow

# Development: Create and apply migration
npx prisma migrate dev --name add_user_roles

# Preview SQL without applying
npx prisma migrate diff \
  --from-schema-datamodel prisma/schema.prisma \
  --to-schema-datamodel prisma/schema.prisma.new \
  --script

# Production deployment
npx prisma migrate deploy

# Reset for development (DESTRUCTIVE)
npx prisma migrate reset
// schema.prisma - Example with relations
model User {
  id        String   @id @default(uuid())
  email     String   @unique
  role      Role     @default(USER)
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([email])
}

enum Role {
  USER
  ADMIN
}

TypeORM Migration

// migrations/1703936400000-AddUserRoles.ts
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddUserRoles1703936400000 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      ALTER TABLE "users" ADD COLUMN "role" VARCHAR(50) DEFAULT 'user'
    `);
    await queryRunner.query(`
      CREATE INDEX "idx_users_role" ON "users"("role")
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP INDEX "idx_users_role"`);
    await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "role"`);
  }
}

Data Migration Pattern

// Separate data migration from schema migration
export class MigrateUserNames1703936500000 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    // Batch process to avoid memory issues
    const batchSize = 1000;
    let offset = 0;

    while (true) {
      const result = await queryRunner.query(`
        UPDATE users
        SET full_name = first_name || ' ' || last_name
        WHERE full_name IS NULL
        LIMIT ${batchSize}
      `);

      if (result.affectedRows === 0) break;
      offset += batchSize;

      // Optional: Add delay to reduce load
      await new Promise(r => setTimeout(r, 100));
    }
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    // Data migrations often aren't reversible
    console.log('Data migration rollback: manual intervention required');
  }
}

Use Cases

  • Adding new features requiring schema changes
  • Refactoring database structure safely
  • Splitting or merging tables
  • Changing column data types
  • Large-scale data migrations

Best Practices

Do Avoid
Test migrations on production-like data Testing only on empty databases
Use expand-contract for breaking changes Direct column renames in production
Create CONCURRENTLY for large table indexes Blocking index creation on live tables
Batch large data updates Updating millions of rows in one transaction
Include rollback in every migration Forward-only migrations without escape
Run migrations in CI before deploy Manual migration execution
Version control all migrations Modifying applied migrations
Separate schema and data migrations Mixing DDL and large DML in one migration

Migration Checklist

Pre-Migration:
[ ] Migration tested on staging with production data volume
[ ] Rollback script written and tested
[ ] Estimated execution time documented
[ ] Backup verified

During Migration:
[ ] Monitor database locks and connections
[ ] Check application error rates
[ ] Verify migration progress

Post-Migration:
[ ] Verify data integrity
[ ] Check application functionality
[ ] Monitor performance metrics
[ ] Document completion

CI/CD Integration

# GitHub Actions example
- name: Run Migrations
  run: |
    # Wait for healthy database
    until pg_isready -h $DB_HOST; do sleep 1; done

    # Run migrations with timeout
    timeout 600 npx prisma migrate deploy

    # Verify migration status
    npx prisma migrate status
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}

Related Skills

See also these related skill documents:

  • designing-database-schemas - Schema design principles
  • managing-databases - DBA operations and maintenance
  • optimizing-databases - Performance tuning
Weekly Installs
3
GitHub Stars
3
First Seen
Feb 26, 2026
Installed on
opencode3
gemini-cli3
claude-code3
github-copilot3
codex3
amp3