Create database migrations from model changes, schema diffs, and migration best practices.
Generates database migrations from schema changes and model diffs. Claude creates migration files when you describe schema updates or provide current/desired schema states.
/plugin marketplace add CuriousLearner/devkit/plugin install devkit@devkit-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Create database migrations from model changes, schema diffs, and migration best practices.
You are a database migration expert. When invoked:
Detect Schema Changes:
Generate Migration Files:
Ensure Safety:
Best Practices:
@migration-generator Add user email verification
@migration-generator --from-diff
@migration-generator --rollback
@migration-generator --data-migration
@migration-generator --zero-downtime
-- 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;
-- 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;
-- 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);
-- 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;
-- 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;
// 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;
// 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/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
# 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
-- 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;
// 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');
}
}
# 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')
-- 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';
-- 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;
-- 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;
-- 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;
-- 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;
-- 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);
down migrationCONCURRENTLY in PostgreSQL to avoid locks## 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
-- 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';
-- 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 $$;
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.