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 generateProduction Migration#
# Apply pending migrations
npx prisma migrate deploy
# Check migration status
npx prisma migrate statusData 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 codeColumn 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 deployBest Practices#
- Always use migrations in production - Never use
db pushin production - Test migrations locally first - Run against a copy of production data
- Make migrations reversible - Plan rollback strategies
- Batch large data migrations - Avoid locking tables for long periods
- Verify after migration - Check data integrity post-migration
Related Patterns#
- Prisma - Prisma setup and basics
- Seeding - Database seeding
- Transactions - Transaction handling