الأخطاء مجرد رسائل من SQLite يحاول إخبارك بشيء
رسائل أخطاء SQLite قصيرة وأحياناً غامضة، لكنها في الحقيقة تعود إلى عدد محدود من المشاكل الجذرية. معظم أخطاء SQLite الشائعة التي ستواجهها في بيئة الإنتاج تنحصر في خمس فئات: الأقفال، والصلاحيات، وتلف الملفات، وعدم تطابق المخطط (schema)، ومخالفات القيود (constraints). في هذه الصفحة سنمرّ على كل فئة على حدة — ما الذي يسبّبها، وماذا تعني فعلاً، وكيف تحلّها.
تأتي نصوص الأخطاء مرفقة برموز رقمية (والرموز الموسّعة أكثر تحديداً). وستجد الصيغتين معاً في السجلات (logs):
Error: database is locked -- الرمز 5 (SQLITE_BUSY)
Error: unable to open database -- الرمز 14 (SQLITE_CANTOPEN)
Error: attempt to write a readonly -- الرمز 8 (SQLITE_READONLY)
Error: database disk image is -- الرمز 11 (SQLITE_CORRUPT)
معرفة رمز الخطأ يساعدك كثيراً أثناء البحث — البحث عن SQLITE_BUSY يعطيك نتائج أفضل بكثير من الرسالة الإنجليزية المجردة.
خطأ database is locked (SQLITE_BUSY)
هذا أشهر خطأ في SQLite يصادفك في أي تطبيق يكتب على قاعدة البيانات من أكثر من مكان. SQLite يُسلسل عمليات الكتابة: اتصال واحد فقط يستطيع الإمساك بقفل الكتابة في اللحظة نفسها. وإذا لم يتمكن كاتب ثانٍ من الحصول على القفل خلال مهلة الانتظار (busy timeout)، تظهر لك هذه الرسالة.
إليك ثلاثة حلول لمشكلة database is locked، مرتبة حسب الأثر:
وضع WAL لوحده يكفي لحل مشكلة القفل في معظم الحالات، ويبقى الـ busy timeout هو شبكة الأمان عند حدوث تنافس فعلي على الكتابة. لكن الإعدادات وحدها لا تكفي، راجع الكود نفسه: أي معاملة (transaction) تُترك مفتوحة بينما البرنامج ينتظر استجابة من الشبكة ستُبقي القفل طوال تلك المدة. اجعل المعاملات قصيرة، ونفّذ COMMIT أو ROLLBACK فور انتهاء العمل.
خطأ unable to open database file في SQLite (SQLITE_CANTOPEN)
حاولت SQLite فتح الملف فرفض نظام التشغيل ذلك. في 95% من الحالات تكون المشكلة في مسار الملف نفسه أو في المجلد الذي يحتويه:
-- أشياء يجب التحقق منها:
-- 1. هل المسار موجود؟ ls -l /path/to/db.sqlite
-- 2. هل المجلد الأصلي موجود؟ ينشئ SQLite الملف
-- لكنه لا ينشئ المجلد الذي يحتويه.
-- 3. هل المستخدم الذي يشغّل العملية لديه صلاحيات قراءة وكتابة
-- على المجلد (وليس فقط على الملف)؟
-- 4. هل وحدة التخزين موصولة، وغير ممتلئة، وغير مخصصة للقراءة فقط؟
حالة دقيقة قد تفوتك: محرك SQLite يحتاج إلى إنشاء ملفات مرافقة (مثل -journal و-wal و-shm) بجوار ملف قاعدة البيانات. فإذا كان الملف نفسه قابلاً للكتابة لكن المجلد الحاوي له ليس كذلك، سينجح الفتح لكن عمليات الكتابة ستفشل. لذا احرص دائماً على منح صلاحيات الكتابة على مستوى المجلد أيضاً.
خطأ attempt to write a readonly database في SQLite (SQLITE_READONLY)
هذا الخطأ قريب جداً من سابقه؛ فالملف فُتح بنجاح لكن الكتابة عليه تفشل. وفيما يلي الأسباب مرتبة من الأكثر شيوعاً إلى الأقل:
- مستخدم نظام التشغيل لا يملك صلاحية الكتابة على الملف أو على المجلد الذي يحتويه.
- تم فتح الاتصال بوضع القراءة فقط (مثل استخدام
SQLITE_OPEN_READONLY، أو إضافةmode=roفي رابط URI). - وحدة التخزين مركّبة بصيغة للقراءة فقط (وهذا شائع مع روابط Docker bind mounts وبعض أنظمة الملفات السحابية).
- قاعدة البيانات موجودة على نظام ملفات شبكي لا يدعم آلية القفل (locking) التي يعتمد عليها SQLite.
أصلح الصلاحيات أو أعد تركيب وحدة التخزين. إذا كنت تعمل داخل Docker، فتأكد أن الـ bind mount ليس :ro وأن مستخدم الحاوية يملك المجلد.
خطأ database disk image is malformed (SQLITE_CORRUPT)
بايتات الملف لم تعد مطابقة لتنسيق SQLite. الأسباب الحقيقية في الغالب بيئية: إنهاء العمليات في منتصف الكتابة على أنظمة ملفات لا تدعم fsync بشكل سليم، أو نسخ قاعدة البيانات أثناء وجود عملية كتابة نشطة، أو أعطال في العتاد، أو مزامنة الملف عبر خدمات مثل Dropbox وiCloud.
تحقق أولاً من حجم الضرر:
إذا أرجع integrity_check القيمة ok، فقاعدة البيانات سليمة والخطأ مصدره شيء آخر (غالبًا اتصال قديم لم يُغلق بشكل صحيح). أما إذا أعاد لك قائمة بالمشاكل، فأنت بحاجة إلى عملية استرجاع.
أنظف طريقة للاسترجاع هي استخدام الأمر .recover من واجهة CLI، إذ يقوم باستخراج كل ما يمكن إنقاذه من البيانات إلى قاعدة بيانات جديدة:
sqlite3 corrupt.db ".recover" | sqlite3 recovered.db
sqlite3 recovered.db "PRAGMA integrity_check;"
إذا كان لديك نسخة احتياطية حديثة، استرجع منها بدلًا من ذلك — فهي أسرع وتُجنّبك الغموض الذي يرافق عبارة "استرجعنا معظم البيانات". راجع صفحة النسخ الاحتياطي والاسترجاع لتعرف الطريقة الصحيحة لنسخ قاعدة بيانات قيد التشغيل (تلميح: ليست عبر cp).
أخطاء no such table و no such column في sqlite
هذه الأخطاء تعني حرفيًا ما تقوله، لكن السبب في الغالب يعود إلى أحد أمرين: إمّا أنك متصل بقاعدة بيانات غير التي تظنها، وإمّا أن أحد الـ migrations لم يُنفَّذ.
تحقَّق من سلسلة الاتصال (connection string) في تطبيقك، فالمسارات النسبية تُحسب بناءً على مجلّد العمل الحالي (current working directory)، وهذا المجلّد يختلف بين الطرفية (terminal) وبيئة التطوير (IDE) وبيئة الإنتاج. أما قاعدة البيانات في الذاكرة (:memory:) فهي تبدأ فارغة في كل تشغيل، وهذه نقطة تُربك كثيرين ممن يتوقّعون أن تبقى البيانات محفوظة.
ولا تنسَ موضوع تنصيص أسماء المُعرِّفات (identifiers). الأسماء غير المحاطة بعلامات اقتباس لا تفرّق بين الأحرف الكبيرة والصغيرة، لكن "User" و"user" يُعدّان مُعرِّفَين مختلفَين تمامًا. فإن أنشأت جدولًا باسم محاط بعلامات اقتباس، فعليك الالتزام بالاقتباس في كل مرة تشير إليه.
انتهاك القيود (Constraint violations)
ترفض SQLite أي عملية كتابة قد تكسر قيدًا من القيود المُعرَّفة، ورسالة الخطأ تُحدِّد لك بالضبط أيّ قيد هو السبب:
وراء كل فشل من هذه الأخطاء كود مختلف داخلياً (SQLITE_CONSTRAINT_UNIQUE، SQLITE_CONSTRAINT_CHECK، SQLITE_CONSTRAINT_NOTNULL). والحل في الغالب يكون على مستوى التطبيق نفسه: تحقّق من المدخلات قبل الكتابة، أو استخدم INSERT ... ON CONFLICT للتعامل مع التكرارات بشكل مقصود.
أمّا خطأ FOREIGN KEY constraint failed فيستحق وقفة خاصة: المفاتيح الأجنبية في SQLite مُعطَّلة افتراضياً. وإذا لم تُفعِّلها، ستمرّ المراجع غير الصحيحة بصمت ثم تنفجر في وجهك لاحقاً عندما تُشغِّل التحقق. لذا اضبط هذا الـ pragma على كل اتصال:
cannot start a transaction within a transaction
هذا الخطأ يعني أنك استدعيت BEGIN بينما هناك معاملة (transaction) مفتوحة بالفعل. SQLite لا يدعم المعاملات المتداخلة، لكنه يتيح لك استخدام نقاط الحفظ المتداخلة (nested savepoints) التي تؤدي نفس الغرض:
إذا كان ORM أو إطار العمل الذي تستخدمه هو من يتولى إدارة المعاملات، فالغالب أنك طلبت منه فتح معاملة مرتين. تحقّق من تفعيل خيار autocommit، وما إذا كان connection pool يعيد استخدام اتصال يحتوي بالفعل على معاملة مفتوحة.
خطأ disk I/O error (SQLITE_IOERR)
نظام التشغيل رفض عملية قراءة أو كتابة. السبب قد يكون امتلاء القرص، أو تذبذبًا في نظام ملفات شبكي، أو أن الملف حُذف من تحت قدمَي SQLite. أول ما يجب فحصه هو df -h. وثاني شيء: هل قاعدة البيانات موجودة على وسيط غير موثوق مثل NFS أو مجلد متزامن مع السحابة؟ فـ SQLite يفترض وجود نظام ملفات POSIX محلي مع fsync يعمل بشكل سليم. وإن لم يكن بمقدورك نقل الملف، فاعلم أن احتمال التلف يرتفع.
خطأ syntax error near "..."
محلِّل SQLite يخبرك بالضبط بالرمز الذي أربكه. عادةً ما يكون الإصلاح على بُعد ثلاثة أسطر قبل الموضع الذي تشير إليه رسالة الخطأ — فاصلة ناقصة، أو معرّف غير محاط بعلامات اقتباس ويتعارض مع كلمة محجوزة، أو نص فيه علامات اقتباس مفردة بحاجة إلى تهريب ('it''s'، وليس 'it's').
استخدم ربط المعاملات (عبر علامات ? كعناصر نائبة) عند التعامل مع مدخلات المستخدم بدلًا من بناء استعلامات SQL بالدمج النصي — بهذه الطريقة تتفادى دفعة كاملة من أخطاء الصياغة وثغرات حقن SQL في آن واحد.
قائمة تشخيصية لأخطاء sqlite الشائعة
حين ينهار شيء ما في بيئة الإنتاج، الترتيب التالي يكفي لتغطية معظم الحالات في أقل من دقيقة:
خمسة pragmas، خمس إجابات. عند دمجها مع رمز الخطأ الذي ظهر من الاستعلام الفاشل، ستعرف إلى أي فئة تنتمي المشكلة، وأي صفحة من التوثيق عليك فتحها بعد ذلك.
ختام المنهج
هذه هي الجولة. انطلقت من CREATE TABLE ومررت بعمليات الـ joins والفهارس والـ transactions ووضع WAL والنسخ الاحتياطي، ووصلت الآن إلى أنماط الفشل التي تواجهها حين يلتقي SQLite بالواقع الفعلي. الأنماط تتكرر: transactions قصيرة، تفعيل الـ foreign keys، وضع WAL، نسخ احتياطي منتظم، واحترام صادق لـ PRAGMA integrity_check. حافظ على هذه العادات وسيعمل SQLite بهدوء لسنوات طويلة.
الأسئلة الشائعة
لماذا يظهر لي خطأ database is locked في SQLite؟
السبب أن اتصالاً آخر يحتجز قفل الكتابة، واتصالك انتهت مهلة انتظاره. الحلول المعتادة هي تفعيل وضع WAL عبر PRAGMA journal_mode=WAL حتى لا يحجب الكاتبُ القُرّاء، ورفع مهلة الانتظار بـ PRAGMA busy_timeout = 5000، والحرص على إنهاء (commit) المعاملات بسرعة بدل تركها مفتوحة.
كيف أحل مشكلة attempt to write a readonly database في SQLite؟
في الغالبية العظمى من الحالات، المشكلة في صلاحيات نظام الملفات وليست في SQLite نفسه. يحتاج المستخدم الذي يشغّل العملية إلى صلاحية الكتابة على ملف قاعدة البيانات والمجلد الذي يحتويه (لأن SQLite ينشئ ملفات مساعِدة بامتداد -journal أو -wal بجواره). تحقّق من المالك (ownership) وبتات الصلاحيات، وتأكد أن وحدة التخزين ليست مركّبة للقراءة فقط.
ما معنى رسالة database disk image is malformed؟
تعني أن SQLite قرأ بايتات لا تطابق التنسيق المتوقع، وعادةً يكون السبب تلفاً ناتجاً عن إنهاء مفاجئ للعملية، أو قرص تالف، أو نسخ الملف وهو مفتوح. شغّل PRAGMA integrity_check للتأكد، ثم استخدم أمر .recover في واجهة CLI لاستخراج ما يمكن إنقاذه إلى قاعدة بيانات جديدة. وإن كان لديك نسخة احتياطية، فاستعادتها أسرع بكثير.
لماذا تظهر لي رسالة no such table أو no such column؟
غالباً أنت متصل بملف قاعدة بيانات مختلف عمّا تظن، أو أن أحد ملفات الترحيل (migration) لم يُنفَّذ. شغّل PRAGMA database_list لمعرفة المسار الفعلي للملف الذي فتحه SQLite، ثم .schema tablename لرؤية الأعمدة الحقيقية. كما أن الأخطاء الإملائية واختلاف حالة الأحرف في المعرّفات شائعة — فـ SQLite لا يفرّق بين الأحرف الكبيرة والصغيرة في الأسماء غير المقتبسة، لكنه يفرّق بينها داخل علامات الاقتباس.