Database Migrations

Patterns for managing database migrations with Prisma.

Overview#

Migrations enable safe schema evolution:

  • Version-controlled schema changes
  • Reversible modifications
  • Data transformations
  • Zero-downtime deployments

Prerequisites:

  • Prisma ORM setup
  • Version control (Git)

Implementation#

Development Migration#

1# Create and apply migration 2npx prisma migrate dev --name add_user_role 3 4# Reset database (drops all data!) 5npx prisma migrate reset 6 7# Generate Prisma client only 8npx prisma generate

Production Migration#

# Apply pending migrations npx prisma migrate deploy # Check migration status npx prisma migrate status

Data Migration Script#

1// scripts/migrate-add-slugs.ts 2import { prisma } from '@/lib/db' 3import { slugify } from '@/lib/utils' 4 5async function migrate() { 6 const posts = await prisma.post.findMany({ 7 where: { slug: null } 8 }) 9 10 console.log(`Migrating ${posts.length} posts...`) 11 12 for (const post of posts) { 13 const slug = slugify(post.title) 14 15 await prisma.post.update({ 16 where: { id: post.id }, 17 data: { slug } 18 }) 19 } 20 21 console.log('Migration complete') 22} 23 24migrate() 25 .catch(console.error) 26 .finally(() => prisma.$disconnect())

Batch Data Migration#

1// scripts/migrate-batch.ts 2import { prisma } from '@/lib/db' 3 4const BATCH_SIZE = 100 5 6async function migrate() { 7 let processed = 0 8 let hasMore = true 9 10 while (hasMore) { 11 const users = await prisma.user.findMany({ 12 where: { migratedAt: null }, 13 take: BATCH_SIZE 14 }) 15 16 if (users.length === 0) { 17 hasMore = false 18 continue 19 } 20 21 await prisma.$transaction( 22 users.map((user) => 23 prisma.user.update({ 24 where: { id: user.id }, 25 data: { 26 // Migration logic 27 displayName: user.name || user.email.split('@')[0], 28 migratedAt: new Date() 29 } 30 }) 31 ) 32 ) 33 34 processed += users.length 35 console.log(`Processed ${processed} users...`) 36 } 37 38 console.log(`Migration complete: ${processed} users`) 39} 40 41migrate() 42 .catch(console.error) 43 .finally(() => prisma.$disconnect())

Reversible Migration#

1// scripts/migrate-reversible.ts 2import { prisma } from '@/lib/db' 3 4async function up() { 5 // Add new column with default 6 await prisma.$executeRaw` 7 ALTER TABLE "User" ADD COLUMN "status" TEXT DEFAULT 'ACTIVE' 8 ` 9 10 // Populate based on existing data 11 await prisma.$executeRaw` 12 UPDATE "User" SET "status" = 'INACTIVE' 13 WHERE "lastLoginAt" < NOW() - INTERVAL '90 days' 14 ` 15} 16 17async function down() { 18 await prisma.$executeRaw` 19 ALTER TABLE "User" DROP COLUMN "status" 20 ` 21} 22 23// Run with: npx ts-node scripts/migrate-reversible.ts up 24const direction = process.argv[2] 25 26if (direction === 'up') { 27 up().then(() => console.log('Migration up complete')) 28} else if (direction === 'down') { 29 down().then(() => console.log('Migration down complete')) 30} else { 31 console.log('Usage: npx ts-node script.ts [up|down]') 32}

Safe Schema Changes (Zero-Downtime)#

1// Approach for zero-downtime migrations 2 3// Step 1: Add new column (nullable) 4// schema.prisma: newField String? 5 6// Step 2: Deploy code that writes to both columns 7await prisma.user.update({ 8 where: { id }, 9 data: { 10 oldField: value, 11 newField: value // Write to both 12 } 13}) 14 15// Step 3: Backfill existing data 16// scripts/backfill.ts 17 18// Step 4: Deploy code that reads from new column 19 20// Step 5: Remove old column in schema 21// schema.prisma: remove oldField 22 23// Step 6: Clean up code

Column Rename Pattern#

1// scripts/rename-column.ts 2// Safe column rename without downtime 3 4async function renameColumn() { 5 // Phase 1: Add new column 6 await prisma.$executeRaw` 7 ALTER TABLE "Post" ADD COLUMN "slug" TEXT 8 ` 9 10 // Phase 2: Copy data 11 await prisma.$executeRaw` 12 UPDATE "Post" SET "slug" = "permalink" 13 ` 14 15 // Phase 3: Add constraints (after code deploy) 16 await prisma.$executeRaw` 17 ALTER TABLE "Post" ALTER COLUMN "slug" SET NOT NULL 18 ` 19 20 // Phase 4: Drop old column (after verification) 21 await prisma.$executeRaw` 22 ALTER TABLE "Post" DROP COLUMN "permalink" 23 ` 24}

Migration Verification#

1// scripts/verify-migration.ts 2import { prisma } from '@/lib/db' 3 4async function verify() { 5 // Check all records migrated 6 const unmigrated = await prisma.user.count({ 7 where: { displayName: null } 8 }) 9 10 if (unmigrated > 0) { 11 console.error(`${unmigrated} users not migrated!`) 12 process.exit(1) 13 } 14 15 // Check data integrity 16 const invalid = await prisma.user.count({ 17 where: { 18 displayName: '' 19 } 20 }) 21 22 if (invalid > 0) { 23 console.error(`${invalid} users have empty displayName!`) 24 process.exit(1) 25 } 26 27 console.log('Migration verification passed!') 28} 29 30verify() 31 .catch(console.error) 32 .finally(() => prisma.$disconnect())

CI/CD Integration#

1# .github/workflows/deploy.yml 2name: Deploy 3 4on: 5 push: 6 branches: [main] 7 8jobs: 9 deploy: 10 runs-on: ubuntu-latest 11 12 steps: 13 - uses: actions/checkout@v4 14 15 - name: Setup Node.js 16 uses: actions/setup-node@v4 17 with: 18 node-version: '20' 19 20 - name: Install dependencies 21 run: npm ci 22 23 - name: Generate Prisma Client 24 run: npx prisma generate 25 26 - name: Run migrations 27 run: npx prisma migrate deploy 28 env: 29 DATABASE_URL: ${{ secrets.DATABASE_URL }} 30 31 - name: Deploy application 32 run: npm run deploy

Best Practices#

  1. Always use migrations in production - Never use db push in production
  2. Test migrations locally first - Run against a copy of production data
  3. Make migrations reversible - Plan rollback strategies
  4. Batch large data migrations - Avoid locking tables for long periods
  5. Verify after migration - Check data integrity post-migration