Zero-Downtime Database Migrations with Drizzle ORM

Run database migrations without downtime using Drizzle ORM. Covers expand-contract pattern, backward-compatible schema changes, rollback strategies, and CI/CD integration for PostgreSQL.

E
ECOSIRE Research and Development Team
|19 مارچ، 202611 منٹ پڑھیں2.4k الفاظ|

بوندا باندی ORM کے ساتھ زیرو ڈاؤن ٹائم ڈیٹا بیس کی منتقلی

ڈیٹا بیس کی منتقلی پیداوار کی تعیناتیوں میں سب سے خطرناک عمل ہے۔ ایک ہجرت جو ٹیبل کو لاک کرتی ہے — یہاں تک کہ 200ms کے لیے بھی — کنکشن ٹائم آؤٹ کا سبب بنتی ہے جو 500 غلطیوں میں جھڑ جاتی ہے جو 2 AM پر الرٹ طوفانوں میں جھڑ جاتی ہے۔ زیادہ تر ORM مائیگریشن ٹیوٹوریلز آپ کو "آسان" راستہ دکھاتے ہیں: ایپ کو بند کریں، منتقلی چلائیں، دوبارہ شروع کریں۔ یہ صفر ڈاؤن ٹائم نہیں ہے؛ یہ طے شدہ دیکھ بھال ہے.

حقیقی صفر-ڈاؤن ٹائم ہجرت کے لیے ایک مختلف ذہنی ماڈل کی ضرورت ہوتی ہے: آپ کا اسکیما تعیناتی کے پورے عمل کے دوران فی الحال چلنے والے ایپلیکیشن کوڈ کے ساتھ پسماندہ مطابقت پذیر ہونا چاہیے۔ اس کا مطلب ہے کہ رول آؤٹ ونڈو کے دوران پرانا کوڈ اور نیا کوڈ دونوں کو بیک وقت کام کرنا چاہیے۔ توسیعی معاہدے کا نمونہ اسے ممکن بناتا ہے۔ Drizzle ORM کا SQL-first اپروچ آپ کو اسے صحیح طریقے سے نافذ کرنے کا کنٹرول فراہم کرتا ہے۔

اہم ٹیک ویز

  • پروڈکشن میں drizzle-kit push کبھی نہ چلائیں — SQL بنائیں اور اس کا جائزہ لیں، پھر کنٹرول کے ساتھ درخواست دیں۔
  • توسیعی معاہدے کا نمونہ: شامل کریں (توسیع کریں) → نیا کوڈ تعینات کریں → پرانے کو ہٹا دیں (معاہدہ)
  • کالعدم کالم شامل کرنا محفوظ ہے۔ بغیر ڈیفالٹ کے NOT NULL شامل کرنا خطرناک ہے۔
  • کالم کا نام تبدیل کرنے کی ضرورت ہے: نیا کالم شامل کریں → بیک فل → اپ ڈیٹ کوڈ → پرانا کالم ہٹائیں (4 مراحل)
  • انڈیکس بنانے کے دوران ٹیبل لاک سے بچنے کے لیے PostgreSQL CREATE INDEX CONCURRENTLY استعمال کریں۔
  • پروڈکشن کے لیے درخواست دینے سے پہلے اپنے ڈیٹا بیس کی پروڈکشن سائز کی نقل پر منتقلی کی جانچ کریں۔
  • ہمیشہ ایک رول بیک پلان رکھیں: ہر منتقلی کے لیے، رول بیک SQL لکھیں اور جانچیں۔
  • اسکیما تبدیلیاں جن کے لیے ٹیبل کو دوبارہ لکھنے کی ضرورت ہوتی ہے (بڑے ٹیبل پر ALTER TYPE) مینٹیننس ونڈوز کی ضرورت ہوتی ہے

بوندا باندی ORM سکیما سیٹ اپ

// packages/db/src/schema/contacts.ts
import { pgTable, uuid, varchar, text, timestamp, pgEnum, index } from 'drizzle-orm/pg-core';

export const contactStatusEnum = pgEnum('contact_status', ['active', 'inactive', 'archived']);

export const contacts = pgTable('contacts', {
  id:             uuid('id').primaryKey().defaultRandom(),
  organizationId: uuid('organization_id').notNull(),
  name:           varchar('name', { length: 255 }).notNull(),
  email:          varchar('email', { length: 255 }),
  phone:          varchar('phone', { length: 50 }),
  status:         contactStatusEnum('status').default('active').notNull(),
  notes:          text('notes'),
  createdAt:      timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
  updatedAt:      timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
  orgIdIdx:  index('contacts_org_id_idx').on(table.organizationId),
  emailIdx:  index('contacts_email_idx').on(table.email),
  statusIdx: index('contacts_status_idx').on(table.status),
}));

نقل مکانی پیدا کریں:

# Generate SQL migration (never auto-apply to production)
npx drizzle-kit generate

# Review the generated SQL before applying
cat drizzle/0001_add_contact_status.sql

# Apply to development database
npx drizzle-kit migrate

# For production: apply via your deployment pipeline

توسیعی معاہدے کا پیٹرن

بنیادی اصول: ہر اسکیما کی تبدیلی کو متعدد پسماندہ مطابقت پذیر تعیناتیوں میں تقسیم کیا جاتا ہے۔

فیز 1: پھیلائیں (بغیر توڑنے کے شامل کریں)

اسکیما تبدیلیاں تعینات کریں جو صرف اضافی ہیں — ڈیفالٹس کے ساتھ نئے کالم، نئی میزیں، نئے اشاریہ جات۔

-- Migration: 0010_expand_add_company.sql
-- Safe to apply while old code is running: nullable column with a default

ALTER TABLE contacts
  ADD COLUMN IF NOT EXISTS company_id UUID,
  ADD COLUMN IF NOT EXISTS company_name VARCHAR(255);

-- Old code ignores these columns; new code uses them
-- Both versions coexist during rolling deployment
// Drizzle schema after expansion
export const contacts = pgTable('contacts', {
  // ... existing columns ...
  companyId:   uuid('company_id'),           // nullable — old code ignores it
  companyName: varchar('company_name', { length: 255 }), // nullable — safe
});

فیز 2: نیا ایپلیکیشن کوڈ لگائیں۔

ایپ کا نیا ورژن متعین کریں جو پرانے اور نئے کالموں کو پڑھتا اور لکھتا ہے۔ رولنگ تعیناتی کے دوران، پرانی مثالیں (کوئی کمپنی سپورٹ نہیں) اور نئی مثالیں (کمپنی سپورٹ) ایک ساتھ چلتی ہیں - دونوں کام کرتے ہیں کیونکہ کالم کالعدم ہے۔

فیز 3: بیک فل

ٹیبل لاک سے بچنے کے لیے موجودہ قطاروں کے لیے نئے کالم کو چھوٹے بیچوں میں آباد کریں:

-- Migration: 0011_backfill_company_name.sql
-- Run in small batches to avoid locking

DO $$
DECLARE
  batch_size INTEGER := 1000;
  offset_val INTEGER := 0;
  rows_updated INTEGER;
BEGIN
  LOOP
    UPDATE contacts
    SET company_name = 'Unknown'
    WHERE company_name IS NULL
      AND id IN (
        SELECT id FROM contacts
        WHERE company_name IS NULL
        ORDER BY id
        LIMIT batch_size
        OFFSET offset_val
      );

    GET DIAGNOSTICS rows_updated = ROW_COUNT;
    EXIT WHEN rows_updated = 0;

    offset_val := offset_val + batch_size;
    -- Brief pause between batches to reduce I/O pressure
    PERFORM pg_sleep(0.1);
  END LOOP;
END $$;

فیز 4: معاہدہ (پرانا کالم ہٹائیں)

تمام ایپلیکیشن کوڈ اپ ڈیٹ ہونے اور بیک فل مکمل ہونے کے بعد، پرانے کالم یا رکاوٹ کو ہٹا دیں۔


محفوظ ہجرت کے نمونے۔

NOT NULL کالم شامل کرنا

-- WRONG — will fail if table has rows (no default, no nullable)
ALTER TABLE contacts ADD COLUMN tier VARCHAR(20) NOT NULL;

-- WRONG — locks table while it writes the default to every row
ALTER TABLE contacts ADD COLUMN tier VARCHAR(20) NOT NULL DEFAULT 'free';

-- CORRECT — three-step approach
-- Step 1: Add nullable column
ALTER TABLE contacts ADD COLUMN tier VARCHAR(20);

-- Step 2: Backfill existing rows
UPDATE contacts SET tier = 'free' WHERE tier IS NULL;

-- Step 3 (next deployment): Add NOT NULL constraint (instant if no NULLs exist)
ALTER TABLE contacts ALTER COLUMN tier SET NOT NULL;
ALTER TABLE contacts ALTER COLUMN tier SET DEFAULT 'free';

کالم کا نام تبدیل کرنا (4 قدمی عمل)

-- Step 1: Add the new column
ALTER TABLE contacts ADD COLUMN full_name VARCHAR(255);

-- Step 2: Backfill + keep in sync with a trigger
UPDATE contacts SET full_name = name;

CREATE OR REPLACE FUNCTION sync_full_name() RETURNS trigger AS $$
BEGIN
  NEW.full_name := NEW.name;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER contacts_sync_full_name
  BEFORE INSERT OR UPDATE OF name ON contacts
  FOR EACH ROW EXECUTE FUNCTION sync_full_name();

-- Step 3: Deploy new code that writes to full_name, reads from both
-- Step 4 (next deployment, after all instances updated):
DROP TRIGGER contacts_sync_full_name ON contacts;
DROP FUNCTION sync_full_name;
ALTER TABLE contacts DROP COLUMN name;

اینوم کی قسم کو تبدیل کرنا

PostgreSQL مکمل ٹیبل کو دوبارہ لکھے بغیر enum اقدار کو ہٹانے کی اجازت نہیں دیتا ہے۔ محفوظ اینوم تبدیلیوں کے لیے:

-- Adding a new enum value is safe (instant, no lock)
ALTER TYPE contact_status ADD VALUE IF NOT EXISTS 'pending';

-- Removing an enum value requires a full table rewrite
-- Use the expand-contract pattern: add a new enum type, migrate, drop old
CREATE TYPE contact_status_new AS ENUM ('active', 'inactive', 'archived', 'pending');

-- Migrate data
ALTER TABLE contacts
  ALTER COLUMN status TYPE contact_status_new
  USING status::text::contact_status_new;

DROP TYPE contact_status;
ALTER TYPE contact_status_new RENAME TO contact_status;

انڈیکس کی تخلیق بغیر تالے کے

باقاعدہ CREATE INDEX ایک ShareLock حاصل کرتا ہے جو مدت کے لئے تمام تحریروں کو روکتا ہے۔ ایک بڑی میز پر، اس میں منٹ لگ سکتے ہیں۔

-- WRONG — locks writes during index creation
CREATE INDEX contacts_email_idx ON contacts(email);

-- CORRECT — concurrent index creation; no write lock
CREATE INDEX CONCURRENTLY IF NOT EXISTS contacts_email_idx
  ON contacts(email)
  WHERE email IS NOT NULL; -- Partial index for better performance

-- Drizzle note: drizzle-kit does not generate CONCURRENTLY by default
-- Edit the generated SQL migration to add CONCURRENTLY before applying

اپنی بوندا باندی کی منتقلی فائل میں، دستی طور پر CONCURRENTLY کو نسل کے بعد شامل کریں:

-- drizzle/0012_add_email_index.sql (edited after generation)
CREATE INDEX CONCURRENTLY IF NOT EXISTS contacts_email_idx
  ON contacts (email)
  WHERE email IS NOT NULL;

پروڈکشن CI/CD میں بوندا باندی کی منتقلی

پروڈکشن میں کبھی بھی drizzle-kit push استعمال نہ کریں — یہ بغیر کسی جائزے کے قدم کے منتقلی کا اطلاق کرتا ہے۔ drizzle-kit migrate جنریٹڈ ایس کیو ایل فائلوں کے ساتھ استعمال کریں جو کہ ورژن کے زیر کنٹرول ہیں:

# scripts/deploy-production.sh

echo "=== Running database migrations ==="

# Check if there are pending migrations
PENDING=$(npx drizzle-kit migrate --dry-run 2>&1 | grep "pending")
if [ -n "$PENDING" ]; then
  echo "Pending migrations detected:"
  echo "$PENDING"

  # Apply migrations
  npx drizzle-kit migrate
  echo "Migrations applied successfully"
else
  echo "No pending migrations"
fi
// Programmatic migration in main.ts (NestJS)
import { drizzle } from 'drizzle-orm/postgres-js';
import { migrate } from 'drizzle-orm/postgres-js/migrator';

async function runMigrations() {
  const migrationClient = postgres(process.env.DATABASE_URL!, { max: 1 });
  const db = drizzle(migrationClient);

  await migrate(db, {
    migrationsFolder: join(__dirname, '..', '..', '..', 'drizzle'),
  });

  await migrationClient.end();
}

// In bootstrap(), before app.listen():
if (process.env.RUN_MIGRATIONS === 'true') {
  await runMigrations();
}

رول بیک حکمت عملی

ہر ہجرت میں ایک ساتھی رول بیک مائیگریشن ہونا ضروری ہے جو تعیناتی سے پہلے لکھا اور ٹیسٹ کیا گیا ہو:

// drizzle/0013_add_company_id.sql (forward migration)
ALTER TABLE contacts ADD COLUMN IF NOT EXISTS company_id UUID;
CREATE INDEX CONCURRENTLY IF NOT EXISTS contacts_company_id_idx
  ON contacts(company_id);

// drizzle/rollbacks/0013_rollback_add_company_id.sql
DROP INDEX CONCURRENTLY IF EXISTS contacts_company_id_idx;
ALTER TABLE contacts DROP COLUMN IF EXISTS company_id;

ایمرجنسی رول بیک کے لیے:

# Emergency rollback script
# Run the rollback SQL, then redeploy the previous app version

psql "$DATABASE_URL" < drizzle/rollbacks/0013_rollback_add_company_id.sql
git checkout HEAD~1
pnpm build && pm2 restart ecosystem.config.cjs --update-env

ہجرت کی جانچ کی حکمت عملی

// packages/db/src/tests/migration.spec.ts
import { describe, it, expect, beforeAll } from 'vitest';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import postgres from 'postgres';
import { drizzle } from 'drizzle-orm/postgres-js';

describe('Database Migrations', () => {
  let sql: ReturnType<typeof postgres>;

  beforeAll(async () => {
    sql = postgres(process.env.TEST_DATABASE_URL!);
  });

  it('all migrations apply cleanly on a fresh database', async () => {
    const db = drizzle(sql);
    await expect(migrate(db, { migrationsFolder: './drizzle' }))
      .resolves.not.toThrow();
  });

  it('migrations are idempotent (can be applied twice safely)', async () => {
    const db = drizzle(sql);
    await migrate(db, { migrationsFolder: './drizzle' });
    // Running again should be a no-op, not an error
    await expect(migrate(db, { migrationsFolder: './drizzle' }))
      .resolves.not.toThrow();
  });
});

خطرناک آپریشنز جن میں ونڈوز کی دیکھ بھال کی ضرورت ہوتی ہے۔

کچھ آپریشنز کو بہت بڑی میزوں پر صفر-ڈاؤن ٹائم نہیں بنایا جا سکتا (سیکڑوں لاکھوں قطاریں):

آپریشنخطرہتخفیف
ALTER TYPE USING بڑے کالم پرمکمل ٹیبل دوبارہ لکھنا، طویل تالاشیڈول مینٹیننس ونڈو
ADD COLUMN NOT NULL بغیر ڈیفالٹ کےاگر قطاریں موجود ہوں تو فوری ناکامتین قدمی توسیعی معاہدہ استعمال کریں۔
CREATE INDEX (بغیر اتفاق کے)مدت کے لیے لاک لکھیںہمیشہ CONCURRENTLY استعمال کریں۔
ALTER TABLE ... RENAME TOفوری، لیکن پرانے کوڈ کو توڑتا ہےکوڈ اپ ڈیٹ ہونے کے بعد صرف توسیعی مرحلے میں
DROP TABLEناقابل واپسیہمیشہ بیک اپ رکھیں؛ 30 دن کی نرم ڈیلیٹ پالیسی
VACUUM FULLمدت کے لیے ٹیبل لاکدیکھ بھال کے دوران باقاعدہ VACUUM + شیڈول FULL استعمال کریں۔

اکثر پوچھے گئے سوالات

drizzle-kit push اور drizzle-kit migrate میں کیا فرق ہے؟

push منتقلی کی فائلیں بنائے بغیر آپ کے موجودہ اسکیما کو براہ راست ڈیٹا بیس پر لاگو کرتا ہے — ترقی کے لیے مفید ہے جہاں آپ کو منتقلی کی تاریخ کی پرواہ نہیں ہے۔ migrate آپ کی drizzle/ ڈائرکٹری میں SQL فائلوں کو ترتیب سے لاگو کرتا ہے، جس کا اطلاق کیا گیا ہے۔ پروڈکشن میں ہمیشہ migrate استعمال کریں تاکہ آپ کے پاس ہر اسکیما تبدیلی کی ایک کنٹرول شدہ، قابل جائزہ، ورژن کے زیر کنٹرول تاریخ ہو۔

Drizzle drizzle_migrations ٹریکنگ ٹیبل کو کیسے ہینڈل کرتی ہے؟

بوندا باندی ایک drizzle_migrations (یا __drizzle_migrations) ٹیبل بناتی ہے جو ریکارڈ کرتی ہے کہ کون سی منتقلی فائلوں کو لاگو کیا گیا ہے اور کب۔ منتقلی کا اطلاق کرنے سے پہلے، یہ اس جدول کو چیک کرتا ہے۔ اس ٹیبل سے قطاریں دستی طور پر داخل یا حذف نہ کریں - یہ بوندا باندی کا اسٹیٹ ٹریکنگ میکانزم ہے۔ اگر آپ کو پچھلی منتقلی کی حالت میں واپس جانے کی ضرورت ہے، تو اپنا رول بیک SQL چلائیں اور متعلقہ قطار کو دستی طور پر حذف کریں۔

میں پیداوار کے سائز کے ڈیٹا بیس کے خلاف منتقلی کی جانچ کیسے کروں؟

اپنے پروڈکشن ڈیٹا بیس کی سینیٹائزڈ (PII سے ہٹائی گئی) کاپی اسٹیجنگ سرور پر بحال کریں۔ وہاں منتقلی کا اطلاق کریں اور پیمائش کریں: وال کلاک ٹائم، لاک انتظار کا وقت (pg_stat_activity سے)، ٹیبل بلوٹ (pg_stat_user_tables سے)، اور استفسار کی کارکردگی پر اثر (EXPLAIN ANALYZE سے)۔ اگر منتقلی میں 5 سیکنڈ سے زیادہ وقت لگتا ہے تو اسے دوبارہ ڈیزائن کریں۔

کیا میں ڈیٹا بیس کی منتقلی کے لیے لین دین استعمال کرسکتا ہوں؟

PostgreSQL میں زیادہ تر DDL ٹرانزیکشنل ہوتا ہے — آپ ٹرانزیکشن میں ALTER TABLE, CREATE TABLE, CREATE INDEX (لیکن CREATE INDEX CONCURRENTLY نہیں) کو لین دین میں لپیٹ سکتے ہیں اور اگر کوئی مرحلہ ناکام ہو جاتا ہے تو واپس لوٹ سکتے ہیں۔ بوندا باندی ہر مائیگریشن فائل کو بطور ڈیفالٹ ٹرانزیکشن میں لپیٹ دیتی ہے۔ منتقلی کے لیے جو CONCURRENTLY استعمال کرتے ہیں، آپ کو انہیں ایک علیحدہ منتقلی فائل میں تقسیم کرنا ہوگا جو ٹرانزیکشن سے باہر چلتی ہے۔

میں TypeScript میں Drizzle enum موازنہ کو کیسے ہینڈل کروں؟

بوندا باندی کے اینوم کالم ڈیٹا بیس سے سٹرنگ ویلیو واپس کرتے ہیں۔ موازنہ کرتے وقت، TypeScript ٹائپ کو صحیح طریقے سے تنگ نہیں کر سکتا۔ واضح طور پر کاسٹ کریں: if ((contact.status as ContactStatus) === 'active') یا اپنی بوندا باندی where شق: where(eq(contacts.status, 'active' as ContactStatus)) میں قسم کا دعویٰ استعمال کریں۔ یہ TypeScript کو خوش رکھتے ہوئے رن ٹائم کی غلطیوں سے بچتا ہے۔


اگلے اقدامات

صفر-ڈاؤن ٹائم ڈیٹا بیس کی منتقلی کے لیے نظم و ضبط کی ضرورت ہوتی ہے — لیکن متبادل (ڈاؤن ٹائم، ڈیٹا بدعنوانی، صبح 3 بجے ایمرجنسی رول بیکس) اس سے کہیں زیادہ خراب ہے۔ توسیعی معاہدے کا پیٹرن، CREATE INDEX CONCURRENTLY، اور مناسب رول بیک تیاری اسکیما کو ایک معمول، محفوظ آپریشن بناتی ہے۔

ECOSIRE Drizzle ORM، PostgreSQL 17 کے ساتھ NestJS ایپلیکیشنز کے لیے ڈیٹا بیس اسکیموں کا انتظام کرتا ہے، اور 65+ اسکیما فائلوں اور 300+ منتقلی میں زیرو-ڈاؤن ٹائم تعیناتی پائپ لائن کا تجربہ کرتا ہے۔ یہ جاننے کے لیے کہ ہم ڈیٹابیس کی کارروائیوں کو بڑے پیمانے پر محفوظ طریقے سے کیسے ہینڈل کرتے ہیں ہماری بیک اینڈ انجینئرنگ سروسز کو دریافت کریں۔

E

تحریر

ECOSIRE Research and Development Team

ECOSIRE میں انٹرپرائز گریڈ ڈیجیٹل مصنوعات بنانا۔ Odoo انٹیگریشنز، ای کامرس آٹومیشن، اور AI سے چلنے والے کاروباری حل پر بصیرت شیئر کرنا۔

Chat on WhatsApp