قيد CHECK في SQLite: قاعدة يجب أن كل صف يحققها
قيد CHECK هو ببساطة تعبير منطقي (boolean) بترفقه بالجدول، وSQLite بيتحقق منه مع كل عملية INSERT أو UPDATE. لو نتيجة التعبير كانت false، العملية بتفشل وما بتعدّيش. يعني باختصار، ده أسلوب بتدمج بيه قواعد منطق العمل جوّه السكيمة نفسها — زي "السعر ما يبقاش بالسالب"، أو "الحالة يجب أن تكون واحدة من ثلاث قيم محددة".
يدخل الصفّان الأوّلان بدون مشاكل، أمّا الصفّ الثالث فيُرفض ويظهر الخطأ CHECK constraint failed — ولا يصل إلى الجدول إطلاقًا. القيد يفرض القاعدة على كلّ من يكتب في الجدول، سواء كان تطبيقك، أو سكربت ترحيل (migration)، أو حتى شخصًا يعبث بالأوامر من الـ CLI.
قيد CHECK على مستوى العمود مقابل مستوى الجدول
يمكنك كتابة قيد CHECK في موضعين: بعد تعريف العمود مباشرةً (على مستوى العمود)، أو بعد تعريف كلّ الأعمدة (على مستوى الجدول). السلوك واحد في الحالتين، والفرق فقط في أيّهما يقرأ بشكل أوضح.
الحجز الأول ينجح، أما الثاني فيفشل لأن تاريخ النهاية يسبق تاريخ البداية. القواعد التي تخص عمودًا واحدًا تكون أوضح حين تُكتب على مستوى العمود، بينما أي قاعدة تقارن بين عمودين أو أكثر فالأفضل كتابتها على مستوى الجدول.
حصر القيم ضمن قائمة محددة
من أكثر استخدامات قيود CHECK في SQLite شيوعًا هو إجبار العمود على قبول قيمة من مجموعة ثابتة فقط. بما أن SQLite لا يدعم نوع enum بشكل أصلي، فإن الصيغة المعتادة لذلك هي CHECK ... IN (...):
الصف الثالث يفشل لأن القيمة 'pending' ليست ضمن القائمة المسموح بها. ولو احتجت لاحقًا إضافة حالة جديدة، فسيتعيّن عليك إعادة بناء الجدول (سنتطرق لذلك بعد قليل)، لذا فكّر مليًّا قبل أن تُقفل القائمة. لكن مع المفردات الثابتة فعلًا، كأسماء الأدوار أو حالات الطلبات، فهذا القيد هو ما تحتاجه بالضبط.
تسمية قيود CHECK في SQLite
افتراضيًا يكون القيد بلا اسم. ورسالة الخطأ تكتفي بقول "CHECK constraint failed" مع التعبير، وهذا مقبول حين يوجد قيد CHECK واحد على الجدول، لكنه مربك حين تكون لديك خمسة قيود. أضف اسمًا للقيد عبر CONSTRAINT:
الآن أصبحت رسالة الخطأ تتضمن اسم القيد، فتعرف فورًا أيّ قاعدة جرى انتهاكها. صحيح أن التسمية تكلّفك بضعة حروف إضافية، لكنها تردّ لك الجميل أول مرة ينكسر فيها شيء على بيئة الإنتاج.
قيود CHECK مع قيم NULL: المفاجأة غير المتوقعة
قيد CHECK يُعتبر ناجحًا عندما يكون التعبير صحيحًا (true) أو يساوي NULL، ولا يفشل إلا حين تكون النتيجة false صراحةً. يبدو هذا غريبًا للوهلة الأولى، لكن تذكّر أن أي مقارنة مع NULL تنتج عنها قيمة NULL، لا true ولا false.
الصف الذي يحمل قيمة NULL يُضاف بدون أي مشكلة — لأن نتيجة NULL >= 0 هي NULL وليست false، وبالتالي قيد CHECK لا يفشل. إذا كنت تريد فعلاً منع الأرقام السالبة والقيم الفارغة في آنٍ واحد، اجمع بين NOT NULL وقيد CHECK كالتالي:
الآن عملية الإدراج تفشل عند قيد NOT NULL قبل أن يصل التنفيذ إلى CHECK أصلاً. القيدان يكمّلان بعضهما: NOT NULL يمنع غياب القيمة، وCHECK يضبط شكلها.
دوال مدمجة مفيدة داخل قيد CHECK
يمكن للتعبير داخل CHECK أن يستخدم معظم الدوال المدمجة في SQLite. وهذه بعض الدوال التي تتكرر كثيراً في الاستخدام العملي:
ثلاث حالات فشل: بريد إلكتروني بصيغة غير صحيحة، واسم مستخدم أقصر من اللازم، ورمز دولة بأحرف صغيرة. عامل LIKE يكفي للأنماط البسيطة، ويمكنك أيضاً استخدام length() وupper() وlower() والعمليات الحسابية بحرّية. فقط احرص على أن يبقى التعبير حتمياً (deterministic)؛ فاستخدام شيء مثل random() أو current_timestamp داخل CHECK ينتج قاعدة قد تتغير نتيجتها بين صفّ وآخر، وهذا نادراً ما يكون المطلوب.
الفرق بين CHECK و trigger في SQLite
كلاهما قادر على رفض البيانات السيئة، وكثيراً ما يحتار المبتدئ في أيّهما يستخدم. القاعدة العامة:
- استخدم CHECK عندما تعتمد القاعدة على الصف الذي يُكتب فقط. مثل: "مقارنة عمود بعمود آخر"، أو "قيمة ضمن نطاق معين"، أو "نص يطابق نمطاً محدداً".
- استخدم trigger (وتحديداً
BEFORE INSERT/UPDATEمع استدعاءRAISE) عندما تعتمد القاعدة على صفوف أخرى أو جداول أخرى، أو تحتاج إلى منطق أعقد من مجرد تعبير منطقي واحد.
قيد CHECK أسرع وأبسط وواضح داخل التعريف نفسه؛ فأي شخص يقرأ CREATE TABLE يرى القاعدة مباشرة. لا تلجأ إلى trigger إلا حين يعجز CHECK عن التعبير عمّا تريده.
لا يمكن حذف قيد CHECK باستخدام ALTER
هذه هي النقطة المزعجة الوحيدة. لا يدعم SQLite الأمر ALTER TABLE ... DROP CONSTRAINT، لذا لإزالة قيد CHECK أو تعديله عليك إعادة بناء الجدول:
BEGIN;
CREATE TABLE products_new (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL CHECK (price >= 0 AND price <= 1000000)
);
INSERT INTO products_new SELECT * FROM products;
DROP TABLE products;
ALTER TABLE products_new RENAME TO products;
COMMIT;
غلِّف العملية كلها داخل معاملة (transaction) حتى لو فشل شيء في المنتصف، تبقى قاعدة البيانات سليمة كما هي. وإذا كانت هناك جداول أخرى فيها مفاتيح أجنبية تشير إلى الجدول الذي تعيد بناءه، فالقصة تطول قليلاً: عطِّل foreign_keys، أعد البناء، فعِّلها من جديد، ثم تحقق من سلامة العلاقات. سنتناول هذا الموضوع لاحقاً في درس الـ migrations ضمن المنهج.
ما التالي؟ قيود UNIQUE
قيد CHECK يتحقق من شكل القيم داخل الصف الواحد، أما القيد التالي UNIQUE فيتحقق من العلاقة بين الصفوف بعضها ببعض، إذ يضمن ألا يتشارك صفّان في نفس القيمة ضمن عمود معيّن أو مجموعة أعمدة. هذا ما سنبدأ به في الدرس القادم.
الأسئلة الشائعة
ما هو قيد CHECK في SQLite؟
قيد CHECK هو تعبير منطقي (boolean) مرتبط بجدول، يجب أن تحققه كل صفوف الجدول. يقوم SQLite بتقييمه عند كل عملية INSERT أو UPDATE، ويرفض التغيير إذا كانت نتيجة التعبير false. باختصار، هو أبسط طريقة لفرض قاعدة مثل «السعر يجب أن يكون موجبًا» دون الحاجة لكتابة كود في التطبيق.
هل يمكن لقيد CHECK في SQLite أن يشير إلى أكثر من عمود؟
نعم، اكتبه كقيد على مستوى الجدول بدلًا من ربطه بعمود واحد. مثلًا CHECK (start_date <= end_date) المُعرَّف بعد قائمة الأعمدة يستطيع الإشارة إلى العمودين معًا. صحيح أن قيود مستوى العمود تستطيع تقنيًا الإشارة لأعمدة أخرى، لكن قيود مستوى الجدول تكون أوضح للقارئ عندما يتعلق الأمر بأكثر من عمود.
لماذا لا يعمل قيد CHECK عند وجود قيمة NULL؟
قيد CHECK يَقبل القيمة عندما تكون نتيجة التعبير true أو NULL، ولا يفشل إلا إذا كانت النتيجة false صراحةً. لذلك CHECK (age >= 0) سيقبل قيمة NULL للعمر، لأن NULL >= 0 نتيجتها NULL وليست false. إذا أردت منع NULL أيضًا، أضف قيد NOT NULL بجانب قيد CHECK.
هل يمكن حذف أو تعديل قيد CHECK في SQLite؟
ليس مباشرةً. SQLite لا يدعم ALTER TABLE ... DROP CONSTRAINT. لتعديل قيد CHECK، إما أن تعدّل sqlite_schema يدويًا باستخدام PRAGMA writable_schema (طريقة متقدمة وفيها مخاطرة)، أو تعيد بناء الجدول: أنشئ جدولًا جديدًا بالقيود المطلوبة، انسخ البيانات إليه، احذف الجدول القديم، ثم أعد التسمية. تسمية القيود تجعل سكربت إعادة البناء أوضح بكثير.