এই পর্বে যা শিখবেন
- Prisma-র
migrate dev,deploy,status,resolveআরdb pushকখন নিরাপদ, কখন বিপজ্জনক - তৈরি হওয়া SQL আর
_prisma_migrationsটেবিল পড়া, দলে conflict আর drift সামলানো nameথেকেtitleনাম বদল আর একটি কলাম ভাগ করা, পুরোনো ও নতুন অ্যাপ একসাথে চলার সময়- NOT NULL কলাম, ব্যাচে backfill, দীর্ঘ lock আর
CREATE INDEX CONCURRENTLY - Deploy-এর সঠিক ক্রম, rollback-এর বাস্তবতা, idempotent seed আর backup থেকে ফেরা
১ · Schema বদল কেন সংস্করণ ধরে রাখতে হয়
বাজারঘরের পণ্যের টেবিলে একটি নতুন কলাম দরকার। আপনি pgAdmin খুলে হাতে যোগ করলেন, অ্যাপ চলল। এক সপ্তাহ পরে সহকর্মী রহিমার কম্পিউটারে অ্যাপ ভেঙে গেল, কারণ তাঁর ডেটাবেসে কলামটি নেই। Production-এও কেউ যোগ করতে ভুলে গেছেন। এখন কেউ জানে না কোন ডেটাবেসের গঠন কেমন।
চায়ের দোকানের হিসাব খাতার কথা ভাবুন। পুরোনো পাতার লেখা ঘষে বদলালে পরে কেউ বুঝবে না কী হয়েছিল। ভালো দোকানদার নতুন লাইনে লেখেন: “১২ তারিখ, দাম ২ টাকা বাড়ল”। Migration সেই নতুন লাইন: আগের অবস্থা থেকে পরের অবস্থায় যাওয়ার একটি ছোট, তারিখ দেওয়া, ক্রমিক লেখা। সব শাখার খাতা একই লাইনগুলো একই ক্রমে তুললে সব খাতা মিলে যায়।
ডেটাবেসের তত্ত্ব, মানে migration কী আর expand–contract কেন, এসেছে DBMS কোর্সের ORM আর migration পর্বে। এখানে সেটি আর পড়াব না, বরং পর্ব ৩৯-এর মতো একটি ছোট Prisma প্রজেক্টে, সত্যিকারের PostgreSQL-এ চালিয়ে দেখব। বদলগুলো চোখে পড়ার জন্য Product মডেল এখানে সেখানকার চেয়ে সরল। সেই সরলতার জন্য এই lab-এ price পুরো টাকায় (যেমন 420), যদিও বাজারঘরের আসল চুক্তিতে দাম integer পয়সায়, pricePaisa. কোড আর ডেটার একটি মৌলিক পার্থক্য মনে রাখুন: কোড ভুল হলে আগের সংস্করণ আবার চালানো যায়, কিন্তু ডেটাবেসের ভেতরের ডেটা অবস্থা ধরে রাখে। একটি কলাম মুছে ফেললে আগের কোড চালালে ডেটা ফেরে না।
Migration ফাইল তিনটি প্রশ্নের উত্তর দেয়: কী বদলাল (SQL), কোন ক্রমে (নামের সময়চিহ্ন), আর কোথায় চলেছে (ডেটাবেসের ভেতরের হিসাবের টেবিল)। তিনটি মিলেই যেকোনো খালি ডেটাবেস থেকে আজকের গঠন আবার বানানো যায়।
এই পর্বের সব “সত্যিকারের run”: Prisma 7.10.1, PostgreSQL 18.6 (npm embedded-postgres), Node 24, Windows ল্যাপটপ। লেখার সময়ে npm-এর latest ট্যাগে আছে 8.0.0-rc.29, একটি release candidate, আর শেষ stable লাইন 7.10.1. তাই আমরা prisma@7.10.1 নির্দিষ্ট করে বসিয়েছি। খেয়াল করুন, prisma.io-র সংস্করণহীন docs এখন Prisma 8-এর API দেখায়, যেমন db migrate, যা 7-এর migrate deploy-এর বদলি। 7-এর docs আছে /docs/orm/v7/ ঠিকানায়, আমরা সেটিই মিলিয়েছি। আপনিও npx prisma --version দেখে docs পড়ুন। আউটপুটে Windows-এর পথের চিহ্ন আর শুরুর config লাইন ছেঁটে দিয়েছি।
কোথায় পথ বদলেছি। আমাদের lab চলেছে কোনো সত্যিকারের terminal ছাড়া। migrate dev সেখানে সাধারণ বদলে চলেছে, কিন্তু সতর্কতার প্রশ্ন উঠলে “non-interactive” বলে থেমেছে। তখন ফাইল বানিয়েছি migrate diff --script দিয়ে, চালিয়েছি migrate dev বা migrate deploy দিয়ে। আপনার terminal-এ migrate dev নিজেই প্রশ্ন করবে। আর migrate reset আমরা চালাইনি, কারণ দেখবেন অংশ ১০-এ। নতুন শুরুর দরকারে একটি নতুন খালি ডেটাবেস ব্যবহার করেছি।
প্রজেক্টের config-ও আগের দিনের মতো নেই। Prisma 7-এ ডেটাবেসের ঠিকানা schema ফাইলে বসে না, বসে prisma.config.ts-এ, আর seed-এর কমান্ডও এখানেই। পুরোনো কায়দায় url = env("DATABASE_URL") লিখলে 7.10.1 সরাসরি P1012 দেয়। ফাইলটি আমরা হাতে লিখেছি। prisma init দিয়ে শুরু করলে, যেমন পর্ব ৩৯-এ, নাম হয় prisma7.config.ts, কারণ 7.10.0 থেকে Prisma 8-এর পাশাপাশি থাকার জন্য এই নাম এসেছে। এখানে আমরা প্রচলিত পুরোনো নামটি রেখেছি, যা বেশিরভাগ docs আর টিউটোরিয়ালে পাবেন। 7.10.1-এর CLI দুটি নামই খুঁজে নেয়, তবে আগে prisma7.config.* দেখে। তাই একই project-এ দুটি ফাইল রাখবেন না।
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
seed: 'tsx prisma/seed.ts',
},
datasource: {
url: env('DATABASE_URL'),
shadowDatabaseUrl: env('SHADOW_DATABASE_URL'),
},
});২ · Prisma-র পাঁচ কমান্ড — কোনটি কোথায় নিরাপদ
Prisma Migrate-এর কমান্ডগুলো নামে কাছাকাছি, কিন্তু কাজে আলাদা। ভুল জায়গায় ভুল কমান্ড চালানোই বেশিরভাগ দুর্ঘটনার শুরু। আগে টেবিলটি মাথায় রাখুন:
| কমান্ড | কী করে | কোথায় | সতর্কতা |
|---|---|---|---|
migrate dev | schema-র বদল থেকে SQL ফাইল বানায়, চালায় | শুধু dev | drift পেলে পুরো ডেটাবেস reset করতে চায় |
migrate deploy | Git-এ থাকা বাকি ফাইলগুলো চালায়, নতুন ফাইল বানায় না | staging, production, CI | drift ধরে না; বদলানো ফাইল নিয়ে অংশ ৫ দেখুন |
migrate status | ফাইল আর হিসাবের টেবিল মেলায় | সব জায়গায় | পড়ে শুধু, বাকি থাকলে exit code 1; drift ধরে না |
migrate resolve | শুধু হিসাবের টেবিলে “চলেছে” বা “ফেরানো হয়েছে” লেখে | ব্যর্থ deploy-এর পরে | কোনো SQL চালায় না |
db push | schema হুবহু ডেটাবেসে বসায়, ফাইল বা হিসাব রাখে না | খসড়া, prototype | production-এ নয়, ইতিহাস থাকে না |
দুটি সত্যিকারের ফল দেখুন। প্রথমটি production-ধরনের ডেটাবেসে status, তারপর deploy, তারপর আবার deploy. দ্বিতীয়বারে কিছুই হয় না, তাই পাইপলাইনে এটি বারবার চালানো নিরাপদ।
$ npx prisma migrate status
2 migrations found in prisma/migrations
Following migrations have not yet been applied:
20261101174715_init
20261101174717_add_stock
[exit code 1]
$ npx prisma migrate deploy
Applying migration `20261101174715_init`
Applying migration `20261101174717_add_stock`
All migrations have been successfully applied.
$ npx prisma migrate deploy
No pending migrations to apply.
Exit code ১ কাজের জিনিস: CI-তে migrate status দিয়ে “কিছু বাকি আছে কি না” যাচাই করা যায়। আর db push ফাইল রাখে না বলে শুরুর খসড়ায় দ্রুত, কিন্তু দলে বা production-এ তার কোনো হিসাব থাকে না। সেখানে সে ডেটা হারানোর আগে থামে, যদি না কেউ জোর করে --accept-data-loss দেন:
$ npx prisma db push
⚠️ There might be data loss when applying the changes:
• You are about to drop the column `discount` on the `Product` table, which still contains 1 non-null values.
Error: Use the --accept-data-loss flag to ignore the data loss warnings like prisma db push --accept-data-loss
৩ · migrate dev-এর ভেতরে — SQL ফাইল আর হিসাবের টেবিল
ছোট Product মডেলে stock কলাম যোগ করলাম। কমান্ড একটিই, কিন্তু ভেতরে কয়েকটি আলাদা কাজ হয়। নিচে সত্যিকারের run-এর ফাইল আর সারি দিয়ে ধাপগুলো দেখুন।
- schema
.prismaআপনি বদলালেন - Shadow DB
replayইতিহাস মিলিয়ে দেখা - SQL ফাইল
migration.sqlপার্থক্য লেখা হলো - টেবিল
ALTERচলল আর হিসাবে উঠল
model Product {
id Int @id @default(autoincrement())
name String
price Int
stock Int @default(0)
}
একটি খালি সাময়িক ডেটাবেসে পুরোনো ফাইলগুলো ক্রমে চলে:
20261101174715_init/migration.sql
ফল = “ইতিহাস অনুযায়ী গঠন যেমন হওয়ার কথা”
এর সাথে dev ডেটাবেস মেলানো হয় → drift নেই
-- AlterTable ALTER TABLE "Product" ADD COLUMN "stock" INTEGER NOT NULL DEFAULT 0;
migration_name applied_steps finished_at
20261101174715_init 1 17:47:15
20261101174717_add_stock 1 17:47:17
- আপনি schema বদলালেন। এটি শুধু ইচ্ছা, ডেটাবেসে এখনো কিছু হয়নি।
npx prisma migrate dev --name add_stockচালালেন। - ইতিহাস যাচাই। Prisma একটি shadow database-এ পুরোনো ফাইলগুলো চালিয়ে প্রত্যাশিত গঠন বের করে, আর dev ডেটাবেসের সাথে মেলায়। PostgreSQL-এ সে নিজে এটি বানিয়ে নিতে পারে, তাই প্রথম run-এ আলাদা কিছু লাগেনি; ঠিকানা দিতে চাইলে config-এ
shadowDatabaseUrl. - পার্থক্য থেকে SQL. নতুন schema আর ইতিহাসের গঠনের পার্থক্য থেকে একটি নতুন ফোল্ডার আর
migration.sqlতৈরি হলো। নামের সময়চিহ্ন UTC-তে, আর এটিই ক্রম ঠিক করে। - চালানো আর হিসাব। SQL চলল, আর হিসাবের টেবিলে একটি সারি উঠল। Prisma Client কিন্তু নতুন হয়নি। v7-এর CLI reference স্পষ্ট বলে, Prisma 7-এ
migrate devআর নিজে থেকেprisma generateবা seed চালায় না (v7 docs-এর কিছু পুরোনো পাতায় এখনো উল্টো কথা থাকতে পারে)। 7.10.1-এর দুটি আলাদা run-এওgeneratedফোল্ডার তৈরিই হয়নি। তাই অভ্যাস রাখুন: এরপরnpx prisma generateনিজে চালান।
হিসাবের টেবিলের আসল চেহারা, প্রথম migration-এর পরে:
migration_name checksum (প্রথম ১২) started_at finished_at applied_steps_count rolled_back_at
20261101174715_init 235f0213d39c …17:47:15.150 …17:47:15.154 1 null
ভিতরের প্রতিটি ঘরের কাজ আছে। finished_at ফাঁকা মানে চলা শেষ হয়নি বা ব্যর্থ। rolled_back_at ভরা মানে হাতে ফেরানো চিহ্নিত। আর checksum ফাইলের আঙুলের ছাপ, যা দিয়ে ধরা পড়ে কেউ পরে ফাইল বদলেছেন কি না। পুরো ব্যবস্থাটি এত সরল যে ২৫ লাইনে একটি শিক্ষামূলক মডেল বানানো যায়। নিচের Node কোডে ডেটাবেসের জায়গায় একটি সাধারণ object, কিন্তু যুক্তি একই:
const { createHash } = require('node:crypto');
const files = [
{ name: '001_init', sql: 'CREATE TABLE Product (id INT, name TEXT)' },
{ name: '002_add_stock', sql: 'ALTER TABLE Product ADD COLUMN stock INT' },
];
const db = { ran: [], migrations: [] }; // ডেটাবেসের নকল
const hash = (s) => createHash('sha256').update(s).digest('hex').slice(0, 8);
function migrate() {
let applied = 0;
for (const f of files) {
const row = db.migrations.find((m) => m.name === f.name);
if (row) {
if (row.checksum !== hash(f.sql)) throw new Error(f.name + ' changed after apply');
continue; // আগেই চলেছে
}
db.ran.push(f.sql);
db.migrations.push({ name: f.name, checksum: hash(f.sql) });
applied++;
}
console.log('applied', applied, '| total', db.migrations.length);
}
migrate();
migrate();
files.push({ name: '003_add_brand', sql: 'ALTER TABLE Product ADD COLUMN brand TEXT' });
migrate();
files[0].sql += ' -- পরে বদলানো';
try { migrate(); } catch (e) { console.log('ERROR:', e.message); }
console.log(db.ran.length, 'statements ran');আউটপুট:
applied 2 | total 2
applied 0 | total 2
applied 1 | total 3
ERROR: 001_init changed after apply
3 statements ran
দ্বিতীয় ডাকে কিছুই চলল না, কারণ দুটি নামই হিসাবে আছে। নতুন ফাইল এলে শুধু সেটি চলল। আর পুরোনো ফাইল বদলালে checksum মিলল না। এটি শেখানোর মডেল, Prisma-র কোড নয়। একটি জায়গায় মডেলটি আসল টুলের চেয়ে কড়া, সেটি পরের অংশেই দেখবেন।
৪ · দলে migration — commit, conflict আর drift
Migration ফোল্ডার আর migration_lock.toml Git-এ commit করতে হয়। এগুলোই দলের সবার সাধারণ ইতিহাস। .env commit হবে না। দুজন একসাথে কাজ করলে কী হয়, একটি আলাদা scratch repo-তে সত্যিই করে দেখলাম। রহিম brand, করিম weight যোগ করলেন, প্রথমে করিমেরটি production-এ গেল।
$ git merge rahim/brand
Auto-merging prisma/schema.prisma
CONFLICT (content): Merge conflict in prisma/schema.prisma
Automatic merge failed; fix conflicts and then commit the result.
model Product {
…
stock Int @default(0)
<<<<<<< HEAD
weight Int?
=======
brand String?
>>>>>>> rahim/brand
}
Conflict হলো schema.prisma-য়, migration ফাইলে নয়, কারণ ফোল্ডার দুটির নাম আলাদা। দুটি লাইনই রেখে merge শেষ করলাম। এবার মজার দৃশ্য: রহিমের ফাইলের সময়চিহ্ন করিমেরটির আগের, অথচ production-এ করিমেরটি আগেই চলে গেছে। এরপরও deploy বাকি ফাইলটি চালাল:
migration_name started_at
20261101175005_init 17:50:12.628
20261101175010_add_weight 17:50:12.635
20261101175007_add_brand 17:50:23.335 ← নামে আগে, চলেছে পরে
এখানে দুই বদল আলাদা কলামে, তাই ক্রম উল্টে গেলেও ক্ষতি হয়নি, পরে migrate diff “No difference detected” বলল। কিন্তু দুজন একই কলাম যোগ করলে পরেরটি column already exists-এ থামবে, আর একটির ওপর অন্যটি নির্ভর করলে ক্রম উল্টে SQL ভাঙতে পারে। Merge-এর পরে তাই খালি ডেটাবেসে পুরো ইতিহাস একবার চালিয়ে দেখা ভালো অভ্যাস। Git-এর দিকটি আছে Git branching পর্বে।
Drift — খাতার বাইরে বদল
গভীর রাতে কেউ production-এ হাতে একটি কলাম যোগ করলেন। হিসাবের সাথে আসল গঠন আর মিলছে না, এর নাম schema drift. ডেটাবেসে হাতে ALTER TABLE "Product" ADD COLUMN "discount" INTEGER চালিয়ে তিনটি কমান্ড দিলাম:
$ npx prisma migrate status
2 migrations found in prisma/migrations
Database schema is up to date! ← drift ধরল না
$ npx prisma migrate dev --name noop
Drift detected: Your database schema is not in sync with your migration history.
[*] Changed the `Product` table
[+] Added column `discount`
We need to reset the "public" schema at "127.0.0.1:38741"
You may use prisma migrate reset to drop the development database.
All data will be lost.
$ npx prisma migrate diff --from-migrations prisma/migrations \
--to-config-datasource --exit-code
[*] Changed the `Product` table
[+] Added column `discount`
[exit code 2]
এখান থেকে তিনটি শিক্ষা। status শুধু ফাইল আর হিসাবের টেবিল মেলায়, আসল গঠন দেখে না। migrate dev ধরে, কিন্তু তার সমাধান reset, যা dev-এ চলে, production-এ কখনো নয়। আর production-এ drift খুঁজতে migrate diff --exit-code সবচেয়ে সৎ: পার্থক্য থাকলে exit code ২. এটি পড়ে শুধু, লেখে না, তবে shadow database-এর ঠিকানা লাগে।
Hotfix-কে ইতিহাসে তোলা। Production-এ বদলটি রেখেই খাতা মেলাতে হলে migrate diff --from-migrations … --to-schema … --script দিয়ে একই বদলের ফাইল বানান, dev-এ migrate dev চালান, আর production-এ SQL না চালিয়ে শুধু লিখে রাখুন: migrate resolve --applied 20261101180000_add_discount. আমাদের run-এ সেখানে সারিটি উঠল applied_steps_count = 0 নিয়ে, মানে SQL চলেনি, শুধু হিসাব হয়েছে।
৫ · হাতে সম্পাদনা, ধ্বংসাত্মক বদল আর ব্যর্থ migration
Prisma SQL বানায় পার্থক্য দেখে, আপনার মনের ইচ্ছা দেখে নয়। name কলামের নাম title করলাম। Prisma এটিকে নাম বদল ভাবল না, ভাবল একটি মুছে আরেকটি যোগ:
-- AlterTable
ALTER TABLE "Product" DROP COLUMN "name",
ADD COLUMN "title" TEXT NOT NULL;
$ npx prisma migrate dev --name rename_title
⚠️ We found changes that cannot be executed:
• Step 0 Added the required column `title` to the `Product` table without a default value.
There are 3 rows in this table, it is not possible to execute this step.
You can use prisma migrate dev --create-only to create the migration file, and manually modify it
to address the underlying issue(s).
এই SQL চলতে পারলে ৩টি পণ্যের নাম চিরতরে যেত। এটিই destructive change. কলাম মুছলে Prisma আরেকটি সতর্কতা দেয়:
⚠️ Warnings for the current datasource:
• You are about to drop the column `discount` on the `Product` table, which still contains 1 non-null values.
পথ হলো --create-only: ফাইল বানায়, চালায় না। আপনি খুলে হাতে ঠিক করেন, তারপর চালান। আমাদের terminal-ছাড়া lab-এ শুধু-সতর্কতার ক্ষেত্রে এটিও “non-interactive” বলে থেমেছে, তখন একই ফাইল এসেছে migrate diff --script থেকে। মূল নিয়মটি স্পষ্ট: যে ফাইল এখনো কোথাও চলেনি, সেটি সম্পাদনা করা যায়; যেটি চলেছে, সেটি আর ছোঁবেন না। চলে যাওয়া ফাইলে শুধু একটি comment যোগ করলে কী হয়, দেখুন:
$ npx prisma migrate dev --name check2
The migration `20261101180000_add_discount` was modified after it was applied.
We need to reset the "public" schema at "127.0.0.1:38741"
$ npx prisma migrate deploy # production-ধরনের ডেটাবেসে
No pending migrations to apply. ← কিছুই বলল না
আগে যে কড়া মডেলের কথা বলেছিলাম, এই তার পার্থক্য। v7 docs বলছে deploy এমন ফাইলে একটি WARNING দেয়, কিন্তু 7.10.1-এ আলাদা একটি পরীক্ষাতেও কোনো সতর্কতা ছাপা হয়নি, বাকি migration থাকলেও না। তাই এর ওপর ভরসা করবেন না। ধরার ভরসা migrate dev, code review আর CI-এর যাচাই।
ব্যর্থ migration আর migrate resolve
৩টি সারি আছে এমন টেবিলে "sku" TEXT NOT NULL চালাতে গেলে ফল এমন:
$ npx prisma migrate deploy
Applying migration `20261101181000_add_sku`
Error: P3018
A migration failed to apply. New migrations cannot be applied before the error is recovered from.
Migration name: 20261101181000_add_sku
Database error code: 23502
ERROR: column "sku" of relation "Product" contains null values
$ npx prisma migrate deploy
Error: P3009
migrate found failed migrations in the target database, new migrations will not be applied.
$ npx prisma migrate resolve --rolled-back 20261101181000_add_sku
Migration 20261101181000_add_sku marked as rolled back.
ব্যর্থ ফাইল পরের সব migration আটকে দেয়, যতক্ষণ না আপনি জানান কী হলো। --rolled-back মানে “আমি নিশ্চিত, এর ফল ডেটাবেসে নেই”। তারপর ফাইল ঠিক করলাম তিন ধাপে, আবার deploy চলল। হিসাবের টেবিলে একই নামের দুটি সারি থেকে গেল, প্রথমটি ফেরানো, দ্বিতীয়টি সফল। এটি ইতিহাসই, ভুল নয়।
ALTER TABLE "Product" ADD COLUMN "sku" TEXT;
UPDATE "Product" SET "sku" = 'BG-' || "id" WHERE "sku" IS NULL;
ALTER TABLE "Product" ALTER COLUMN "sku" SET NOT NULL;একটি ফাঁদ, যা আমরা run করে পেয়েছি। PostgreSQL-এ DDL transaction-এর ভেতরে চলতে পারে, তাই অনেকে ধরে নেন ব্যর্থ migration পুরোটা ফিরে যাবে। Prisma 7.10.1-এ তা হলো না। তিন statement-এর ফাইলে তৃতীয়টি ব্যর্থ হলে প্রথম দুটি থেকে গেল: brand কলাম রইল, আর সব price হলো 0. তখন --rolled-back বলার আগে ডেটা হাতে ঠিক করতে হয়। ফাইলের শুরুতে BEGIN; আর শেষে COMMIT; লিখে আবার চালালাম: এবার দাম ৪২০ আর ১৩৫-ই রইল, তবে error ছিল কম স্পষ্ট (current transaction is aborted). v7 docs-ও বলছে migration আংশিক চলতে পারে, আর পরামর্শ দিচ্ছে ধাপগুলো idempotent রাখা (IF NOT EXISTS) বা হাতে ফেরানো। এক ফাইলে একাধিক বদল থাকলে নিজে পরীক্ষা করে নিন।
৬ · Expand and contract — পুরোনো আর নতুন অ্যাপ একসাথে
Rolling deployment-এ (deployment কৌশল পর্ব) সার্ভার একটি একটি করে বদলায়। কয়েক মিনিট পুরোনো V1 আর নতুন V2 একই ডেটাবেসে চলে। তাই প্রতিটি schema বদলকে দুই সংস্করণের সাথেই চলতে হবে। এক ধাপে নাম বদলালে কী হয়, সত্যিকারের Prisma Client দিয়ে দেখুন:
ALTER TABLE "Product" RENAME COLUMN "name" TO "title";
V1 findMany ERROR P2022 The column `Product.name` does not exist in the current database.
V1 create ERROR P2022 The column `name of relation Product` does not exist in the current database.
ডেটা একটুও হারায়নি, তবু পুরোনো অ্যাপের পণ্যের পাতা বন্ধ। Zero-downtime চাইলে expand and contract লাগবে।
পুরোনো রাস্তা ভাঙার আগে পাশে নতুন রাস্তা বানানো হয়। কিছুদিন রিকশা দুই রাস্তাতেই চলে, ট্রাফিক পুলিশ ধীরে ধীরে সবাইকে নতুনটিতে পাঠান। কেউ আর পুরোনোটিতে নেই নিশ্চিত হলে তবেই ভাঙা হয়। মাঝখানের ওই সময়টিই পুরো কৌশলের মূল।
নিচের stepper আমাদের real run. দুটি আলাদা generated client, V1 আর V2, একই PostgreSQL টেবিলে একসাথে পড়ছে আর লিখছে। V2-এর schema-য় name String? @ignore আছে, যাতে V2 পুরোনো কলাম SELECT-ই না করে। এই লাইনটি না থাকলে contract-এর পরে V2-ও ভেঙে যেত।
| চলছে | |
| trigger | |
| title | |
| name |
- শুরু। শুধু V1 চলছে। টেবিলে
nameআছে, সবই NOT NULL. - M1, expand. nullable
titleযোগ হলো,name-এর NOT NULL উঠল, আর একটি trigger বসল, যে যেকোনো একটি কলামে লেখা অন্যটিতে কপি করে। V1 টের পায়নি: Prisma SELECT-এ কলামের নাম ধরে পড়ে, তাই নতুন কলাম তার ক্ষতি করে না। - V1 লিখতে থাকে। V1 শুধু
nameলেখে। নতুন সারি ৬ আর বদলানো সারি ২-এ triggertitleভরে দিল। ছোঁয়া হয়নি এমন সারি ১-এtitleএখনো null. - M2, backfill. পুরোনো সারিগুলো ব্যাচে ভরা হলো। পরের অংশে এই ধাপটি আলাদা করে দেখবেন। এখন null নেই।
- M3, title NOT NULL. আগে CHECK, তারপর VALIDATE, তারপর SET NOT NULL, যাতে টেবিল আটকে পুরো scan না হয়। V1 তবু লিখতে পারে, কারণ trigger
titleভরে দেয়। - V2 আসছে, দুজন একসাথে। V2-র লেখা “চিনি” V1 পড়ল
nameহিসেবে, V1-এর লেখা “লবণ” V2 পড়লtitleহিসেবে। V2 সারি ১-এর নাম বদলালে V1-ও নতুন নাম দেখল। এটিই সবচেয়ে গুরুত্বপূর্ণ ধাপ। - V1 বিদায়। সব সার্ভারে V2. Rollback-এর দরকার নেই নিশ্চিত হতে কিছুদিন অপেক্ষা করা ভালো। এই সময়ে V1-এ ফেরা সম্ভব, কারণ
nameএখনো হালনাগাদ। - M4, contract. trigger, ফাংশন আর
nameমোছা হলো। V2 চলছে:findManyঠিক, নতুন create-এ id 9. এই ধাপের পরে V1 চালালেP2022, তাই এটিই ফেরার পথহীন ধাপ, আর এখান থেকে পেছনে যেতে হলে backup লাগবে।
চারটি migration ফাইলই হাতে লেখা, চালানো হয়েছে migrate deploy দিয়ে। M1-এর trigger-টি PostgreSQL-এর:
BEGIN;
ALTER TABLE "Product" ADD COLUMN "title" TEXT;
ALTER TABLE "Product" ALTER COLUMN "name" DROP NOT NULL;
CREATE FUNCTION product_sync_title() RETURNS trigger AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW."title" := COALESCE(NEW."title", NEW."name");
NEW."name" := COALESCE(NEW."name", NEW."title");
ELSIF NEW."name" IS DISTINCT FROM OLD."name" THEN
NEW."title" := NEW."name";
ELSIF NEW."title" IS DISTINCT FROM OLD."title" THEN
NEW."name" := NEW."title";
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER product_sync_title_trg
BEFORE INSERT OR UPDATE ON "Product"
FOR EACH ROW EXECUTE FUNCTION product_sync_title();
COMMIT;শেষে যাচাই: V3 schema-র সাথে migrate diff --exit-code বলল “No difference detected.”. Trigger ছাড়া আরেকটি পথ হলো V2 কোড নিজেই দুই কলামে লেখা (dual write), তবে তখন V1-এর লেখা সারিতে title ফাঁকা থাকবে, তাই V1 বন্ধের পরে আরেকবার backfill লাগে। এবার পুরো ধারণাটি পাতায় নিজে চালান। এখানে ইঞ্জিন SQLite: trigger-এর ভাষা ভিন্ন, আর ALTER COLUMN … SET NOT NULL SQLite-এ নেই, তাই ওই ধাপটি বাদ।
CREATE TABLE Product (id INTEGER PRIMARY KEY, name TEXT, price INTEGER);
INSERT INTO Product (name, price) VALUES ('চাল', 420), ('ডাল', 135);
-- expand: নতুন কলাম, পুরোনো অ্যাপ টের পায় না
ALTER TABLE Product ADD COLUMN title TEXT;
CREATE TRIGGER sync_ins AFTER INSERT ON Product
WHEN NEW.title IS NULL OR NEW.name IS NULL
BEGIN
UPDATE Product SET title = COALESCE(NEW.title, NEW.name),
name = COALESCE(NEW.name, NEW.title)
WHERE id = NEW.id;
END;
-- দুই সংস্করণ একসাথে: V1 লেখে name, V2 লেখে title
INSERT INTO Product (name, price) VALUES ('তেল', 190);
INSERT INTO Product (title, price) VALUES ('চিনি', 150);
-- backfill: পুরোনো সারি
UPDATE Product SET title = name WHERE title IS NULL;
SELECT id, name, title FROM Product;
-- contract: এখন কেউ name পড়ে না
DROP TRIGGER sync_ins;
ALTER TABLE Product DROP COLUMN name;
SELECT id, title, price FROM Product;প্রথম SELECT-এ ৪টি সারির name আর title মিলে গেছে: 1 চাল চাল, 2 ডাল ডাল, 3 তেল তেল, 4 চিনি চিনি. UPDATE-এ বদলেছে মাত্র ২টি সারি, কারণ নতুন দুটি trigger আগেই ভরেছিল। শেষ SELECT-এ কলাম শুধু id, title, price. একটি সীমা মনে রাখুন: এই ছোট trigger শুধু INSERT ধরে, UPDATE ধরে না। আসল PostgreSQL ফাইলে দুটোই ধরা হয়েছে।
একটি কলাম ভাগ করা — ঠিকানা থেকে area আর city
নাম বদলে তথ্য একই থাকে। ভাগ করায় পুরোনো ডেটাকে বুঝতে হয়, আর বাস্তবের ডেটা অগোছালো। fullAddress থেকে area আর city করার সময় শেষ কমা ধরে backfill চালালাম, একই expand ধাপে। PostgreSQL-এর real ফল:
backfilled 3
id fullAddress area city
1 মিরপুর ১০, ঢাকা মিরপুর ১০ ঢাকা
2 আগ্রাবাদ, চট্টগ্রাম আগ্রাবাদ চট্টগ্রাম
3 জিন্দাবাজার সিলেট null null
4 বাড়ি ৫, রোড ২, ধানমন্ডি, ঢাকা বাড়ি ৫, রোড ২, ধানমন্ডি ঢাকা
৪টির মধ্যে ১টি বোঝা গেল না, কারণ কমা নেই। অনুমান করে ভাগ না করে এমন সারি আলাদা তালিকায় তুলে মানুষ দিয়ে বা গ্রাহকের কাছ থেকে ঠিক করানোই সৎ পথ। তাই NOT NULL আর contract আসবে এই সংখ্যা ০ হওয়ার পরে। Trigger দুই দিকেই কাজ করল: পুরোনো অ্যাপের নিউ মার্কেট, খুলনা ভেঙে area, city হলো, আর নতুন অ্যাপের কাজলা, রাজশাহী জুড়ে কাজলা, রাজশাহী হলো।
৭ · NOT NULL, backfill, lock আর CONCURRENTLY
Expand and contract ঠিক থাকলেও একটি বদল সাইট থামিয়ে দিতে পারে, যদি সেটি বড় বা ভুল সময়ে হয়। কারণ lock. PostgreSQL-এ বেশিরভাগ ALTER TABLE নেয় ACCESS EXCLUSIVE lock, যা সাধারণ SELECT-ও আটকায়। Lock-এর তত্ত্ব আছে concurrency control পর্বে। ভয়ের জায়গাটি হলো সারি: ALTER নিজে এক মিলিসেকেন্ডের কাজ হলেও সে অপেক্ষায় দাঁড়ালে পেছনের সবাই তার পেছনে দাঁড়ায়। ১০ লাখ সারির টেবিলে তিনটি সংযোগ দিয়ে এটি সত্যিই ঘটালাম।
| S1 দীর্ঘ transaction | |
| S2 ALTER TABLE | |
| S3 সাধারণ SELECT | |
| Lock |
- S1 ভুলে transaction খোলা রাখল। একটি মাত্র SELECT, কিন্তু COMMIT হয়নি। এমন হয় কোনো ধীর রিপোর্টে, বা
BEGIN-এর পরে বাইরের API ডাকলে। S1 টেবিলে হালকা ACCESS SHARE lock ধরে আছে। - S2 ALTER চাইল। তার দরকার সবচেয়ে কড়া lock, যা S1-এর সাথে মেলে না। সে অপেক্ষা করছে। এখনো কেউ বিপদ টের পায়নি।
- S3 একটি সাধারণ SELECT. S1-এর সাথে S3-এর কোনো বিরোধ নেই, তবু S3 আটকে গেল:
blocked_byবলছে S2. Lock-এর সারিতে S2 তার সামনে, তাই পরের সবাই পেছনে দাঁড়ায়। এই মুহূর্তে পণ্যের পাতা বন্ধ। - S1 শেষ করতেই সব ছাড়া পেল। ১ মিলিসেকেন্ডের ব্যবধানে ALTER, তার পরের মিলিসেকেন্ডে SELECT. ALTER নিজে সময় নেয়নি। ২ সেকেন্ডের পুরো অচলাবস্থা এসেছে অপেক্ষা থেকে।
ওষুধ lock_timeout: “এত সময়ে lock না পেলে হাল ছাড়ো”। আমাদের run-এ SET lock_timeout = '2s' দিয়ে ২০১৬ ms পরে ALTER ব্যর্থ হলো: 55P03 canceling statement due to lock timeout. ব্যর্থ হওয়া এখানে ভালো ফল। সাইট চলতে থাকে, পরে আবার চেষ্টা করা যায়।
একটি হিসাব (ধরে নেওয়া সংখ্যা)। পণ্যের টেবিলে সেকেন্ডে ২০০টি অনুরোধ আসে, connection pool-এ ২০টি সংযোগ। ALTER একটি ৩০ সেকেন্ডের রিপোর্টের পেছনে দাঁড়াল। Pool ফুরাবে ২০ ÷ ২০০ = ০.১ সেকেন্ডে। ৩০ সেকেন্ডে জমবে ২০০ × ৩০ = ৬,০০০টি অনুরোধ। lock_timeout = 2s থাকলে আটকে থাকা অনুরোধ সর্বোচ্চ ২০০ × ২ = ৪০০টি, আর বিরতি ২ সেকেন্ডের বেশি নয়।
কোন বদল কতটা ভারী — এক run-এর মাপ
১০ লাখ সারির (৮৭ MB) টেবিলে কাজ চলার সময় পাশে একটি লেখক প্রতি ৫ ms-এ একটি ছোট UPDATE পাঠাচ্ছিল। শেষ ঘরটি সেই UPDATE-এর সবচেয়ে বেশি অপেক্ষা। এটি একটি ল্যাপটপে একবারের মাপ, সাধারণ নিয়ম নয়। তুলনাটুকুই দেখার।
| কাজ | সময় | লেখার সর্বোচ্চ অপেক্ষা |
|---|---|---|
| nullable কলাম যোগ | 0 ms | মাপা হয়নি |
NOT NULL DEFAULT 'pcs' কলাম যোগ | 1 ms | মাপা হয়নি |
int থেকে bigint (পুরো টেবিল নতুন করে লেখা) | 1233 ms | 1218 ms |
সরাসরি SET NOT NULL | 110 ms | 111 ms |
CHECK NOT VALID + VALIDATE + SET NOT NULL | 1 + 36 + 0 ms | 2 ms |
CREATE INDEX | 766 ms | 757 ms |
CREATE INDEX CONCURRENTLY | 654 ms | 1 ms |
এক UPDATE-এ ১০ লাখ সারি | 8192 ms | 8157 ms |
| ১০০ ব্যাচে ১০ লাখ সারি | 15460 ms | 99 ms |
প্রথম দুই সারি দেখায় কেন PostgreSQL ১১-এর পরে স্থির DEFAULT-সহ কলাম যোগ এত দ্রুত: মানটি টেবিলের metadata-য় থাকে, সারিগুলো নতুন করে লেখা হয় না। আর NOT NULL-এর সারি দেখায় নিরাপদ পথটি: VALIDATE হালকা lock নেয়, আর বৈধ CHECK থাকলে SET NOT NULL scan করে না। ব্যাচে মোট সময় প্রায় দ্বিগুণ, কিন্তু লেখার অপেক্ষা ৮ সেকেন্ড থেকে ১ সেকেন্ডের নিচে। এই দামটিই আমরা দিই।
NOT NULL কলাম, ব্যাচে backfill
নতুন slug কলামের মান প্রতিটি সারির জন্য আলাদা, তাই সহজ DEFAULT চলে না। ধাপ: nullable যোগ, অ্যাপ নতুন সারিতে লেখা শুরু, পুরোনো সারি ব্যাচে ভরা, তারপর NOT NULL. বড় টেবিলের backfill migration ফাইলে রাখবেন না, কারণ deploy ততক্ষণ আটকে থাকবে। এটি আলাদা script. নিচেরটি Prisma-র $queryRaw দিয়ে সত্যিই চলেছে, parameter সহ, তাই SQL injection-এর ভয় নেই:
const BATCH = 10_000;
let lastId = 0n;
let total = 0;
for (;;) {
const rows = await prisma.$queryRaw<{ id: bigint }[]>`
WITH batch AS (
SELECT id FROM products
WHERE id > ${lastId} AND slug2 IS NULL
ORDER BY id LIMIT ${BATCH}
)
UPDATE products p SET slug2 = 'p-' || p.id
FROM batch WHERE p.id = batch.id
RETURNING p.id`;
if (rows.length === 0) break;
lastId = rows.reduce((m, r) => (r.id > m ? r.id : m), lastId);
total += rows.length;
await sleep(20); // অন্য কাজকে জায়গা দেওয়া
}batch 25: total 250000, lastId 250000
batch 50: total 500000, lastId 500000
batch 75: total 750000, lastId 750000
batch 100: total 1000000, lastId 1000000
done: 1000000 rows in 100 batches
প্রতিটি ব্যাচ নিজের transaction, তাই lock দ্রুত ছাড়া পায়। slug2 IS NULL শর্ত থাকায় script মাঝপথে থামলে আবার চালানো নিরাপদ। ছোট একটি টেবিলে ব্যাচের হাঁটা দেখুন, expand and contract-এর real run থেকে নেওয়া:
| lastId | |
| এই ব্যাচের id | |
| বাকি null |
- শুরু। ৬টি সারির ৪টিতে
titlenull. সারি ২ আর ৬ trigger আগেই ভরেছে, তাই backfill সেগুলো ছোঁবে না। - ব্যাচ ১.
id > 0আর null, এমন প্রথম ২টি: id 1 আর 3. COMMIT হয়ে lock ছাড়া পেল,lastIdহলো 3, বাকি ২. - ব্যাচ ২.
id > 3: id 4 আর 5.lastId5, বাকি ০. একটু বিরতির মধ্যে সাইটের অন্য লেখা চলতে পারে। - খালি ব্যাচ, শেষ। আর কোনো সারি নেই, লুপ থামল। এখন NOT NULL বসানো নিরাপদ, তবে আগে
SELECT count(*) … IS NULLদিয়ে নিজে যাচাই করুন।
একই কৌশল পাতায় চালান। SQLite-এ লুপ নেই, তাই প্রতিটি ব্যাচ আলাদা UPDATE:
-- ১০টি সারি, ব্যাচের আকার ৪
UPDATE Product SET slug = 'p-' || id
WHERE id IN (SELECT id FROM Product WHERE slug IS NULL ORDER BY id LIMIT 4);
SELECT count(*) AS still_null FROM Product WHERE slug IS NULL;
UPDATE Product SET slug = 'p-' || id
WHERE id IN (SELECT id FROM Product WHERE slug IS NULL ORDER BY id LIMIT 4);
SELECT count(*) AS still_null FROM Product WHERE slug IS NULL;
UPDATE Product SET slug = 'p-' || id
WHERE id IN (SELECT id FROM Product WHERE slug IS NULL ORDER BY id LIMIT 4);
SELECT count(*) AS still_null FROM Product WHERE slug IS NULL;
SELECT id, slug FROM Product WHERE id IN (1, 10);still_null ধাপে ধাপে ৬, ২, ০. তৃতীয় UPDATE-এ বদলেছে ২টি সারি, যা জানিয়ে দেয় আর কিছু বাকি নেই। শেষ SELECT: 1 p-1, 10 p-10. SQLite-এ এক লেখকই থাকে, তাই এখানে lock-এর সুফল দেখা যাবে না। সেটি উপরের PostgreSQL মাপে দেখেছেন।
CREATE INDEX CONCURRENTLY — সত্যিকারের আচরণ
সাধারণ CREATE INDEX লেখা আটকায় (পড়া নয়)। উপরের টেবিলে তাই ৭৫৭ ms অপেক্ষা। CONCURRENTLY লেখা আটকায় না, একই সময়ে ৪৪টি UPDATE চলেছে, আগেরটিতে মাত্র ২টি। সাধারণত এটি বেশি সময় নেয়, আমাদের এই একটি run-এ তফাত ছিল না। এর দুটি শর্ত real run-এ দেখুন:
BEGIN; CREATE INDEX CONCURRENTLY idx_c ON products (name);
25001 CREATE INDEX CONCURRENTLY cannot run inside a transaction block
CREATE UNIQUE INDEX CONCURRENTLY idx_u ON products (name);
23505 could not create unique index "idx_u" Key (name)=(ডুপ্লিকেট) is duplicated.
relname indisvalid
products_pkey true
idx_b true
idx_u false ← ব্যর্থ, কিন্তু পড়ে আছে
ব্যর্থ হলে সে একটি invalid index রেখে যায়। Query এটি ব্যবহার করে না, অথচ লেখার খরচ থাকতে পারে। PostgreSQL docs-এর পরামর্শ: DROP INDEX করে আবার চেষ্টা। Prisma-য় একে নিজের ফাইলে রাখুন, একটিমাত্র statement, কোনো BEGIN নয়। schema-তে @@index লিখে index-এর নাম মিলিয়ে দিন, নইলে drift হবে। আমাদের run-এ একা ফাইলে থাকা CREATE INDEX CONCURRENTLY "Product_cat_idx" ON "Product"("cat"); deploy-এ চলেছে।
৮ · Deploy-এর ক্রম, rollback আর backup
Migration চলবে একবার, অ্যাপের আগে, একটি আলাদা ধাপে। “আগে”, কারণ expand করা schema-য় পুরোনো অ্যাপ চলে, কিন্তু পুরোনো schema-য় নতুন অ্যাপ চলে না। “একবার”, কারণ ১০টি সার্ভার চালু হওয়ার সময় ১০ বার চালানো অর্থহীন আর ঝুঁকির।
একটি নিরাপদ release-এর ক্রম
Migration runs once, before the new app version
দুটি প্রক্রিয়া ভুলে একসাথে deploy চালালে কী হয়? Prisma একটি advisory lock ধরে। ৩ সেকেন্ডের migration-এ দ্বিতীয়টি অপেক্ষা করে “No pending migrations to apply.” বলল, আর একটি migration একবারই চলল। ১৪ সেকেন্ডের migration-এ দ্বিতীয়টি হাল ছাড়ল। v7 docs অনুযায়ী এই ১০ সেকেন্ডের সীমা বদলানো যায় না, আবার চালাতে হয়:
Error: P1002
The database server was reached but timed out.
Context: Timed out trying to acquire a postgres advisory lock
(SELECT pg_advisory_lock(72707369)). Timeout: 10000ms.
অর্থাৎ lock দুর্ঘটনা থামায়, কিন্তু অ্যাপ চালুর সময় প্রতিটি server থেকে migration চালানোকে ভালো নকশা বানায় না। এটি পাইপলাইনের একটি আলাদা ধাপ। আরও আছে পর্ব ৮১-তে আর pipeline নকশা পর্বে।
Rollback — সামনে সারানো বনাম down migration
7.10.1-এ migrate down বলে কোনো কমান্ড নেই। উল্টো SQL দরকার হলে বানানো যায়, কিন্তু কী পাওয়া যায় দেখুন। discount কলাম মোছার উল্টো script:
$ npx prisma migrate diff --from-schema new.prisma --to-schema old.prisma --script
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "discount" INTEGER;
কলাম ফিরল, কিন্তু খালি। Down migration গঠন ফেরায়, ডেটা নয়। তা ছাড়া migration-এর পরে নতুন অ্যাপ যে ডেটা লিখেছে, সেটিও সে জানে না। তাই বাস্তবের অভ্যাস roll forward: ভুল দেখলে আরেকটি নতুন migration. Expand and contract এটি সহজ করে দেয়। contract-এর আগ পর্যন্ত অ্যাপের কোড পেছনে নিলেই হয়, schema দুই দিকেই চলে। দ্রুত ব্যবহারের নিয়মটি হলো: কোড ফেরান, schema সামনে সারান, ডেটা ফিরবে শুধু backup থেকে।
Backup — restore না করলে সেটি আশা মাত্র
embedded-postgres-এ pg_dump নেই, তাই PostgreSQL-এর অফিসিয়াল 18.6 Windows বাইনারি থেকে নিলাম। একটি ভুল UPDATE ঘটিয়ে আলাদা ডেটাবেসে ফেরালাম (পাসওয়ার্ড PGPASSWORD-এ, লেখা example-secret):
$ pg_dump -h 127.0.0.1 -p 38741 -U postgres -Fc -f before.dump bazarghar
# ভুল: সব দাম 0
UPDATE "Product" SET price = 0; → BG-1001 0, BG-1002 0, BG-1003 0
# আলাদা, নতুন খালি ডেটাবেস: bazarghar_verify
$ pg_restore -h 127.0.0.1 -p 38741 -U postgres -d bazarghar_verify before.dump
[exit code 0]
sku price
BG-1001 420
BG-1002 135
BG-1003 190
_prisma_migrations-এ সফল সারি: 5
দুটি বিষয় খেয়াল করুন। Restore করা হয়েছে আলাদা ডেটাবেসে, চলমানটির ওপর নয়, তাই আগে দেখে নেওয়া যায়। আর হিসাবের টেবিলও সাথে এসেছে, ফলে restore-এর পরে Prisma ঠিকই জানে কোন migration চলেছে। Backup-এর পরে লেখা ডেটা এই ফাইলে নেই, এই ফাঁক কমানোর কৌশল backup আর replication পর্বে।
৯ · Seed — বারবার চালালেও একই ফল
Migration গঠন বদলায়, seed শুরুর ডেটা বসায়। নতুন সহকর্মী ফাঁকা অ্যাপ না দেখে যেন “মিনিকেট চাল”-সহ দোকান দেখেন। Seed-এ দুই ধরনের ডেটা আলাদা রাখুন: reference data, যেমন বিভাগ, যা সব পরিবেশে লাগে; আর নমুনা ডেটা, যা production-এ যাবে না। 7.10.1-এ কমান্ডটি prisma.config.ts-এর migrations.seed-এ থাকে। পুরোনো package.json-এর prisma.seed লিখলে সে শুধু বলে ⚠️ No seed command configured. Docs বলছে, v7-এ seed আর নিজে থেকে migrate dev বা reset-এর পরে চলে না, npx prisma db seed নিজে চালাতে হয়।
const categories = [
{ slug: 'chal-dal', nameBn: 'চাল ও ডাল' },
{ slug: 'tel-moshla', nameBn: 'তেল ও মসলা' },
];
const demoProducts = [
{ sku: 'BG-1001', name: 'মিনিকেট চাল ৫ কেজি', price: 420, stock: 30, cat: 'chal-dal' },
{ sku: 'BG-1002', name: 'মসুর ডাল ১ কেজি', price: 135, stock: 50, cat: 'chal-dal' },
{ sku: 'BG-1003', name: 'সয়াবিন তেল ১ লিটার', price: 190, stock: 0, cat: 'tel-moshla' },
];
async function main() {
for (const c of categories) {
await prisma.category.upsert({
where: { slug: c.slug },
update: { nameBn: c.nameBn },
create: c,
});
}
if (process.env.NODE_ENV === 'production') {
console.log('production: নমুনা ডেটা বাদ');
return;
}
for (const { cat, ...p } of demoProducts) {
await prisma.product.upsert({
where: { sku: p.sku },
update: { name: p.name, price: p.price },
create: { ...p, category: { connect: { slug: cat } } },
});
}
}$ npx prisma db seed
Running seed command `tsx prisma/seed.ts` ...
{ categories: 2, products: 3 }
The seed command has been executed.
$ npx prisma db seed
{ categories: 2, products: 3 } ← দ্বিতীয়বারেও ২ আর ৩
# dev-এ হাতে BG-1001: price 999, stock 7 করে আবার seed
sku price stock
BG-1001 420 7 ← price ফিরল, stock রইল
# NODE_ENV=production
production: নমুনা ডেটা বাদ → Category 2, Product 0
দ্বিতীয় run-এ কিছু বাড়েনি, এটিই idempotent. Upsert-এ where লাগে কোনো unique কলামে, তাই sku আর slug @unique. update-এ যা লিখবেন শুধু সেটিই ফেরে, তাই stock বাদ দিয়ে হাতের বদলটি রক্ষা পেল। Upsert ছাড়া সাধারণ create দ্বিতীয়বারে দেয় P2002 Unique constraint failed on the constraint: `Category_slug_key`. Unique না থাকলে আরও খারাপ: কোনো error ছাড়াই ডুপ্লিকেট। নিচে দুটোই:
CREATE TABLE Category (id INTEGER PRIMARY KEY, slug TEXT UNIQUE NOT NULL, nameBn TEXT NOT NULL);
CREATE TABLE BadCategory (id INTEGER PRIMARY KEY, slug TEXT, nameBn TEXT);
-- প্রথম seed
INSERT INTO Category (slug, nameBn) VALUES ('chal-dal', 'চাল'), ('tel', 'তেল')
ON CONFLICT (slug) DO UPDATE SET nameBn = excluded.nameBn;
INSERT INTO BadCategory (slug, nameBn) VALUES ('chal-dal', 'চাল'), ('tel', 'তেল');
-- দ্বিতীয় seed, একটি নাম ঠিক করা হয়েছে
INSERT INTO Category (slug, nameBn) VALUES ('chal-dal', 'চাল ও ডাল'), ('tel', 'তেল')
ON CONFLICT (slug) DO UPDATE SET nameBn = excluded.nameBn;
INSERT INTO BadCategory (slug, nameBn) VALUES ('chal-dal', 'চাল ও ডাল'), ('tel', 'তেল');
SELECT slug, nameBn FROM Category ORDER BY id;
SELECT count(*) AS good FROM Category;
SELECT count(*) AS bad FROM BadCategory;ফল: Category-তে chal-dal → চাল ও ডাল আর tel → তেল, good = 2, কিন্তু bad = 4. Prisma-র upsert-এর ধারণা একই। এই SQL রূপ PostgreSQL আর SQLite দুটোতেই চলে। excluded মানে “যে সারিটি ঢুকতে চেয়েছিল”।
Seed আর test fixture আলাদা
Seed মানুষের জন্য, test fixture একটি নির্দিষ্ট test-এর জন্য। দুটি মেশালে একদিন কেউ seed-এর দাম বদলাবেন, আর দশটি test অকারণে ভাঙবে। Test নিজের ডেটা নিজে বানাবে আর গুছিয়ে নেবে, পর্ব ৭৫ আর ৭৭-এ দেখবেন।
| Seed | Test fixture | |
|---|---|---|
| পাঠক | ডেভেলপার, demo দর্শক | একটি test |
| ডেটা | বাস্তবের মতো, বাংলা নাম | ন্যূনতম, যা test-এর দরকার |
| আয়ু | থেকে যায়, বারবার চালানো যায় | test শেষে মোছে |
| বদলালে | কোনো test ভাঙা উচিত নয় | শুধু ওই test বদলায় |
আরও গভীরে। PostgreSQL ১৮-এ NOT NULL-ও NOT VALID হিসেবে যোগ করা যায়: ADD CONSTRAINT … NOT NULL category NOT VALID, তারপর VALIDATE. আমাদের ১০ লাখ সারিতে এই দুটি নিল ২ ms আর ৭০ ms, তাই নতুন সংস্করণে CHECK-এর ঘুরপথ নাও লাগতে পারে। পুরোনো সংস্করণে CHECK-এর পথটিই চলে। আর ধাপে ধাপে ভাবার নাম “parallel change”, যা Danilo Sato ২০১৪ সালে Martin Fowler-এর সাইটে বর্ণনা করেন।
পরীক্ষা ও ইন্টারভিউতে।
migrate devআরmigrate deploy-এর পার্থক্য? dev নতুন ফাইল বানায় ও reset চাইতে পারে; deploy শুধু আগে থেকে থাকা ফাইল চালায়।- Drift কী? আসল গঠন আর migration ইতিহাসের অমিল।
migrate diff --exit-codeদিয়ে ধরা যায়। - কলামের নাম বদল downtime ছাড়া? নতুন কলাম, sync, backfill, পড়া বদল, পুরোনো মোছা।
- ALTER কেন সাধারণ SELECT আটকায়? ACCESS EXCLUSIVE lock সারিতে দাঁড়ালে পরের সবাই তার পেছনে যায়।
- Idempotent seed? unique key-তে upsert, বারবার চালালে একই ফল।
১০ · AI-এর যুগে migration
AI একটি সুন্দর migration মিনিটে লিখে দেয়। কিন্তু সে আপনার টেবিলের আকার, সারির ব্যস্ততা বা কোন সংস্করণ চলছে, কিছুই দেখে না। ভুল কোড ফেরানো যায়, ভুল migration সব সময় নয়। মজার তথ্য: আমাদের lab-এ Prisma 7.10.1 নিজেই টের পেল যে একটি AI agent কমান্ড চালাচ্ছে, আর থামিয়ে দিল। আমরা এই reset চালাইনি:
$ npx prisma migrate reset --force
Error: Prisma Migrate detected that it was invoked by Claude Code.
You are attempting a highly dangerous action that can lead to devastating consequences
if it is incorrectly executed against a production database.
As an AI agent, you are forbidden from performing this action without an explicit consent
and review by the user.
টুলের লেখকরাই বলছেন, ধ্বংসাত্মক কমান্ডের সিদ্ধান্ত মানুষের। AI যে ভুলগুলো বেশি করে:
| AI যা দেয় | সমস্যা | কীভাবে ধরবেন |
|---|---|---|
schema-তে url = env(...), package.json-এ seed | পুরোনো Prisma-র config; 7.10.1-এ P1012 | prisma --version মিলিয়ে docs পড়া |
production-এ migrate dev বা db push --accept-data-loss | reset বা ডেটা হারানো | পাইপলাইনে শুধু migrate deploy |
| এক ধাপে RENAME বা DROP | চলমান V1 ভাঙে (P2022) | expand and contract-এ ভাঙতে বলা |
| migration ফাইলে বিশাল UPDATE | দীর্ঘ lock, deploy আটকে থাকে | আলাদা ব্যাচ script |
BEGIN-এর ভেতরে CONCURRENTLY | 25001 | আলাদা ফাইল, একটি statement |
| চলে যাওয়া migration সম্পাদনা | দল আর production-এর ইতিহাস আলাদা হয় | নতুন ফাইল লেখা |
seed-এ create, বা নমুনা ডেটায় আসল ফোন নম্বর | ডুপ্লিকেট, ব্যক্তিগত তথ্য ফাঁস | upsert, বানানো ডেটা |
AI-কে কাজ দেওয়ার সময় টেবিলের আকার আর deploy-এর ধরন বলে দিন, আর চলমান ডেটাবেসে তাকে নিজে চালাতে দেবেন না। প্রথমে শুধু পরিকল্পনা চান:
Migration merge করার আগে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Migration ফাইলের ধারণা কোথা থেকে এলো? আগে দলগুলো ডেটাবেসের বদল কীভাবে সামলাত?
অনেক দিন ধরে ডেটাবেস ছিল একজন DBA-র হাতে। ডেভেলপাররা একটি SQL script পাঠাতেন, তিনি রাতে হাতে চালাতেন। কোন সার্ভারে কোন script চলেছে, তার হিসাব থাকত খাতায় বা মানুষের স্মৃতিতে। কোড Git-এর মতো টুলে সংস্করণ পেলেও ডেটাবেস থেকে গিয়েছিল আলাদা জগতে, আর দুটির মিল রাখা ছিল হাতের কাজ।
২০০৩ সালে Pramod Sadalage আর Martin Fowler-এর লেখা “Evolutionary Database Design” বলল, ডেটাবেসও ছোট ছোট ধাপে বদলানো যায়, যদি প্রতিটি বদল একটি script হয় আর কোডের সাথেই সংস্করণ পায়। ২০০৬ সালে Scott Ambler আর Sadalage-এর Refactoring Databases বইতে ধারণাটি পূর্ণ রূপ পায়। প্রায় একই সময়ে Ruby on Rails migration-কে দৈনন্দিন কাজ বানিয়ে দেয়, যেখানে সময়চিহ্ন দেওয়া ফাইল আর একটি হিসাবের টেবিল সাধারণ অভ্যাস হয়ে যায়। পরে Java জগতে Liquibase আর Flyway এই পথ ধরে। Prisma এসে যোগ করল একটি ঘোষণামূলক ধাপ: আপনি লেখেন কাঙ্ক্ষিত অবস্থা, টুল পার্থক্য থেকে SQL বানায়।
ঘোষণামূলক পদ্ধতির সুবিধা হলো কম লেখা। কিন্তু এই পর্বেই দেখেছেন, পার্থক্য থেকে SQL বানালে নাম বদল হয়ে যায় DROP আর ADD. টুল অবস্থা দুটি জানে, উদ্দেশ্য জানে না। তাই Prisma-তেও SQL ফাইল থাকে, যা মানুষ পড়েন ও বদলান।
মূল উপলব্ধি: Migration-এর আসল আবিষ্কার ফাইল নয়, বরং এই ধারণা যে ডেটাবেসের ইতিহাস আছে আর সেটি কোডের মতো পর্যালোচনা করা যায়।
প্র ০২অনেক দল down migration লেখেই না। এটি কি অলসতা, নাকি যুক্তি আছে?
যুক্তি আছে, আবার মতভেদও আছে। Rails-এর মতো টুলে down স্বাভাবিক অংশ, আর dev-এ ভুল হলে ফেরার জন্য সেটি কাজের। Prisma 7-এ এমন চালানোর কমান্ডই নেই, migrate diff দিয়ে উল্টো SQL বানিয়ে হাতে চালাতে হয়। Flyway-তেও undo সব edition-এ ছিল না বলে জানা যায়।
বিপক্ষের প্রথম যুক্তি: down ডেটা ফেরায় না। আমাদের run-এ discount ফিরেছে খালি কলাম হয়ে। দ্বিতীয়: down-এর পরীক্ষা হয় কম, আর যে কোড বিপদের দিনে প্রথমবার চলে, তার ওপর ভরসা করা কঠিন। তৃতীয়: migration-এর পরে নতুন অ্যাপ যে ডেটা লিখেছে, পুরোনো গঠনে তার জায়গা না-ও থাকতে পারে। নতুন কলামে আজ সকালের অর্ডারের তথ্য থাকলে down সেটি মুছে দেবে, অর্থাৎ ফেরাটিই নতুন একটি ধ্বংসাত্মক বদল।
পক্ষের যুক্তিও ফেলে দেওয়ার নয়। নিয়ন্ত্রিত পরিবেশে ফেরার লিখিত পরিকল্পনা চাওয়া হতে পারে, আর ছোট, ডেটা-না-ছোঁয়া বদলে down নিরাপদ ও দ্রুত। তাই একে পুরোপুরি বাদ দেওয়াও একটি সিদ্ধান্ত, যা দলকে জেনেবুঝে নিতে হয়। মাঝের পথ হলো প্রতিটি migration-এর সাথে একটি “ফেরার নোট”: কোড ফেরালে চলবে কি না, schema ফেরাতে হলে কী লাগবে, ডেটা কোন backup থেকে আসবে।
মূল উপলব্ধি: আসল প্রশ্ন “down লিখব কি না” নয়, বরং “ভুল হলে কত মিনিটে কীভাবে ফিরব”। Expand and contract সেই উত্তরকে সহজ করে, কারণ contract-এর আগে schema-কে ফেরাতেই হয় না।
প্র ০৩GitLab-এর ২০১৭ সালের ঘটনা থেকে backup নিয়ে কী শেখার আছে?
২০১৭ সালের ৩১ জানুয়ারি GitLab.com-এ একটি বড় ঘটনা ঘটে, যার বিস্তারিত তারা নিজেরাই খোলাখুলি প্রকাশ করে। Replication-এর সমস্যা সামলাতে গিয়ে একজন প্রকৌশলী রাতে কাজ করতে গিয়ে ভুল সার্ভারে একটি ফোল্ডার মোছার কমান্ড চালান, আর প্রধান ডেটাবেসের ডেটা মুছে যায়। তাদের প্রকাশিত হিসাব অনুযায়ী প্রায় ছয় ঘণ্টার ডেটা হারায়।
শিক্ষার জায়গাটি মোছা নয়, পরের ঘটনা। তারা দেখল, ভরসার অনেকগুলো backup পদ্ধতির বেশিরভাগ কাজ করছিল না বা পাওয়া যায়নি। নিয়মিত backup-এ pg_dump 9.2 চলছিল, অথচ ডেটাবেস PostgreSQL 9.6, তাই সেগুলো ব্যবহারের অযোগ্য ছিল। শেষে উদ্ধার হয় একটি LVM snapshot থেকে, যা কাজের আগে একজন প্রকৌশলী নিয়ে স্টেজিং-এ বসিয়েছিলেন।
এখান থেকে migration-এর জন্য সরাসরি তিনটি অভ্যাস আসে। Backup-এর সাফল্য মাপা হয় restore দিয়ে, ফাইল থাকা দিয়ে নয়, ঠিক যেমন আমরা আলাদা ডেটাবেসে ফিরিয়ে দাম মিলিয়েছি। Backup টুলের সংস্করণ server-এর সাথে মেলা চাই, আমাদের lab-এ দুটিই 18.6. আর মাঝরাতে, ভুল terminal-এ ধ্বংসাত্মক কমান্ড চালানোর ঝুঁকি বাস্তব, তাই পাইপলাইন আর আলাদা অনুমতি লাগে। GitLab-এর খোলা বিবরণ নিজেও একটি শিক্ষা: দোষারোপ নয়, ব্যবস্থা ঠিক করা। ঘটনার সময়ও তারা কাজটি প্রকাশ্যে জানিয়ে গিয়েছিল।
মূল উপলব্ধি: যে backup কখনো restore করা হয়নি, সেটি backup নয়, একটি আশা। ঝুঁকির migration-এর আগে “backup নিয়েছি” নয়, বলুন “restore করে দেখেছি”।
প্র ০৪PostgreSQL নিজেই অনেক বদল হালকা করেছে। তবু migration-এ lock নিয়ে এত সতর্কতা কেন?
PostgreSQL ধাপে ধাপে অনেক কিছু সহজ করেছে। CREATE INDEX CONCURRENTLY এসেছে ২০০৬ সালের দিকে, সংস্করণ ৮.২-তে। ২০১৮ সালের সংস্করণ ১১-তে স্থির DEFAULT-সহ কলাম যোগ আর টেবিল নতুন করে লেখে না। ১২-তে বৈধ CHECK থাকলে SET NOT NULL scan এড়ায়। ১৮-তে NOT NULL নিজেই NOT VALID হতে পারে, যা আমরা চালিয়ে দেখেছি।
তবু সমস্যা শেষ হয়নি, কারণ বিপদের মূল কাজের দৈর্ঘ্য নয়, lock-এর সারি। আমাদের run-এ ALTER নিজে এক মিলিসেকেন্ডের, তবু সে একটি অলস transaction-এর পেছনে দাঁড়িয়ে একটি সাধারণ SELECT-কে ২ সেকেন্ড আটকে রেখেছিল। ব্যস্ত সাইটে এই সারি connection pool ফুরিয়ে দেয়, আর তখন যে অনুরোধ ওই টেবিল ছোঁয় না, সেটিও সংযোগ পায় না। এজন্য অভিজ্ঞ দলের নিয়ম হলো ছোট lock_timeout, ব্যর্থ হলে একটু পরে আবার চেষ্টা, আর দীর্ঘ transaction ধরার পর্যবেক্ষণ।
টুলের জগৎও এই অভিজ্ঞতা থেকে গড়েছে। Rails জগতে strong_migrations, PostgreSQL-এর জন্য squawk-এর মতো linter ঝুঁকির migration PR-এই ধরে। MySQL জগতে gh-ost-এর মতো টুল। কোন টুল আপনার জন্য ঠিক, তা নির্ভর করে ডেটাবেসের ধরন, সংস্করণ আর টেবিলের আকারের ওপর। ছোট টেবিল আর কম ব্যবহারকারীর সাইটে রাতে এক মিনিটের বিরতিও সৎ, সহজ সমাধান হতে পারে।
মূল উপলব্ধি: “এই SQL কত সময় নেয়” যথেষ্ট প্রশ্ন নয়। জিজ্ঞেস করুন, “এটি কোন lock চায়, আর না পেলে কার পথ আটকায়?”
প্র ০৫অ্যাপ চালু হওয়ার সময় নিজেই migration চালিয়ে নিলে সমস্যা কী? অনেক টিউটোরিয়াল তো তাই করে।
ছোট সাইটে, একটি মাত্র সার্ভারে, এটি কাজ করে আর সহজও। npm start-এর আগে migrate deploy জুড়ে দিলে ভুলে যাওয়ার ভয় থাকে না। Django বা Rails-এর অনেক ছোট প্রজেক্টও এভাবে চলে। সমস্যা শুরু হয় সার্ভার বাড়লে।
প্রথমত, দশটি instance একসাথে চালু হলে দশটিই migration চালাতে চায়। Advisory lock একসাথে চলা আটকায়, কিন্তু আমাদের run-এ দেখেছেন ১০ সেকেন্ডের বেশি অপেক্ষায় P1002. অর্থাৎ লম্বা migration-এ বাকিগুলো ব্যর্থ হয়ে restart-এর চক্রে পড়তে পারে। দ্বিতীয়ত, migration ব্যর্থ হলে অ্যাপ চালুই হয় না, তাই একটি ভুল ফাইল পুরো সাইট নামিয়ে দেয়, অথচ পুরোনো instance-গুলো দিব্যি চলতে পারত। তৃতীয়ত, অ্যাপের সংযোগের ব্যবহারকারীকে টেবিল বদলানোর অধিকার দিতে হয়, যা least privilege-এর উল্টো। migration-এর জন্য আলাদা, বেশি ক্ষমতার ব্যবহারকারী থাকলে অ্যাপ হ্যাক হলেও ক্ষতি কম। চতুর্থত, serverless পরিবেশে কখন কতগুলো instance উঠবে, তার নিয়ন্ত্রণই থাকে না।
তাই পরিণত ব্যবস্থায় migration হয় পাইপলাইনের একটি একক ধাপ, অ্যাপ rollout-এর আগে, যার সাফল্য না হলে rollout শুরুই হয় না। Kubernetes-এ এটি প্রায়ই একটি আলাদা job হিসেবে থাকে। পর্ব ৮১-তে এই পাইপলাইন বানাবেন। অন্য দিকে, একটি VPS-এ একা চলা ছোট অ্যাপে deploy script-এর ভেতরে একটি লাইনই যথেষ্ট, এখানে অতিরিক্ত যন্ত্রপাতি লাগে না।
মূল উপলব্ধি: Migration অ্যাপের জীবনচক্রের অংশ নয়, release-এর অংশ। যে কাজ পুরো ব্যবস্থায় একবার হওয়ার কথা, তাকে প্রতি instance-এর শুরুতে বসাবেন না।
অনুশীলন
হিসাব, কমান্ড, ফল আন্দাজ আর একটি PR পর্যালোচনা।
-
Backfill-এর হিসাব: ২০ লাখ সারি, ব্যাচের আকার ৫,০০০. প্রতি ব্যাচে ৩০ ms কাজ, তারপর ৭০ ms বিরতি। কতটি ব্যাচ, মোট কত সময়? বিরতি বাদ দিলে কত? পাশে সেকেন্ডে ১০০টি লেখা আসে, আর প্রতি ব্যাচ ৩০ ms lock ধরে। একটি লেখা সবচেয়ে বেশি কত আটকাতে পারে (অন্য কিছু না ধরে)?
ব্যাচ: ২০,০০,০০০ ÷ ৫,০০০ = ৪০০টি। প্রতি ব্যাচ ৩০ + ৭০ = ১০০ ms, মোট ৪০০ × ১০০ ms = ৪০ সেকেন্ড। বিরতি ছাড়া ৪০০ × ৩০ ms = ১২ সেকেন্ড। ঠিক একই সারিতে লেখা পড়লে সে সর্বোচ্চ প্রায় ৩০ ms অপেক্ষা করে, কারণ ব্যাচের transaction শেষ হলেই lock ছাড়ে। এক
UPDATE-এ হলে সেই একই লেখা পুরো কাজের সময় আটকাত। ২৮ সেকেন্ড বেশি খরচ করে আমরা অপেক্ষা ছোট রাখছি। -
ফল আন্দাজ করুন: Prisma 7.10.1 আর PostgreSQL.
Product-এ ২টি সারি, দাম ৪২০ আর ১৩৫. migration ফাইলে পরপর:ADD COLUMN "brand" TEXT;,UPDATE "Product" SET "price" = 0;, আবারADD COLUMN "brand" TEXT;. কোনোBEGINনেই।migrate deploy-এর পরে কলাম, দাম আর পরেরdeploy-এর ফল কী হবে?তৃতীয় statement
42701-এ ব্যর্থ,deployদেয়P3018. আমাদের run-এbrandকলাম থেকে গেছে আর দুটি দামই 0, অর্থাৎ প্রথম দুটি statement ফেরেনি। পরেরdeployদেবেP3009. উদ্ধার: ডেটা আর কলাম হাতে আগের অবস্থায় ফেরান (দাম backup থেকে), তারপরmigrate resolve --rolled-back, ফাইল ঠিক করে আবারdeploy. আংশিক অবস্থা না সারিয়ে--rolled-backবললে পরের চেষ্টায় আবার “already exists”. -
একটি PR পর্যালোচনা করুন: ৫০ লাখ সারির
Orderটেবিলে একটি ফাইল:ALTER TABLE "Order" RENAME COLUMN "total" TO "amount";,ALTER TABLE "Order" ADD COLUMN "currency" TEXT NOT NULL DEFAULT 'BDT';,UPDATE "Order" SET "note" = trim("note");,CREATE INDEX "Order_amount_idx" ON "Order"("amount");. Deploy হয় rolling. কোনটি রাখবেন, কোনটি বদলাবেন?RENAME: চলমান V1 সাথে সাথে
P2022দেবে। expand and contract-এ ভাঙুন। currency: এটিই ঠিক আছে, স্থির DEFAULT, metadata-র কাজ। তবেlock_timeoutরাখুন। UPDATE: ৫০ লাখ সারি এক transaction-এ, দীর্ঘ row lock আর deploy আটকে থাকা। migration থেকে সরিয়ে আলাদা ব্যাচ script করুন। সত্যিই দরকার কি না, সেটিও প্রশ্ন। CREATE INDEX: লেখা আটকাবে, তাই নিজের ফাইলেCONCURRENTLY, schema-তে@@index. ব্যর্থ হলে invalid index মুছে আবার। সব মিলিয়ে একটি ফাইল নয়, চার-পাঁচটি ধাপ, কয়েকটি release জুড়ে। -
নিজে চালান: একটি নতুন খালি ডেটাবেসে পর্ব ৩৯-এর প্রজেক্ট নিন। হাতে একটি কলাম যোগ করে drift ঘটান। তারপর
migrate statusআরmigrate diff --from-migrations prisma/migrations --to-config-datasource --exit-codeচালান। প্রতিটির exit code কত? কেন দুটি আলাদা উত্তর?statusবলবে “Database schema is up to date!”, exit code 0, কারণ সে ফাইল আর হিসাবের টেবিল মেলায়, দুটিই ঠিক আছে।diffবাড়তি কলাম দেখাবে, exit code 2, কারণ সে migration ইতিহাস shadow database-এ চালিয়ে আসল গঠনের সাথে তুলনা করে।--exit-codeনা দিলে পার্থক্য থাকলেও 0 আসে। তাই CI-তে flag-টি জরুরি। Shadow database-এর ঠিকানা config-এ লাগবে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমচলমান অ্যাপে name কলামের নাম title করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- nullable
titleকলাম আর দুই কলাম মেলানোর trigger যোগ - পুরোনো সারি ব্যাচে backfill
title-এ NOT NULL- নতুন অ্যাপ rollout, কিছুক্ষণ দুই সংস্করণ একসাথে
- পুরোনো অ্যাপ বন্ধ হওয়া নিশ্চিত করা
- trigger আর
nameকলাম মোছা (contract)
Production ডেটাবেসে Git-এ থাকা বাকি migration চালাতে Prisma 7-এর কোন কমান্ড ঠিক?
prisma migrate devprisma migrate deployprisma db pushprisma migrate reset
কেউ production-এ হাতে একটি কলাম যোগ করেছেন। Prisma 7.10.1-এ migrate status বলছে “Database schema is up to date!”. কেন?
- হাতে যোগ করা কলাম PostgreSQL মুছে দেয়
- status নিজেই drift সারিয়ে নিয়েছে
- status শুধু ফাইল আর হিসাবের টেবিল মেলায়, আসল গঠন দেখে না
- status সব সময় exit code 2 দেয়
PostgreSQL-এ S1 একটি transaction খোলা রেখেছে, S2-এর ALTER TABLE তার জন্য অপেক্ষায়। এখন S3 একটি সাধারণ SELECT পাঠাল। কী হবে?
- S3-ও আটকে যাবে, কারণ সে S2-এর পেছনে সারিতে দাঁড়ায়
- S3 সাথে সাথে চলবে, কারণ SELECT কখনো আটকায় না
- S2 নিজে থেকে বাতিল হবে
- deadlock হবে, আর S1 বাতিল হবে
PostgreSQL-এ BEGIN;-এর পরে CREATE INDEX CONCURRENTLY চালালে কী হয়?
- সাধারণ index-এর মতো লেখা আটকে চলে
- COMMIT-এর সময় index তৈরি হয়
- একটি invalid index রেখে যায়
- error দেয়, কারণ এটি transaction block-এর ভেতরে চলে না
একটি কলাম DROP করার migration ভুল ছিল, আর কলামে দরকারি ডেটা ছিল। সবচেয়ে সত্য কথা কোনটি?
- Down migration চালালে কলাম আর মান দুটোই ফিরবে
- গঠন ফেরানো যায়, কিন্তু মানগুলো ফিরবে শুধু backup থেকে
migrate resolve --rolled-backকলাম ফিরিয়ে আনবে- আগের অ্যাপ সংস্করণ চালালেই ডেটা ফিরবে
একটি seed script দ্বিতীয়বার চালালে পণ্যের সংখ্যা ৩ থেকে ৬ হয়ে যাচ্ছে। সবচেয়ে ভালো সমাধান কোনটি?
- seed-এর শুরুতে সব পণ্য মুছে ফেলা
- seed একবারের বেশি না চালানোর নিয়ম করা
- unique
skuকলামেupsertব্যবহার করা - seed-এর ডেটা test fixture-এ সরানো
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Migration হলো ক্রমিক, Git-এ রাখা, একবার চলা SQL ফাইল। যে ফাইল চলেছে, সেটি আর বদলাবেন না।
migrate devdev-এ,migrate deployproduction-এ,db pushখসড়ায়। Drift ধরেmigrate diff --exit-code,statusনয়।- চলমান অ্যাপে expand and contract: যোগ, sync, backfill, অ্যাপ বদল, তারপর আলাদা release-এ মোছা।
- বিপদ lock-এর সারি:
lock_timeout, ব্যাচে backfill,CONCURRENTLYনিজের ফাইলে। - কোড ফেরান, schema সামনে সারান, ডেটা ফিরবে backup থেকে। Seed লিখুন idempotent, test fixture আলাদা।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪১ · সম্পর্ক — one-to-many, many-to-many আর শপের ডেটা মডেলপরবর্তী পর্ববাজারঘরের পুরো ডেটা মডেল Prisma-য়।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের রোডম্যাপ।
- ORM আর migrationতত্ত্বMigration runner, expand–contract আর দলের কাজের ধারণা।
- ALTER TABLE আর constraintভিত্তিSchema বদলের SQL.
- Deployment কৌশলপাশের ট্র্যাকRolling, blue-green আর canary.