Exercise 2: A Batched Backfill for a Huge Table — Possible Solution ===================================================================== Instead of a migration with one giant UPDATE (which would lock many rows and hold one long transaction), run a script after Release 2 is live. Release 3 then contains no data migration at all — just the verification check. // scripts/backfill-post-status.ts import { prisma } from "../lib/prisma"; import { Prisma } from "../generated/prisma/client"; const BATCH = 5_000; const PAUSE_MS = 200; // give normal traffic room between batches async function main() { const remaining = await prisma.post.count({ where: { status: null } }); console.log(`Posts to backfill: ${remaining}`); let lastId = 0; let done = 0; while (true) { // The next batch of ids still needing a status, in id order const batch = await prisma.post.findMany({ where: { id: { gt: lastId }, status: null }, orderBy: { id: "asc" }, take: BATCH, select: { id: true }, }); if (batch.length === 0) break; const ids = batch.map((p) => p.id); const updated = await prisma.$executeRaw` UPDATE "Post" SET "status" = CASE WHEN "published" THEN 'PUBLISHED'::"PostStatus" ELSE 'DRAFT'::"PostStatus" END WHERE "id" IN (${Prisma.join(ids)}) AND "status" IS NULL `; lastId = ids[ids.length - 1]; done += updated; console.log(`Backfilled ${done} / ${remaining} (up to id ${lastId})`); await new Promise((r) => setTimeout(r, PAUSE_MS)); } console.log("Done. Remaining NULLs:", await prisma.post.count({ where: { status: null } })); } main().finally(() => prisma.$disconnect()); Why it's safe to stop and restart: - Each batch is its own short statement: stopping mid-way loses at most the batch in progress, which simply didn't commit. - The WHERE "status" IS NULL condition means a restart skips rows already done, even though lastId starts again at 0. - Posts created or edited meanwhile are handled by Release 2's dual writes, so they never need backfilling. WHY THIS WORKS AS AN ANSWER ------------------------------ Small batches keep each transaction and its locks short, so normal traffic continues. Walking forward by id (keyset style) avoids rescanning the whole table each time, the pause throttles load on the database, and progress is printed with a known total. Every property that made the single UPDATE safe — re-runnable, skips finished rows — is kept, and the final count confirms nothing was missed.