Files
2025-11-30 08:47:23 +08:00

9.4 KiB

name, description
name description
migration-checker WHEN: Database migration review, backwards compatibility, rollback safety, data integrity WHAT: Migration safety + Rollback validation + Downtime analysis + Data preservation + Lock prevention WHEN NOT: Query optimization → sql-optimizer, Schema design → schema-reviewer

Migration Checker Skill

Purpose

Reviews database migrations for safety, backwards compatibility, rollback capability, and zero-downtime deployment.

When to Use

  • Database migration review
  • Schema change safety check
  • Rollback plan validation
  • Zero-downtime deployment check
  • Data migration review

Project Detection

  • migrations/ directory
  • Prisma migrations
  • Alembic (Python)
  • TypeORM migrations
  • Rails migrations
  • Flyway/Liquibase

Workflow

Step 1: Analyze Migration

**Type**: Schema change / Data migration
**Risk Level**: High (adds non-nullable column)
**Tables Affected**: users, orders
**Estimated Rows**: 1M+

Step 2: Select Review Areas

AskUserQuestion:

"Which areas to review?"
Options:
- Full migration review (recommended)
- Backwards compatibility
- Rollback safety
- Lock and downtime analysis
- Data integrity
multiSelect: true

Detection Rules

Dangerous Operations

Operation Risk Mitigation
Add NOT NULL without default CRITICAL Add default or multi-step
Drop column HIGH Ensure no code references
Rename column HIGH Use multi-step migration
Change column type HIGH Check data compatibility
Add unique constraint HIGH Verify no duplicates first
Drop table CRITICAL Ensure no references

Adding Non-Nullable Column

-- BAD: Direct NOT NULL column (will fail on existing rows)
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NOT NULL;
-- ERROR: column "phone" contains null values

-- GOOD: Multi-step migration
-- Step 1: Add nullable column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- Step 2: Backfill data (in batches for large tables)
UPDATE users SET phone = 'unknown' WHERE phone IS NULL;

-- Step 3: Add NOT NULL constraint
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;

Renaming Columns (Zero-Downtime)

-- BAD: Direct rename (breaks running code)
ALTER TABLE users RENAME COLUMN name TO full_name;
-- Old code still uses "name"!

-- GOOD: Multi-step migration

-- Migration 1: Add new column
ALTER TABLE users ADD COLUMN full_name VARCHAR(200);

-- Migration 2: Sync data (application writes to both)
UPDATE users SET full_name = name WHERE full_name IS NULL;

-- Deploy: Update code to read from full_name

-- Migration 3: Drop old column (after code deployment)
ALTER TABLE users DROP COLUMN name;

Index Operations

-- BAD: Regular CREATE INDEX (blocks writes)
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- Locks table for duration!

-- GOOD: Concurrent index (PostgreSQL)
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders(user_id);
-- Doesn't block writes, but takes longer

-- Note: CONCURRENTLY cannot be in transaction
-- Must be separate migration without transaction wrapper

Data Migrations

# BAD: Load all data into memory
def migrate():
    users = User.objects.all()  # 1M rows in memory!
    for user in users:
        user.email = user.email.lower()
        user.save()

# GOOD: Batch processing
def migrate():
    batch_size = 1000
    total = User.objects.count()

    for offset in range(0, total, batch_size):
        users = User.objects.all()[offset:offset + batch_size]
        for user in users:
            user.email = user.email.lower()
        User.objects.bulk_update(users, ['email'])

# BETTER: Raw SQL for large tables
def migrate():
    with connection.cursor() as cursor:
        cursor.execute("""
            UPDATE users
            SET email = LOWER(email)
            WHERE id IN (
                SELECT id FROM users
                WHERE email != LOWER(email)
                LIMIT 10000
            )
        """)

Prisma Migration Safety

// schema.prisma

// BAD: Direct non-nullable addition
model User {
  id    Int    @id @default(autoincrement())
  email String
  phone String  // New non-nullable field - will fail!
}

// GOOD: Add with default first
model User {
  id    Int    @id @default(autoincrement())
  email String
  phone String @default("")  // Safe to add
}

// Or nullable first, then make required
model User {
  id    Int     @id @default(autoincrement())
  email String
  phone String?  // Step 1: nullable
}

TypeORM Migration

// BAD: Unsafe migration
export class AddUserPhone1234567890 implements MigrationInterface {
    async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(
            `ALTER TABLE users ADD phone VARCHAR(20) NOT NULL`
        );
    }

    async down(queryRunner: QueryRunner): Promise<void> {
        // No rollback!
    }
}

// GOOD: Safe migration with rollback
export class AddUserPhone1234567890 implements MigrationInterface {
    async up(queryRunner: QueryRunner): Promise<void> {
        // Step 1: Add nullable column
        await queryRunner.query(
            `ALTER TABLE users ADD phone VARCHAR(20)`
        );

        // Step 2: Backfill
        await queryRunner.query(
            `UPDATE users SET phone = '' WHERE phone IS NULL`
        );

        // Step 3: Add constraint
        await queryRunner.query(
            `ALTER TABLE users ALTER COLUMN phone SET NOT NULL`
        );
    }

    async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(
            `ALTER TABLE users DROP COLUMN phone`
        );
    }
}

Django Migrations

# BAD: RunPython without reverse
class Migration(migrations.Migration):
    operations = [
        migrations.RunPython(forwards_func),  # No reverse!
    ]

# GOOD: With reverse operation
class Migration(migrations.Migration):
    operations = [
        migrations.RunPython(
            forwards_func,
            reverse_code=backwards_func
        ),
    ]

# BAD: Atomic migration for large data change
class Migration(migrations.Migration):
    operations = [
        migrations.RunPython(update_millions_of_rows),
    ]

# GOOD: Non-atomic for large changes
class Migration(migrations.Migration):
    atomic = False

    operations = [
        migrations.RunPython(update_in_batches),
    ]

Rollback Checklist

-- Verify rollback for each operation

-- ADD COLUMN → DROP COLUMN
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Rollback: ALTER TABLE users DROP COLUMN phone;

-- ADD CONSTRAINT → DROP CONSTRAINT
ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);
-- Rollback: ALTER TABLE orders DROP CONSTRAINT fk_user;

-- CREATE INDEX → DROP INDEX
CREATE INDEX idx_email ON users(email);
-- Rollback: DROP INDEX idx_email;

-- RENAME COLUMN → Complex (need old column back)
-- Better to use add/copy/drop pattern

Lock Analysis

-- PostgreSQL: Check what operations lock tables
-- https://www.postgresql.org/docs/current/explicit-locking.html

-- ACCESS EXCLUSIVE (blocks everything):
-- - DROP TABLE
-- - ALTER TABLE ... ADD COLUMN with DEFAULT (< PG 11)
-- - ALTER TABLE ... ALTER COLUMN TYPE

-- SHARE UPDATE EXCLUSIVE (blocks DDL, allows DML):
-- - CREATE INDEX CONCURRENTLY
-- - VACUUM

-- Check for blocking locks before migration
SELECT blocked_locks.pid AS blocked_pid,
       blocked_activity.usename AS blocked_user,
       blocking_locks.pid AS blocking_pid,
       blocking_activity.usename AS blocking_user,
       blocked_activity.query AS blocked_statement
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity
    ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
    ON blocking_locks.locktype = blocked_locks.locktype
WHERE NOT blocked_locks.granted;

Response Template

## Migration Review Results

**Migration**: 20240115_add_user_phone
**Type**: Schema change
**Risk Level**: HIGH

### Safety Analysis
| Status | Issue | Impact |
|--------|-------|--------|
| CRITICAL | Non-nullable column without default | Migration will fail |
| HIGH | No rollback operation defined | Cannot revert |
| MEDIUM | Missing concurrent index | Table lock during creation |

### Lock Analysis
| Operation | Lock Type | Duration |
|-----------|-----------|----------|
| ADD COLUMN | ACCESS EXCLUSIVE | Brief |
| CREATE INDEX | ACCESS EXCLUSIVE | Long (use CONCURRENTLY) |

### Rollback Plan
- [ ] Add rollback script
- [ ] Test rollback in staging
- [ ] Document manual rollback steps

### Recommended Changes
```sql
-- Step 1: Add nullable column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- Step 2: Backfill data
UPDATE users SET phone = '' WHERE phone IS NULL;

-- Step 3: Add NOT NULL
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;

-- Rollback
ALTER TABLE users DROP COLUMN phone;

Pre-Migration Checklist

  • Backup database
  • Test in staging with production data
  • Schedule during low-traffic period
  • Notify team of potential downtime
  • Prepare rollback script

## Best Practices
1. **Multi-Step**: Break dangerous operations into steps
2. **Rollback**: Always define down/reverse migration
3. **Concurrent**: Use CONCURRENTLY for indexes
4. **Batching**: Process large data in chunks
5. **Testing**: Test with production-like data volume

## Integration
- `schema-reviewer`: Schema design review
- `sql-optimizer`: Query performance
- `ci-cd-reviewer`: Migration in pipelines