mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 17:47:16 +00:00
434 lines
10 KiB
Markdown
434 lines
10 KiB
Markdown
---
|
|
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
|
|
|
|
```javascript
|
|
// migrations/20231201-create-users.js
|
|
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");
|
|
},
|
|
};
|
|
|
|
// Run: npx sequelize-cli db:migrate
|
|
// Rollback: npx sequelize-cli db:migrate:undo
|
|
```
|
|
|
|
### TypeORM Migrations
|
|
|
|
```typescript
|
|
// migrations/1701234567-CreateUsers.ts
|
|
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");
|
|
}
|
|
}
|
|
|
|
// Run: npm run typeorm migration:run
|
|
// Rollback: npm run typeorm migration:revert
|
|
```
|
|
|
|
### Prisma Migrations
|
|
|
|
```prisma
|
|
// 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
|
|
|
|
```javascript
|
|
// Safe migration: add column with default
|
|
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)
|
|
|
|
```javascript
|
|
// Step 1: Add new column
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
await queryInterface.addColumn("users", "full_name", {
|
|
type: Sequelize.STRING,
|
|
});
|
|
|
|
// Copy data from old column
|
|
await queryInterface.sequelize.query("UPDATE users SET full_name = name");
|
|
},
|
|
|
|
down: async (queryInterface) => {
|
|
await queryInterface.removeColumn("users", "full_name");
|
|
},
|
|
};
|
|
|
|
// Step 2: Update application to use new column
|
|
|
|
// Step 3: Remove old column
|
|
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
|
|
|
|
```javascript
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
// For large tables, use multi-step approach
|
|
|
|
// 1. Add new column
|
|
await queryInterface.addColumn("users", "age_new", {
|
|
type: Sequelize.INTEGER,
|
|
});
|
|
|
|
// 2. Copy and transform data
|
|
await queryInterface.sequelize.query(`
|
|
UPDATE users
|
|
SET age_new = CAST(age AS INTEGER)
|
|
WHERE age IS NOT NULL
|
|
`);
|
|
|
|
// 3. Drop old column
|
|
await queryInterface.removeColumn("users", "age");
|
|
|
|
// 4. Rename new column
|
|
await queryInterface.renameColumn("users", "age_new", "age");
|
|
},
|
|
|
|
down: async (queryInterface, Sequelize) => {
|
|
await queryInterface.changeColumn("users", "age", {
|
|
type: Sequelize.STRING,
|
|
});
|
|
},
|
|
};
|
|
```
|
|
|
|
## Data Transformations
|
|
|
|
### Complex Data Migration
|
|
|
|
```javascript
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
// Get all records
|
|
const [users] = await queryInterface.sequelize.query(
|
|
"SELECT id, address_string FROM users",
|
|
);
|
|
|
|
// Transform each record
|
|
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(),
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
// Drop old column
|
|
await queryInterface.removeColumn("users", "address_string");
|
|
},
|
|
|
|
down: async (queryInterface, Sequelize) => {
|
|
// Reconstruct original column
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
// Create backup table
|
|
await queryInterface.sequelize.query(
|
|
"CREATE TABLE users_backup AS SELECT * FROM users",
|
|
);
|
|
|
|
try {
|
|
// Perform migration
|
|
await queryInterface.addColumn("users", "new_field", {
|
|
type: Sequelize.STRING,
|
|
});
|
|
|
|
// Verify migration
|
|
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");
|
|
}
|
|
|
|
// Drop backup
|
|
await queryInterface.dropTable("users_backup");
|
|
} catch (error) {
|
|
// Restore from backup
|
|
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
|
|
|
|
```javascript
|
|
// Phase 1: Make changes backward compatible
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
// Add new column (both old and new code can work)
|
|
await queryInterface.addColumn("users", "email_new", {
|
|
type: Sequelize.STRING,
|
|
});
|
|
},
|
|
};
|
|
|
|
// Phase 2: Deploy code that writes to both columns
|
|
|
|
// Phase 3: Backfill data
|
|
module.exports = {
|
|
up: async (queryInterface) => {
|
|
await queryInterface.sequelize.query(`
|
|
UPDATE users
|
|
SET email_new = email
|
|
WHERE email_new IS NULL
|
|
`);
|
|
},
|
|
};
|
|
|
|
// Phase 4: Deploy code that reads from new column
|
|
|
|
// Phase 5: Remove old column
|
|
module.exports = {
|
|
up: async (queryInterface) => {
|
|
await queryInterface.removeColumn("users", "email");
|
|
},
|
|
};
|
|
```
|
|
|
|
## Cross-Database Migrations
|
|
|
|
### PostgreSQL to MySQL
|
|
|
|
```javascript
|
|
// Handle differences
|
|
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, // MySQL JSON type
|
|
},
|
|
});
|
|
} else if (dialectName === "postgres") {
|
|
await queryInterface.createTable("users", {
|
|
id: {
|
|
type: Sequelize.INTEGER,
|
|
primaryKey: true,
|
|
autoIncrement: true,
|
|
},
|
|
data: {
|
|
type: Sequelize.JSONB, // PostgreSQL JSONB type
|
|
},
|
|
});
|
|
}
|
|
},
|
|
};
|
|
```
|
|
|
|
## 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
|
|
|
|
1. **Always Provide Rollback**: Every up() needs a down()
|
|
2. **Test Migrations**: Test on staging first
|
|
3. **Use Transactions**: Atomic migrations when possible
|
|
4. **Backup First**: Always backup before migration
|
|
5. **Small Changes**: Break into small, incremental steps
|
|
6. **Monitor**: Watch for errors during deployment
|
|
7. **Document**: Explain why and how
|
|
8. **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
|