| name | database-migration |
| description | Execute database migrations across ORMs and platforms with zero-downtime strategies, data transformation, and rollback procedures. Use when migrating databases, changing schemas, performing data transformations, or implementing zero-downtime deployment strategies. |
Database Migration
Master database schema and data migrations across ORMs (Sequelize, TypeORM, Prisma), including rollback strategies and zero-downtime deployments.
When to Use This Skill
- Migrating between different ORMs
- Performing schema transformations
- Moving data between databases
- Implementing rollback procedures
- Zero-downtime deployments
- Database version upgrades
- Data model refactoring
ORM Migrations
Sequelize Migrations
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
email: {
type: Sequelize.STRING,
unique: true,
allowNull: false,
},
createdAt: Sequelize.DATE,
updatedAt: Sequelize.DATE,
})
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('users')
},
}
TypeORM Migrations
import { MigrationInterface, QueryRunner, Table } from 'typeorm'
export class CreateUsers1701234567 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',
isUnique: true,
},
{
name: 'created_at',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
},
],
})
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('users')
}
}
Prisma Migrations
// schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now())
}
// Generate migration: npx prisma migrate dev --name create_users
// Apply: npx prisma migrate deploy
Schema Transformations
Adding Columns with Defaults
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'status', {
type: Sequelize.STRING,
defaultValue: 'active',
allowNull: false,
})
},
down: async (queryInterface) => {
await queryInterface.removeColumn('users', 'status')
},
}
Renaming Columns (Zero Downtime)
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'full_name', {
type: Sequelize.STRING,
})
await queryInterface.sequelize.query('UPDATE users SET full_name = name')
},
down: async (queryInterface) => {
await queryInterface.removeColumn('users', 'full_name')
},
}
module.exports = {
up: async (queryInterface) => {
await queryInterface.removeColumn('users', 'name')
},
down: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'name', {
type: Sequelize.STRING,
})
},
}
Changing Column Types
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'age_new', {
type: Sequelize.INTEGER,
})
await queryInterface.sequelize.query(`
UPDATE users
SET age_new = CAST(age AS INTEGER)
WHERE age IS NOT NULL
`)
await queryInterface.removeColumn('users', 'age')
await queryInterface.renameColumn('users', 'age_new', 'age')
},
down: async (queryInterface, Sequelize) => {
await queryInterface.changeColumn('users', 'age', {
type: Sequelize.STRING,
})
},
}
Data Transformations
Complex Data Migration
module.exports = {
up: async (queryInterface, Sequelize) => {
const [users] = await queryInterface.sequelize.query('SELECT id, address_string FROM users')
for (const user of users) {
const addressParts = user.address_string.split(',')
await queryInterface.sequelize.query(
`UPDATE users
SET street = :street,
city = :city,
state = :state
WHERE id = :id`,
{
replacements: {
id: user.id,
street: addressParts[0]?.trim(),
city: addressParts[1]?.trim(),
state: addressParts[2]?.trim(),
},
}
)
}
await queryInterface.removeColumn('users', 'address_string')
},
down: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'address_string', {
type: Sequelize.STRING,
})
await queryInterface.sequelize.query(`
UPDATE users
SET address_string = CONCAT(street, ', ', city, ', ', state)
`)
await queryInterface.removeColumn('users', 'street')
await queryInterface.removeColumn('users', 'city')
await queryInterface.removeColumn('users', 'state')
},
}
Rollback Strategies
Transaction-Based Migrations
module.exports = {
up: async (queryInterface, Sequelize) => {
const transaction = await queryInterface.sequelize.transaction()
try {
await queryInterface.addColumn(
'users',
'verified',
{ type: Sequelize.BOOLEAN, defaultValue: false },
{ transaction }
)
await queryInterface.sequelize.query('UPDATE users SET verified = true WHERE email_verified_at IS NOT NULL', {
transaction,
})
await transaction.commit()
} catch (error) {
await transaction.rollback()
throw error
}
},
down: async (queryInterface) => {
await queryInterface.removeColumn('users', 'verified')
},
}
Checkpoint-Based Rollback
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query('CREATE TABLE users_backup AS SELECT * FROM users')
try {
await queryInterface.addColumn('users', 'new_field', {
type: Sequelize.STRING,
})
const [result] = await queryInterface.sequelize.query(
'SELECT COUNT(*) as count FROM users WHERE new_field IS NULL'
)
if (result[0].count > 0) {
throw new Error('Migration verification failed')
}
await queryInterface.dropTable('users_backup')
} catch (error) {
await queryInterface.sequelize.query('DROP TABLE users')
await queryInterface.sequelize.query('CREATE TABLE users AS SELECT * FROM users_backup')
await queryInterface.dropTable('users_backup')
throw error
}
},
}
Zero-Downtime Migrations
Blue-Green Deployment Strategy
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'email_new', {
type: Sequelize.STRING,
})
},
}
module.exports = {
up: async (queryInterface) => {
await queryInterface.sequelize.query(`
UPDATE users
SET email_new = email
WHERE email_new IS NULL
`)
},
}
module.exports = {
up: async (queryInterface) => {
await queryInterface.removeColumn('users', 'email')
},
}
Cross-Database Migrations
PostgreSQL to MySQL
module.exports = {
up: async (queryInterface, Sequelize) => {
const dialectName = queryInterface.sequelize.getDialect()
if (dialectName === 'mysql') {
await queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
data: {
type: Sequelize.JSON,
},
})
} else if (dialectName === 'postgres') {
await queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
data: {
type: Sequelize.JSONB,
},
})
}
},
}
Resources
- references/orm-switching.md: ORM migration guides
- references/schema-migration.md: Schema transformation patterns
- references/data-transformation.md: Data migration scripts
- references/rollback-strategies.md: Rollback procedures
- assets/schema-migration-template.sql: SQL migration templates
- assets/data-migration-script.py: Data migration utilities
- scripts/test-migration.sh: Migration testing script
Best Practices
- Always Provide Rollback: Every up() needs a down()
- Test Migrations: Test on staging first
- Use Transactions: Atomic migrations when possible
- Backup First: Always backup before migration
- Small Changes: Break into small, incremental steps
- Monitor: Watch for errors during deployment
- Document: Explain why and how
- Idempotent: Migrations should be rerunnable
Common Pitfalls
- Not testing rollback procedures
- Making breaking changes without downtime strategy
- Forgetting to handle NULL values
- Not considering index performance
- Ignoring foreign key constraints
- Migrating too much data at once