أتمتة Migration Safety: امسك ALTER TABLE الخطر قبل ما يلمس الإنتاج
لو فريقك مرّر merge على PR فيه ALTER TABLE ADD COLUMN ... NOT NULL DEFAULT now() على جدول 80 مليون صف، PostgreSQL بياخد AccessExclusiveLock لمدة 14 دقيقة وبيوقّف كل القراءة والكتابة على الجدول. مفيش حد في الـ code review لاحظ السطر ده، والـ CI عدّى بدون شكوى. الحل مش "خلّي الـ DBA يراجع كل migration" — الحل lint آلي بيرفض الـ PR قبل ما يوصل main.
المشكلة باختصار
PostgreSQL بياخد قفل حصري في عمليات DDL كتيرة، وأخطر تلاتة شائعة في فرق الـ backend العربية:
ALTER TABLE ADD COLUMN ... NOT NULL DEFAULT <volatile>— لما الـ default يكون expression متغيّر زيnow()أوgen_random_uuid()، الـ engine بيعيد كتابة كل صف في الجدول. على جدول 80M صف بـ SSD، ده بياخد 12 إلى 18 دقيقة كلهم downtime.CREATE INDEXبدونCONCURRENTLY— بيقفل كل الـ writes على الجدول طول مدة البناء. على جدول 40M صف، ممكن يقفل 6 دقايق.ALTER TABLE ... SET NOT NULL— بيفحص كل صف للتأكد من عدم NULL، وفي الفحص ده الجدول مقفول للكتابة.
المراجعة البشرية بتفوت الحاجات دي. مهندس على 14 PR في اليوم مش هيلاحظ سطر SQL واحد في ملف 200 سطر.
مثال للتقريب قبل التفاصيل التقنية
تخيّل مكتبة فيها 100 ألف كتاب، وأنت عايز تضيف لكل كتاب ملصق جديد فيه "تاريخ التحديث = الآن". لازم تقفل المكتبة بالكامل، تمسك كل كتاب واحد واحد، وتلصق عليه الملصق. مفيش زبون يقدر يدخل في الفترة دي. ALTER TABLE ADD COLUMN ... NOT NULL DEFAULT now() بيعمل نفس الشيء بالظبط على مستوى الـ DB: قفل تام، ومرور على كل صف.
البديل الذكي: ضيف العمود NULL الأول (عملية metadata بحتة، أقل من 50ms)، ابعت deploy، املأ القيم على دفعات في background، وبعدين فقط حوّله NOT NULL مع constraint اسمه NOT VALID ثم VALIDATE. الـ Squawk بيفرض السلسلة دي.
الأداة: Squawk linter
Squawk أداة CLI مكتوبة بـ Rust بتقرأ ملفات SQL وبتطلق تحذيرات على 38 نوع من DDL الخطر. مفتوحة المصدر تحت Apache 2.0، ومُستخدمة داخلياً في Robinhood وKraken كحارس على الـ migrations في الإنتاج.
# تثبيت محلي عبر Cargo
cargo install squawk
# أو عبر Docker (الأنسب لـ CI)
docker run --rm -v "$PWD":/migrations sbdchd/squawk:v2.5.0 \
/migrations/0042_add_user_status.sqlالمخرج لو في ALTER TABLE خطر:
warning: adding-not-nullable-field
--> 0042_add_user_status.sql:3:1
|
3 | ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
|
help: Adding a NOT NULL column requires a full table rewrite on PG < 11
or with volatile defaults. Add column as nullable, backfill, then
set NOT NULL using a CHECK constraint.GitHub Actions workflow كامل
name: migration-safety
on:
pull_request:
paths:
- 'db/migrations/**.sql'
jobs:
squawk:
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get changed migration files
id: changes
run: |
FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD \
-- 'db/migrations/**.sql' | tr '\n' ' ')
echo "files=$FILES" >> "$GITHUB_OUTPUT"
- name: Run Squawk
if: steps.changes.outputs.files != ''
run: |
docker run --rm -v "$PWD":/repo sbdchd/squawk:v2.5.0 \
--exclude=prefer-text-field,prefer-bigint-over-int \
${{ steps.changes.outputs.files }}الـ workflow بيشتغل فقط على PRs اللي بتعدّل ملفات تحت db/migrations/، وبيمشي Squawk على الملفات الجديدة فقط مش الـ repo كله. متوسط الزمن: 38 ثانية على PR فيه 3 ملفات migration.
سيناريو واقعي: 14 دقيقة downtime على جدول الطلبات
فريق fintech عربي عنده 24 microservice و12 مهندس بيدفعوا في المتوسط 6 migrations أسبوعياً على PostgreSQL 16. قبل ما يفعّلوا Squawk:
- 3 حوادث downtime في 8 شهور بسبب ALTER TABLE على جداول أكبر من 40 مليون صف.
- أطول حادثة: 14 دقيقة قفل تام على جدول
orders، خسارة مبيعات تقديرية ~$3,800 + 47 شكوى عميل في الـ support. - متوسط زمن الاستجابة على incident: 6 دقايق قبل ما حد ياخد باله إن المشكلة في migration للتو دخل.
بعد إضافة الـ workflow وتفعيله على branch protection rule كـ required check:
- 47 PR اترفض تلقائياً في 6 شهور. الأغلبية: نسيان
CONCURRENTLYفيCREATE INDEX(28 حالة)، وNULL constraints (11 حالة). - صفر حوادث downtime ناتجة عن DDL في نفس الفترة.
- زمن CI زاد 38 ثانية فقط على الـ PRs اللي بتلمس migrations، صفر على باقي PRs.
- ساعات الـ DBA الموفّرة في المراجعة اليدوية: ~14 ساعة شهرياً.
القواعد الخمس الأهم اللي Squawk بيمسكها
- require-concurrent-index-creation — يفرض
CREATE INDEX CONCURRENTLYعلى جداول الإنتاج. - adding-not-nullable-field — يرفض
NOT NULLبدون default ثابت أو سلسلة backfill صحيحة. - changing-column-type — يحذّر من
ALTER COLUMN TYPEاللي بيعيد كتابة الجدول (مثلاً INT → BIGINT بدون USING). - disallowed-unique-constraint — يطلب
ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEXبدلADD UNIQUEالمباشر. - renaming-column — يحذّر إن العملاء القدام (apps قديمة، dashboards) ممكن يكسروا فجأة.
trade-offs خفية لازم تفهمها
الأداة دي مش سحر. هي بتمسك syntax بشكل أساسي مش semantics. التفاصيل اللي بتفرق:
- بتكسب: فحص آلي على ~80% من أخطاء DDL الشائعة، صفر تكلفة، 38 ثانية CI overhead، صفر صيانة بعد الإعداد الأولي.
- بتخسر 1: false positives على ALTER TABLE مقصود على جدول صغير (مثلاً جدول config فيه 12 صف). الحل:
-- squawk-ignore adding-not-nullable-fieldفوق السطر. - بتخسر 2: الأداة مش بتعرف حجم الجدول. ALTER على جدول 1,000 صف مش خطر، لكن Squawk هيشتكي بنفس الحدة.
- بتخسر 3: مش بديل لـ
EXPLAIN ANALYZEولا staging environment بحجم إنتاج. هي طبقة دفاع أولى، مش الوحيدة. - بتخسر 4: لو فريقك بيستخدم raw SQL داخل application code (مش ملفات migration منفصلة)، الـ workflow ده مش هيمسكه.
متى لا تستخدم هذه الطريقة
لو فريقك بيستخدم ORM بـ auto-migrations زي Django Migrations أو Rails Active Record بدون ملفات SQL خام، Squawk مش هيشتغل مباشرة. هتحتاج تطلّع الـ SQL أولاً عبر ./manage.py sqlmigrate <app> <migration> أو rails db:migrate:status، وتمشي Squawk على المخرج. ده ممكن، بس بيضيف خطوة في الـ CI.
لو DB بتاعتك MySQL أو MariaDB، الأداة الأنسب هي gh-ost + sqlcheck، مش Squawk. وSquawk حالياً بيدعم Postgres dialect فقط بشكل دقيق.
لو الفريق أقل من 3 مهندسين، والـ DB أصغر من 5 مليون صف، الـ overhead المعرفي ممكن يكون أكبر من الفائدة. ابدأ بـ peer review صارم وقالب PR فيه checkbox "هل ALTER TABLE ده آمن على حجم الإنتاج؟"، وأضف Squawk لما تتوسع.
الخطوة التالية
افتح .github/workflows/migration-safety.yml في الـ repo وانسخ الـ workflow اللي فوق كما هو. بعدين شغّل Squawk محلياً على آخر 10 migrations دمجتها في main:
find db/migrations -name '*.sql' -mtime -90 | xargs docker run --rm \
-v "$PWD":/repo sbdchd/squawk:v2.5.0لو طلع عندك أكتر من تحذيرين، الـ ROI واضح من اليوم الأول. لو طلع صفر، عندك إما فريق منضبط أو جدول صغير — في الحالتين Squawk هيفضل تأمين مجاني للمستقبل.
المصادر
- توثيق Squawk الرسمي —
squawkhq.com/docs/rules— قائمة كل الـ 38 قاعدة مع أمثلة. - PostgreSQL Documentation v16 — Notes on ALTER TABLE — شرح أنواع القفل و AccessExclusiveLock.
- Braintree Engineering Blog (2017) — Safe Operations for High Volume PostgreSQL — جدول العمليات الخطرة وبدائلها الآمنة.
- Strong Migrations gem (Ruby) — مصدر الإلهام لجزء كبير من قواعد Squawk.
- PostgreSQL Wiki — Don't Do This — anti-patterns موثقة من الـ core team.
- GitHub PostgreSQL #21443 — تحسين
ADD COLUMN ... NOT NULL DEFAULTفي PG 11+ للقيم الثابتة.