Prisma Migration Assistant
Plan and execute safe Prisma migrations with confidence.
Migration Planning Workflow
// 1. Update schema.prisma model User { id Int @id @default(autoincrement()) email String @unique // NEW: Split name into firstName and lastName firstName String? lastName String? // OLD: name String // Will remove this createdAt DateTime @default(now()) }
// 2. Create migration // npx prisma migrate dev --name split_user_name --create-only
// 3. Review generated SQL // 4. Add data migration // 5. Test migration // 6. Apply to production
Migration Types
- Additive Migration (Safe)
// Adding new optional field - safe! model Product { id Int @id @default(autoincrement()) name String description String? price Float newField String? // NEW - optional, no backfill needed }
Generate migration
npx prisma migrate dev --name add_product_new_field
SQL generated:
ALTER TABLE "Product" ADD COLUMN "newField" TEXT;
- Column Rename (Needs Data Copy)
model User { id Int @id @default(autoincrement()) emailAddr String @unique // Renamed from 'email' }
-- migrations/20240115_rename_email/migration.sql
-- Step 1: Add new column ALTER TABLE "User" ADD COLUMN "emailAddr" TEXT;
-- Step 2: Copy data UPDATE "User" SET "emailAddr" = "email";
-- Step 3: Make new column required ALTER TABLE "User" ALTER COLUMN "emailAddr" SET NOT NULL;
-- Step 4: Add unique constraint CREATE UNIQUE INDEX "User_emailAddr_key" ON "User"("emailAddr");
-- Step 5: Drop old column ALTER TABLE "User" DROP COLUMN "email";
- Data Transformation (Complex)
// Before: Single name field // After: First and last name model User { id Int @id @default(autoincrement()) firstName String lastName String // name String // Removed }
-- migrations/20240115_split_name/migration.sql
-- Step 1: Add new columns ALTER TABLE "User" ADD COLUMN "firstName" TEXT; ALTER TABLE "User" ADD COLUMN "lastName" TEXT;
-- Step 2: Data migration (split name) -- PostgreSQL UPDATE "User" SET "firstName" = SPLIT_PART("name", ' ', 1), "lastName" = CASE WHEN array_length(string_to_array("name", ' '), 1) > 1 THEN array_to_string((string_to_array("name", ' '))[2:], ' ') ELSE '' END WHERE "name" IS NOT NULL;
-- Step 3: Handle NULL values UPDATE "User" SET "firstName" = COALESCE("firstName", ''), "lastName" = COALESCE("lastName", '');
-- Step 4: Make columns required ALTER TABLE "User" ALTER COLUMN "firstName" SET NOT NULL; ALTER TABLE "User" ALTER COLUMN "lastName" SET NOT NULL;
-- Step 5: Drop old column ALTER TABLE "User" DROP COLUMN "name";
- Type Change (Risky)
model Product { id Int @id @default(autoincrement()) price Decimal @db.Decimal(10, 2) // Changed from Float }
-- migrations/20240115_price_to_decimal/migration.sql
-- Step 1: Add new column with correct type ALTER TABLE "Product" ADD COLUMN "price_new" DECIMAL(10,2);
-- Step 2: Copy and convert data UPDATE "Product" SET "price_new" = CAST("price" AS DECIMAL(10,2));
-- Step 3: Drop old column ALTER TABLE "Product" DROP COLUMN "price";
-- Step 4: Rename new column ALTER TABLE "Product" RENAME COLUMN "price_new" TO "price";
-- Step 5: Make NOT NULL if required ALTER TABLE "Product" ALTER COLUMN "price" SET NOT NULL;
Migration Sequencing
Migration Sequence: Add User Roles
Phase 1: Additive (Week 1)
- Add optional
rolefield - Deploy application code that handles NULL roles
- Backfill existing users with default role
Phase 2: Enforcement (Week 2)
- Make
rolefield required - Deploy code that requires role on creation
- Add database constraint
Phase 3: Cleanup (Week 3)
- Remove old permission checking code
- Verify all users have roles
Backfill Strategies
Small Table (< 10k rows)
// scripts/backfill-user-roles.ts import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function backfillUserRoles() { const usersWithoutRoles = await prisma.user.findMany({ where: { role: null }, });
console.log(Backfilling ${usersWithoutRoles.length} users...);
// Single transaction for small dataset await prisma.$transaction( usersWithoutRoles.map((user) => prisma.user.update({ where: { id: user.id }, data: { role: "USER" }, // Default role }) ) );
console.log("✅ Backfill complete"); }
backfillUserRoles();
Large Table (> 10k rows)
// scripts/backfill-large-table.ts async function backfillBatched() { const batchSize = 1000; let processed = 0; let hasMore = true;
while (hasMore) { const batch = await prisma.user.findMany({ where: { role: null }, take: batchSize, select: { id: true }, });
if (batch.length === 0) {
hasMore = false;
break;
}
// Process batch
await prisma.$transaction(
batch.map((user) =>
prisma.user.update({
where: { id: user.id },
data: { role: "USER" },
})
)
);
processed += batch.length;
console.log(`Processed ${processed} users...`);
// Rate limiting
await new Promise((resolve) => setTimeout(resolve, 100));
}
console.log(✅ Backfilled ${processed} users);
}
Rollback Guidance
-- migrations/20240115_add_role/rollback.sql
-- Rollback Step 1: Add back old structure (if needed) ALTER TABLE "User" DROP COLUMN "role";
-- Rollback Step 2: Restore old logic -- (Deploy previous application version)
-- Note: Data loss consideration -- If you backfilled data, document what was lost
Migration Testing
// tests/migrations/split-name.test.ts import { PrismaClient } from "@prisma/client"; import { execSync } from "child_process";
describe("Split name migration", () => { let prisma: PrismaClient;
beforeAll(async () => { // Setup test database execSync("npx prisma migrate deploy", { env: { DATABASE_URL: process.env.TEST_DATABASE_URL }, }); prisma = new PrismaClient(); });
it("should split name correctly", async () => {
// Create user with old schema
await prisma.$executeRaw INSERT INTO "User" (name) VALUES ('John Doe') ;
// Run migration
execSync("npx prisma migrate deploy");
// Verify split
const user = await prisma.user.findFirst();
expect(user?.firstName).toBe("John");
expect(user?.lastName).toBe("Doe");
});
it("should handle single name", async () => {
await prisma.$executeRaw INSERT INTO "User" (name) VALUES ('Madonna') ;
execSync("npx prisma migrate deploy");
const user = await prisma.user.findFirst({
where: { firstName: "Madonna" },
});
expect(user?.lastName).toBe("");
}); });
Pre-Migration Checklist
- Backup database
- Test migration on staging
- Verify data transformation logic
- Check for referential integrity issues
- Estimate migration time
- Plan rollback strategy
- Schedule maintenance window (if needed)
- Notify team of deployment
SQL Preview Script
#!/bin/bash
scripts/preview-migration.sh
echo "🔍 Previewing migration..."
Create migration without applying
npx prisma migrate dev --name "$1" --create-only
Show SQL
echo "" echo "📄 Generated SQL:" echo "==================" cat prisma/migrations/*_$1/migration.sql
Analyze impact
echo "" echo "📊 Impact Analysis:" echo "==================" echo "Tables affected: $(cat prisma/migrations/*_$1/migration.sql | grep -c 'ALTER TABLE')" echo "Rows to update: [Run COUNT query manually]" echo "Estimated time: [Estimate based on table size]"
Best Practices
-
Create migration, don't apply: Use --create-only flag
-
Review SQL carefully: Check generated migration
-
Test on staging: Always test before production
-
Batch large updates: Avoid locking tables
-
Add before removing: Additive migrations first
-
Version application code: Deploy code that handles both schemas
-
Monitor performance: Watch query times during migration
-
Have rollback plan: Document reversal steps
Output Checklist
-
Migration SQL generated and reviewed
-
Data backfill strategy planned
-
Rollback procedure documented
-
Migration sequencing defined
-
Testing plan created
-
Impact analysis completed
-
Staging deployment successful
-
Production deployment scheduled