Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Master database schema and data migrations across ORMs (Sequelize, TypeORM, Prisma), including rollback strategies and zero-downtime deployments.
.claude/skills/database-migration/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-09 | ✗→✓ | ▲ Improved | — | — |
| case-14 | ✗→✗ | = Same ✗ | — | — |
| case-08 | ✗→✗ | = Same ✗ | — | — |
| case-01 | ✗→✗ | = Same ✗ | — | — |
| case-02 | ✗→✗ | = Same ✗ | — | — |
Master database schema and data migrations across ORMs (Sequelize, TypeORM, Prisma), including rollback strategies and zero-downtime deployments.
resources/implementation-playbook.md.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
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// 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
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'); } };
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 }); } };
javascriptmodule.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 }); } };
javascriptmodule.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'); } };
javascriptmodule.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'); } };
javascriptmodule.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; } } };
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'); } };
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 } }); } } };
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.