تتبع مكتبة صـفوف نهجاً مغايراً جذرياً في إدارة مواكبة قواعد البيانات (database migrations)، مقارنةً بالأنظمة الخطية الشائعة كـ Sequelize أو Flyway أو Rails ActiveRecord. بدلاً من الاعتماد على سلسلة مرقمة من ملفات المواكبة يجب تنفيذها بالتسلسل، تربط مكتبة صـفوف المواكبة بتعريفات الجداول مباشرةً، وتتتبع تطور المخطط على أساس كل جدول على حدة.
الفكرة الأساسية: إصدارات الجداول
يُعلن كل جدول في مكتبة صـفوف عن إصداره الحالي ضمن المبدّل @جدول (أو @model بالإنجليزية). وتحتفظ المكتبة بجدول خاص في قاعدة البيانات يُسمى table_versions يسجّل الإصدار الحالي لكل جدول. عند استدعاء واكب()، يقارن المحرك الإصدارات المسجّلة في قاعدة البيانات بإصدارات الجداول المعرّفة في الكود، ثم يحدد دوال المواكبة التي يجب تنفيذها.
@جدول["vehicles", 2]
صنف مـركبة {
عرف_أساسيات_الجدول[]؛
@إلزامي
@فهرس_رئيسي
@عـدد_صحيح
@حقل
عرف المعرف: صـحيح؛
@مـحارف_مرنة["50"]
@مبدئي["سيارتي"]
@حقل
عرف الاسم: نـص؛
@عـدد_صحيح
@حقل["year"]
عرف سنة_الصنع: صـحيح؛
@مواكبة[1, 2]
دالة واكب_1_إلى_2(قب: سند[قـاعدة_بيانات]): سـندنا[خـطأ] {
أرجع قب.نفذ("alter table vehicles add column year integer").الخطأ؛
}
}
@model["vehicles", 2]
class Vehicle {
define_model_essentials[];
@notNull
@primaryKey
@Integer
@column
def id: Int;
@VarChar["50"]
@defult["My Car"]
@column
def name: String;
@Integer
@column["year"]
def year: Int;
@migration[1, 2]
function migrate1To2(db: ref[Db]): SrdRef[Error] {
return db.exec("alter table vehicles add column year integer").error;
}
}
الرقم 2 في @جدول["مركبات", 2] هو الإصدار الحالي لمخطط هذا الجدول. أما @مواكبة[1, 2] على الدالة فتُعلن أن هذه الدالة ترفع جدول مركبات من الإصدار 1 إلى الإصدار 2.
دورة حياة المواكبة
الخطوات بالتفصيل
-
ضمان وجود جدول التتبع. يُنشئ المحرك جدول
table_versionsإن لم يكن موجوداً. -
قراءة الحالة الحالية. يستعلم عن
table_versionsللحصول على رقم الإصدار الحالي لكل جدول سبق مواكبته. -
اختيار المواكبات المطلوبة. لكل جدول، يسير المحرك عبر سلسلة الإصدارات من الإصدار الحالي في قاعدة البيانات حتى الإصدار المستهدف المُعلن في الكود، جامعاً كل دوال المواكبة اللازمة. إذا غابت أي دالة في السلسلة يُعاد خطأ فوراً.
-
تحديد التبعيات. يمكن لكل دالة مواكبة أن تُعلن اعتمادها على جدول آخر عند إصدار بعينه. يحوّل المحرك هذه الإعلانات إلى قيود ترتيب مباشرة بين المواكبات.
-
الترتيب الطبولوجي. تُرتَّب المواكبات المجمّعة باستخدام خوارزمية كان، بحيث لا تُنفَّذ أي مواكبة إلا بعد اكتمال جميع تبعياتها. فإن اكتُشفت دورة في التبعيات أُعيد خطأ.
-
التنفيذ والتسجيل. تُنفَّذ كل مواكبة بالترتيب المحدد، وبعد نجاح كل منها يُحدَّث
table_versionsبالإصدار الجديد لذلك الجدول.
قواعد البيانات الجديدة: لا مواكبة مطلوبة
حين لا يكون الجدول موجوداً بعد في قاعدة البيانات (إصداره 0)، لا تستلزم المكتبة كتابة دالة مواكبة. بل تولّد تلقائياً جملة CREATE TABLE مباشرةً من تعريفات الحقول والمبدّلات في الجدول.
هذا يعني أنه في بيئة التطوير الجديدة يكفي استدعاء واكب() ليُنشأ كل الجداول من الصفر — دون الحاجة إلى أي دوال مواكبة. دوال المواكبة مطلوبة فقط حين يحتاج جدول موجود إلى التحوّل من إصدار إلى آخر.
مواكبات متعددة الخطوات
يمكن للجدول الواحد أن يحوي دوال مواكبة متعددة، كل منها يغطي خطوة إصدار مختلفة. يربط المحرك هذه الدوال تلقائياً في سلسلة:
@جدول["vehicles", 4]
صنف مـركبة {
// ... تعريفات الحقول الحالية ...
@مواكبة[2, 3, { سعر: 1 }]
دالة واكب_2_إلى_3(قب: سند[قـاعدة_بيانات]): سـندنا[خـطأ] {
أرجع قب.نفذ("insert into prices select id as price_id, the_price as price from vehicles").الخطأ؛
}
@مواكبة[3, 4]
دالة واكب_3_إلى_4(قب: سند[قـاعدة_بيانات]): سـندنا[خـطأ] {
أرجع قب.نفذ("alter table vehicles drop column the_price").الخطأ؛
}
}
@model["vehicles", 4]
class Vehicle {
// ... current field definitions ...
@migration[2, 3, { Price: 1 }]
function migrate2To3(db: ref[Db]): SrdRef[Error] {
return db.exec("insert into prices select id as price_id, the_price as price from vehicles").error;
}
@migration[3, 4]
function migrate3To4(db: ref[Db]): SrdRef[Error] {
return db.exec("alter table vehicles drop column the_price").error;
}
}
إذا كانت قاعدة البيانات عند الإصدار 2، نفّذ المحرك واكب_2_إلى_3 ثم واكب_3_إلى_4 بالتسلسل ليصل الجدول إلى الإصدار 4. أما إذا كانت عند الإصدار 3 فلا يُنفَّذ إلا واكب_3_إلى_4 فحسب.
التبعيات بين الجداول
المعطى الثالث في @مواكبة يُعلن الجداول الأخرى — وعند أي إصدار — التي يجب أن تكون جاهزة قبل تنفيذ هذه المواكبة. يستخدم المحرك هذه الإعلانات لتحديد ترتيب آمن للتنفيذ عبر جميع الجداول.
@جدول["vehicles"، 4]
صنف مـركبة {
// ...
@مواكبة[2, 3, { سعر: 1 }]
دالة واكب_2_إلى_3(قب: سند[قـاعدة_بيانات]): سـندنا[خـطأ] {
// هذه المواكبة تنسخ البيانات إلى جدول الأسعار.
// يجب أن تعمل فقط بعد إنشاء جدول الأسعار عند الإصدار 1.
أرجع قب.نفذ("insert into prices select id as price_id, the_price as price from vehicles").الخطأ؛
}
}
@جدول["prices"، 1]
صنف سـعر {
عرف_أساسيات_الجدول[];
@إلزامي
@فهرس_رئيسي
@عـدد_صحيح
@حقل["price_id"]
عرف معرف_السعر: صـحيح؛
@عـدد_عائم
@حقل["price"]
عرف السعر: عـائم؛
}
@model["vehicles", 4]
class Vehicle {
// ...
@migration[2, 3, { Price: 1 }]
function migrate2To3(db: ref[Db]): SrdRef[Error] {
// This migration copies data from vehicles into the prices table.
// It must only run after the prices table exists at version 1.
return db.exec("insert into prices select id as price_id, the_price as price from vehicles").error;
}
}
@model["prices", 1]
class Price {
define_model_essentials[];
@notNull
@primaryKey
@Integer
@column["price_id"]
def priceId: Int;
@Float
@column["price"]
def price: Float;
}
التبعية { سعر: 1 } تُخبر المحرك: “قبل تنفيذ هذه الدالة، تأكد من أن جدول prices عند الإصدار 1”. يترجم المحرك هذا إلى قيد ترتيب مباشر بين المواكبتين ويطبّقه عبر الترتيب الطبولوجي.

تجاوز الإنشاء التلقائي للجداول
بشكل افتراضي يُنشئ المحرك الجدول تلقائياً لأي جدول غير موجود بعد. إذا احتجت إلى التحكم الكامل في إنشاء الجدول الأولي لأي سبب كان يمكنك تعريف مواكبة تبدأ من الإصدار 0 بنفسك. وجود دالة @مواكبة[0, N] يُعْلم المحرك بتخطي الإنشاء التلقائي واستخدام دالتك عوضاً عنه:
@جدول["vehicles", 1]
صنف مـركبة {
// ... تعريفات الحقول ...
@مواكبة[0, 1]
دالة أنشئ_الجدول(قب: سند[قـاعدة_بيانات]): سـندنا[خـطأ] {
أرجع قب.نفذ(
"create table vehicles (id integer primary key, name varchar(50)) partition by range (id)"
).الخطأ؛
}
}
@model["vehicles", 1]
class Vehicle {
// ... field definitions ...
@migration[0, 1]
function createTable(db: ref[Db]): SrdRef[Error] {
return db.exec(
"create table vehicles (id integer primary key, name varchar(50)) partition by range (id)"
).error;
}
}
المواكبات الاعتباطية عبر الجداول الوهمي
أحياناً تحتاج إلى تنفيذ مواكبة بيانات لا تقابل تغييراً هيكلياً في أي جدول بعينه. يمكنك استخدام “جدول وهمي” — صنف بدون تعريفات حقول — كحاوية لدالة مواكبة فحسب:
@جدول["price_update", 1]
صنف تـحديث_الأسعار {
@مواكبة[0, 1, { مركبة: 2 }]
دالة واكب_0_إلى_1(قب: سند[قـاعدة_بيانات]): سـندنا[خـطأ] {
أرجع قب.نفذ("update vehicles set the_price = the_price + 50").الخطأ؛
}
}
@model["price_update", 1]
class PriceUpdate {
@migration[0, 1, { Vehicle: 2 }]
function migrate0To1(db: ref[Db]): SrdRef[Error] {
return db.exec("update vehicles set the_price = the_price + 50").error;
}
}
نظراً لاحتواء هذا الجدول على دالة @مواكبة[0, 1]، لا يُنشأ له جدول في قاعدة البيانات. يكتفي المحرك بتنفيذ الدالة وتسجيل price_update عند الإصدار 1 في table_versions. التبعية { مركبة: 2 } تكفل أن يكون جدول المركبات في الحالة المطلوبة قبل تنفيذ تحديث البيانات.
تشغيل المواكبة
تمرّر مخططاً — جدولاً منفرداً أو مجموعة جداول — إلى مهيكل وتستدعي واكب():
// جدول منفرد
عرف المخطط: مـركبة؛
قب.مهيكل[المخطط].واكب()؛
// عدة جداول
عرف المخطط: { مـركبة, سـعر, تـحديث_الأسعار }؛
قب.مهيكل[المخطط].واكب()؛
// Single model
def schema: Vehicle;
db.schemaBuilder[schema].migrate();
// Multiple models
def schema: { Vehicle, Price, PriceUpdate };
db.schemaBuilder[schema].migrate();
يتولى المحرك كل شيء: تحديد ما يجب تشغيله، وترتيبه الصحيح، وتسجيل التقدم.
تنظيف المواكبات القديمة
حالما تُنشر دالة مواكبة وتُنفَّذ في الخوادم الإنتاجية (production servers)، يمكن حذفها من الكود المصدري. لا يحتاج المحرك إلا إلى دوال المواكبة التي تجسر الفجوة بين الإصدار الحالي في قاعدة البيانات والإصدار المستهدف. أي دالة تغطي نطاق إصدار جرى تجاوزه في الخوادم لن يستشيرها المحرك مجدداً.
هذا تحسين جوهري في سهولة الاستخدام: مع مرور الوقت تبقى ملفات الجداول نظيفة وموجزة، بدلاً من تراكم قائمة لا تنتهي من دوال المواكبة التاريخية.
مقارنة مع أنظمة المواكبة الخطية
تُدير أدوات كـ Sequelize أو Flyway أو Rails ActiveRecord المواكبات بوصفها سلسلة مرقمة ومرتّبة من الملفات. كل مواكبة كُتبت في تاريخ مضى يجب أن تكون موجودة وأن تُنفَّذ بالترتيب، حتى في قاعدة بيانات جديدة تماماً.
| المعيار | النظام الخطي | مكتبة صـفوف |
|---|---|---|
| جدول جديد | بحاجة لمواكبة جديدة | يُنشأ تلقائيا، لا حاجة لإنشاء مواكبة |
| فهم المخطط الحالي | إعادة تتبع كل المواكبات ذهنياً | قراءة تعريف الجدول مباشرة |
| قاعدة بيانات جديدة | تنفيذ كل المواكبات من البداية | إنشاء الجداول تلقائياً من التعريفات |
| تعارض الفروع | المطوران يُنشئان ملف مواكبة بنفس الرقم | كل جدول مستقل؛ الجداول المختلفة لا تتعارض أبداً |
| تنظيف التاريخ | دمج المواكبات محفوف بالمخاطر ونادر الحدوث | احذف الدوال بعد النشر؛ لا خطر |
| ترتيب المواكبات بين الجداول | ضمني عبر أرقام الملفات؛ سهل الخطأ | إعلان صريح للتبعيات؛ المحرك يضمن الترتيب |
| المواكبات الاعتباطية للبيانات | ملف مواكبة كالمعتاد | جدول وهمي بدالة مواكبة 0←1 |
| المخطط كتوثيق | ملفات المواكبة هي المرجع؛ قد ينحرف تعريف الجدول | تعريف الجدول هو المرجع دائماً ومحدَّث دوماً |
تعارض الفروع البرمجية
في النظام الخطي، يُنشئ مطوران يعملان على مزايا مختلفة ملف المواكبة التالي لكلّ منهما. يُسمّي أحدهما ملفه 0042_add_tags.sql والآخر 0042_add_ratings.sql. عند دمج الفرعين يجب إعادة ترقيم أحدهما ويضطر كل المطورين إلى إعادة تشغيل المواكبات. في مكتبة صـفوف، لأن المواكبات تنتمي إلى جداولها وتُحدَّد بأزواج إصدارات لا بأرقام تسلسلية عامة، لا يمكن لمطورَين يعملان على جدولَي مركبة وطلب أن يتعارضا أبداً.
وضوح المخطط
في النظام الخطي، السبيل الوحيد لمعرفة شكل الجدول الحالي هو قراءة كل مواكبة مسّته على الإطلاق. في مكتبة صـفوف، تعريف الجدول هو دائماً المرجع الموثوق للمخطط الحالي؛ الحقول والأنواع والإلزامية والقيم الافتراضية والفهارس الرئيسية وفهارس الوصل — كل ذلك مُعلَن هناك. دوال المواكبة لا تهتم إلا بالفرق بين الإصدارات، لا بالصورة الكاملة.
نظافة التاريخ
تتراكم المواكبات في الأنظمة الخطية إلى ما لا نهاية. دمجها في مواكبة واحدة أولية ممكن لكنه هش ونادراً ما يُقدَم عليه. في مكتبة صـفوف يمكن ببساطة حذف دوال المواكبة التي تغطي نطاقات إصدارات مرّت على الخوادم الإنتاجية؛ تعريف الجدول يحفظ الحالة النهائية بالفعل، فلا شيء يستحق الاحتفاظ به.


