migration-generator
About
This Claude Skill generates database migration scripts by analyzing schema differences between current and desired states. It automatically creates forward (up) and backward (down) migrations while following best practices like atomicity and reversibility. Developers should use it when they need to convert model changes into safe, production-ready database migrations with rollback capabilities.
Documentation
Migration Generator Skill
Create database migrations from model changes, schema diffs, and migration best practices.
Instructions
You are a database migration expert. When invoked:
-
Detect Schema Changes:
- Compare current schema with desired state
- Identify added/removed tables and columns
- Detect modified column types and constraints
- Find changed indexes and foreign keys
-
Generate Migration Files:
- Create forward (up) and backward (down) migrations
- Use ORM-specific migration format when applicable
- Include data migrations when needed
- Handle edge cases and potential data loss
-
Ensure Safety:
- Prevent accidental data deletion
- Add rollback capability
- Include validation steps
- Warn about breaking changes
-
Best Practices:
- Make migrations atomic and reversible
- Avoid destructive operations in production
- Test migrations on staging first
- Keep migrations small and focused
Supported Frameworks
- SQL: Raw SQL migrations (PostgreSQL, MySQL, SQLite)
- Node.js: Prisma, TypeORM, Sequelize, Knex.js
- Python: Alembic, Django migrations, SQLAlchemy
- Ruby: Rails Active Record Migrations
- Go: golang-migrate, goose
- PHP: Laravel migrations, Doctrine
Usage Examples
@migration-generator Add user email verification
@migration-generator --from-diff
@migration-generator --rollback
@migration-generator --data-migration
@migration-generator --zero-downtime
Raw SQL Migrations
PostgreSQL - Add Table
-- migrations/001_create_users_table.up.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
active BOOLEAN DEFAULT true NOT NULL,
created_at TIMESTAMP DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP DEFAULT NOW() NOT NULL
);
-- Create indexes
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_active ON users(active) WHERE active = true;
-- Add comments
COMMENT ON TABLE users IS 'Application users';
COMMENT ON COLUMN users.email IS 'User email address (unique)';
-- migrations/001_create_users_table.down.sql
DROP TABLE IF EXISTS users CASCADE;
Add Column with Default Value
-- migrations/002_add_email_verified.up.sql
-- Step 1: Add column as nullable
ALTER TABLE users ADD COLUMN email_verified BOOLEAN;
-- Step 2: Set default value for existing rows
UPDATE users SET email_verified = false WHERE email_verified IS NULL;
-- Step 3: Make column NOT NULL
ALTER TABLE users ALTER COLUMN email_verified SET NOT NULL;
-- Step 4: Set default for future rows
ALTER TABLE users ALTER COLUMN email_verified SET DEFAULT false;
-- migrations/002_add_email_verified.down.sql
ALTER TABLE users DROP COLUMN email_verified;
Modify Column Type (Safe)
-- migrations/003_increase_email_length.up.sql
-- Safe: increasing varchar length
ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(320);
-- migrations/003_increase_email_length.down.sql
-- Warning: May fail if data exceeds old limit
ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(255);
Add Foreign Key
-- migrations/004_create_orders.up.sql
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
total_amount DECIMAL(10,2) NOT NULL CHECK (total_amount >= 0),
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
created_at TIMESTAMP DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP DEFAULT NOW() NOT NULL,
CONSTRAINT fk_orders_user_id
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE
);
-- Indexes for foreign keys and common queries
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
-- Composite index for common query pattern
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- migrations/004_create_orders.down.sql
DROP TABLE IF EXISTS orders CASCADE;
Rename Column (Safe)
-- migrations/005_rename_password_column.up.sql
-- Step 1: Add new column
ALTER TABLE users ADD COLUMN password_hash_new VARCHAR(255);
-- Step 2: Copy data
UPDATE users SET password_hash_new = password_hash;
-- Step 3: Make NOT NULL
ALTER TABLE users ALTER COLUMN password_hash_new SET NOT NULL;
-- Step 4: Drop old column
ALTER TABLE users DROP COLUMN password_hash;
-- Step 5: Rename new column
ALTER TABLE users RENAME COLUMN password_hash_new TO password_hash;
-- migrations/005_rename_password_column.down.sql
-- Reversible using same pattern
ALTER TABLE users ADD COLUMN password_hash_old VARCHAR(255);
UPDATE users SET password_hash_old = password_hash;
ALTER TABLE users ALTER COLUMN password_hash_old SET NOT NULL;
ALTER TABLE users DROP COLUMN password_hash;
ALTER TABLE users RENAME COLUMN password_hash_old TO password_hash;
ORM Migration Examples
Prisma Migrations
// schema.prisma - Add new model
model User {
id Int @id @default(autoincrement())
email String @unique
username String @unique
passwordHash String @map("password_hash")
active Boolean @default(true)
emailVerified Boolean @default(false) @map("email_verified")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
orders Order[]
profile UserProfile?
@@index([email])
@@index([username])
@@map("users")
}
model UserProfile {
id Int @id @default(autoincrement())
userId Int @unique @map("user_id")
bio String? @db.Text
avatarUrl String? @map("avatar_url")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("user_profiles")
}
# Generate migration
npx prisma migrate dev --name add_user_profile
# Apply migration in production
npx prisma migrate deploy
# Reset database (development only!)
npx prisma migrate reset
Generated Migration:
-- CreateTable
CREATE TABLE "user_profiles" (
"id" SERIAL NOT NULL,
"user_id" INTEGER NOT NULL,
"bio" TEXT,
"avatar_url" TEXT,
CONSTRAINT "user_profiles_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "user_profiles_user_id_key" ON "user_profiles"("user_id");
-- AddForeignKey
ALTER TABLE "user_profiles" ADD CONSTRAINT "user_profiles_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
TypeORM Migrations
// migration/1234567890123-CreateUser.ts
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
export class CreateUser1234567890123 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'users',
columns: [
{
name: 'id',
type: 'int',
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment',
},
{
name: 'email',
type: 'varchar',
length: '255',
isUnique: true,
isNullable: false,
},
{
name: 'username',
type: 'varchar',
length: '50',
isUnique: true,
isNullable: false,
},
{
name: 'password_hash',
type: 'varchar',
length: '255',
isNullable: false,
},
{
name: 'active',
type: 'boolean',
default: true,
isNullable: false,
},
{
name: 'created_at',
type: 'timestamp',
default: 'now()',
isNullable: false,
},
{
name: 'updated_at',
type: 'timestamp',
default: 'now()',
isNullable: false,
},
],
}),
true,
);
// Create indexes
await queryRunner.createIndex(
'users',
new TableIndex({
name: 'idx_users_email',
columnNames: ['email'],
}),
);
await queryRunner.createIndex(
'users',
new TableIndex({
name: 'idx_users_username',
columnNames: ['username'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('users');
}
}
// migration/1234567890124-AddForeignKey.ts
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
export class AddOrdersForeignKey1234567890124 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'orders',
columns: [
{
name: 'id',
type: 'int',
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment',
},
{
name: 'user_id',
type: 'int',
isNullable: false,
},
{
name: 'total_amount',
type: 'decimal',
precision: 10,
scale: 2,
isNullable: false,
},
{
name: 'status',
type: 'varchar',
length: '20',
default: "'pending'",
isNullable: false,
},
{
name: 'created_at',
type: 'timestamp',
default: 'now()',
},
],
}),
true,
);
// Add foreign key
await queryRunner.createForeignKey(
'orders',
new TableForeignKey({
columnNames: ['user_id'],
referencedColumnNames: ['id'],
referencedTableName: 'users',
onDelete: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable('orders');
const foreignKey = table.foreignKeys.find(
fk => fk.columnNames.indexOf('user_id') !== -1,
);
await queryRunner.dropForeignKey('orders', foreignKey);
await queryRunner.dropTable('orders');
}
}
# Generate migration
npx typeorm migration:generate -n AddUserProfile
# Run migrations
npx typeorm migration:run
# Revert last migration
npx typeorm migration:revert
Alembic (Python/SQLAlchemy)
# alembic/versions/001_create_users_table.py
"""create users table
Revision ID: 001
Revises:
Create Date: 2024-01-01 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers
revision = '001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# Create users table
op.create_table(
'users',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('email', sa.String(length=255), nullable=False),
sa.Column('username', sa.String(length=50), nullable=False),
sa.Column('password_hash', sa.String(length=255), nullable=False),
sa.Column('active', sa.Boolean(), server_default='true', nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('username')
)
# Create indexes
op.create_index('idx_users_email', 'users', ['email'])
op.create_index('idx_users_username', 'users', ['username'])
op.create_index(
'idx_users_active',
'users',
['active'],
postgresql_where=sa.text('active = true')
)
def downgrade():
op.drop_table('users')
# alembic/versions/002_add_email_verified.py
"""add email_verified column
Revision ID: 002
Revises: 001
Create Date: 2024-01-02 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = '002'
down_revision = '001'
branch_labels = None
depends_on = None
def upgrade():
# Add column as nullable first
op.add_column('users', sa.Column('email_verified', sa.Boolean(), nullable=True))
# Set default value for existing rows
op.execute('UPDATE users SET email_verified = false WHERE email_verified IS NULL')
# Make column NOT NULL
op.alter_column('users', 'email_verified', nullable=False, server_default='false')
def downgrade():
op.drop_column('users', 'email_verified')
# Generate migration
alembic revision --autogenerate -m "add user profile"
# Run migrations
alembic upgrade head
# Rollback one migration
alembic downgrade -1
# Rollback to specific version
alembic downgrade 001
Django Migrations
# app/migrations/0001_initial.py
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True)),
('email', models.EmailField(max_length=255, unique=True)),
('username', models.CharField(max_length=50, unique=True)),
('password_hash', models.CharField(max_length=255)),
('active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'users',
},
),
migrations.AddIndex(
model_name='user',
index=models.Index(fields=['email'], name='idx_users_email'),
),
migrations.AddIndex(
model_name='user',
index=models.Index(fields=['username'], name='idx_users_username'),
),
]
# app/migrations/0002_add_user_profile.py
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True)),
('bio', models.TextField(blank=True, null=True)),
('avatar_url', models.URLField(blank=True, null=True)),
('user', models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to='app.user',
related_name='profile'
)),
],
options={
'db_table': 'user_profiles',
},
),
]
# Generate migrations
python manage.py makemigrations
# Apply migrations
python manage.py migrate
# Rollback to specific migration
python manage.py migrate app 0001
# Show migration status
python manage.py showmigrations
Data Migrations
Backfill Data (PostgreSQL)
-- migrations/006_backfill_user_roles.up.sql
-- Add role column
ALTER TABLE users ADD COLUMN role VARCHAR(20);
-- Backfill existing users with default role
UPDATE users SET role = 'member' WHERE role IS NULL;
-- Make NOT NULL after backfill
ALTER TABLE users ALTER COLUMN role SET NOT NULL;
ALTER TABLE users ALTER COLUMN role SET DEFAULT 'member';
-- Add check constraint
ALTER TABLE users ADD CONSTRAINT chk_users_role
CHECK (role IN ('admin', 'member', 'guest'));
-- migrations/006_backfill_user_roles.down.sql
ALTER TABLE users DROP COLUMN role;
Complex Data Migration (Node.js/TypeORM)
// migration/1234567890125-MigrateUserData.ts
import { MigrationInterface, QueryRunner } from 'typeorm';
export class MigrateUserData1234567890125 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Get all users
const users = await queryRunner.query('SELECT id, full_name FROM users');
// Split full_name into first_name and last_name
for (const user of users) {
const parts = user.full_name?.split(' ') || ['', ''];
const firstName = parts[0] || '';
const lastName = parts.slice(1).join(' ') || '';
await queryRunner.query(
'UPDATE users SET first_name = $1, last_name = $2 WHERE id = $3',
[firstName, lastName, user.id],
);
}
// Drop old column
await queryRunner.query('ALTER TABLE users DROP COLUMN full_name');
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Add back full_name column
await queryRunner.query('ALTER TABLE users ADD COLUMN full_name VARCHAR(255)');
// Reconstruct full_name
await queryRunner.query(
`UPDATE users SET full_name = first_name || ' ' || last_name`,
);
// Drop first_name and last_name
await queryRunner.query('ALTER TABLE users DROP COLUMN first_name');
await queryRunner.query('ALTER TABLE users DROP COLUMN last_name');
}
}
Data Migration with Python/Alembic
# alembic/versions/003_migrate_prices.py
"""migrate prices to cents
Revision ID: 003
Revises: 002
Create Date: 2024-01-03 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
revision = '003'
down_revision = '002'
def upgrade():
# Add new column
op.add_column('products', sa.Column('price_cents', sa.Integer(), nullable=True))
# Migrate data: convert decimal to cents
op.execute('''
UPDATE products
SET price_cents = CAST(price * 100 AS INTEGER)
''')
# Make NOT NULL after migration
op.alter_column('products', 'price_cents', nullable=False)
# Drop old column
op.drop_column('products', 'price')
# Rename new column
op.alter_column('products', 'price_cents', new_column_name='price')
def downgrade():
# Add back decimal column
op.add_column('products', sa.Column('price_decimal', sa.Numeric(10, 2), nullable=True))
# Convert back to decimal
op.execute('''
UPDATE products
SET price_decimal = price / 100.0
''')
op.alter_column('products', 'price_decimal', nullable=False)
op.drop_column('products', 'price')
op.alter_column('products', 'price_decimal', new_column_name='price')
Zero-Downtime Migrations
Adding NOT NULL Column
-- Migration 1: Add column as nullable
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Deploy application code that writes to phone column
-- Migration 2: Backfill existing data
UPDATE users SET phone = 'UNKNOWN' WHERE phone IS NULL;
-- Migration 3: Make column NOT NULL
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
ALTER TABLE users ALTER COLUMN phone SET DEFAULT 'UNKNOWN';
Renaming Column (Zero Downtime)
-- Phase 1: Add new column
ALTER TABLE users ADD COLUMN email_address VARCHAR(255);
-- Phase 2: Deploy app code that writes to both columns
-- Phase 3: Backfill data
UPDATE users SET email_address = email WHERE email_address IS NULL;
-- Phase 4: Deploy app code that reads from new column
-- Phase 5: Drop old column
ALTER TABLE users DROP COLUMN email;
-- Phase 6: Rename new column (optional)
ALTER TABLE users RENAME COLUMN email_address TO email;
Removing Column (Safe)
-- Phase 1: Deploy code that doesn't use the column
-- Phase 2: Remove NOT NULL constraint (make safe to rollback)
ALTER TABLE users ALTER COLUMN deprecated_field DROP NOT NULL;
-- Phase 3: Wait and verify no issues
-- Phase 4: Drop the column
ALTER TABLE users DROP COLUMN deprecated_field;
Common Patterns
Add Enum Column
-- Create enum type (PostgreSQL)
CREATE TYPE user_status AS ENUM ('active', 'inactive', 'suspended');
-- Add column with enum type
ALTER TABLE users ADD COLUMN status user_status DEFAULT 'active' NOT NULL;
-- Rollback
ALTER TABLE users DROP COLUMN status;
DROP TYPE user_status;
Add JSON Column
-- PostgreSQL
ALTER TABLE users ADD COLUMN metadata JSONB DEFAULT '{}' NOT NULL;
CREATE INDEX idx_users_metadata ON users USING GIN(metadata);
-- MySQL
ALTER TABLE users ADD COLUMN metadata JSON;
Add Full-Text Search
-- PostgreSQL
ALTER TABLE products ADD COLUMN search_vector tsvector;
-- Create generated column
UPDATE products SET search_vector =
to_tsvector('english', name || ' ' || description);
-- Create GIN index for fast searching
CREATE INDEX idx_products_search ON products USING GIN(search_vector);
-- Trigger to keep search_vector updated
CREATE TRIGGER products_search_update
BEFORE INSERT OR UPDATE ON products
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(search_vector, 'pg_catalog.english', name, description);
Best Practices
DO ✓
- Make migrations reversible - Always implement
downmigration - Test on staging first - Never run untested migrations in production
- Keep migrations small - One logical change per migration
- Use transactions - Ensure atomicity (when DB supports it)
- Backup before migration - Always have a rollback plan
- Add indexes concurrently - Use
CONCURRENTLYin PostgreSQL to avoid locks - Version control migrations - Commit migrations with code changes
- Document breaking changes - Add comments for complex migrations
- Use batch updates - For large data migrations, process in chunks
DON'T ✗
- Never modify committed migrations - Create new migration instead
- **Don't use SELECT *** - Specify columns in data migrations
- Avoid long-running migrations - Break into smaller steps
- Don't assume data state - Validate before transforming
- Never skip migrations - Run in order
- Don't ignore warnings - Address deprecation notices
- Avoid circular dependencies - Keep migration order clean
- Don't forget indexes - Especially on foreign keys
Migration Checklist
## Pre-Migration Checklist
- [ ] Migration tested on local database
- [ ] Migration tested on staging environment
- [ ] Database backup created
- [ ] Migration is reversible (down migration works)
- [ ] Reviewed for potential data loss
- [ ] Checked for long-running operations
- [ ] Foreign key constraints validated
- [ ] Indexes added for new columns
- [ ] Performance impact assessed
- [ ] Team notified of migration schedule
## Post-Migration Checklist
- [ ] Migration completed successfully
- [ ] Application logs checked for errors
- [ ] Database performance monitored
- [ ] Rollback plan tested (if needed)
- [ ] Documentation updated
- [ ] Migration marked as applied in version control
Troubleshooting
Migration Failed Mid-Way
-- Check migration status
SELECT * FROM schema_migrations;
-- Manual rollback if transaction failed
BEGIN;
-- Run down migration manually
ROLLBACK;
-- Or mark as not applied
DELETE FROM schema_migrations WHERE version = '20240101120000';
Large Table Migration
-- Use batch processing for large updates
DO $$
DECLARE
batch_size INTEGER := 1000;
offset_val INTEGER := 0;
rows_updated INTEGER;
BEGIN
LOOP
UPDATE users
SET email_verified = false
WHERE id IN (
SELECT id FROM users
WHERE email_verified IS NULL
ORDER BY id
LIMIT batch_size
OFFSET offset_val
);
GET DIAGNOSTICS rows_updated = ROW_COUNT;
EXIT WHEN rows_updated = 0;
offset_val := offset_val + batch_size;
COMMIT;
RAISE NOTICE 'Updated % rows', offset_val;
END LOOP;
END $$;
Notes
- Always test migrations in non-production environment first
- Use database transactions when possible
- Keep migrations in version control
- Document complex migrations
- Consider zero-downtime strategies for production
- Monitor database performance during migrations
- Have rollback plan ready
- Use ORM migration tools when available for type safety
Quick Install
/plugin add https://github.com/CuriousLearner/devkit/tree/main/migration-generatorCopy and paste this command in Claude Code to install this skill
GitHub 仓库
Related Skills
llamaindex
MetaLlamaIndex is a data framework for building RAG-powered LLM applications, specializing in document ingestion, indexing, and querying. It provides key features like vector indices, query engines, and agents, and supports over 300 data connectors. Use it for document Q&A, chatbots, and knowledge retrieval when building data-centric applications.
csv-data-summarizer
MetaThis skill automatically analyzes CSV files to generate comprehensive statistical summaries and visualizations using Python's pandas and matplotlib/seaborn. It should be triggered whenever a user uploads or references CSV data without prompting for analysis preferences. The tool provides immediate insights into data structure, quality, and patterns through automated analysis and visualization.
hybrid-cloud-networking
MetaThis skill configures secure hybrid cloud networking between on-premises infrastructure and cloud platforms like AWS, Azure, and GCP. Use it when connecting data centers to the cloud, building hybrid architectures, or implementing secure cross-premises connectivity. It supports key capabilities such as VPNs and dedicated connections like AWS Direct Connect for high-performance, reliable setups.
Excel Analysis
MetaThis skill enables developers to analyze Excel files and perform data operations using pandas. It can read spreadsheets, create pivot tables, generate charts, and conduct data analysis on .xlsx files and tabular data. Use it when working with Excel files, spreadsheets, or any structured tabular data within Claude Code.
