تعطلت عملية نشر PostgreSQL في بيئة الإنتاج بعد أن أضاف أحد المطورين وسيطاً اختيارياً إلى دالة موجودة باستخدام CREATE OR REPLACE FUNCTION. أدى هذا التغيير إلى وجود دالتين بنفس الاسم، مما تسبب في إرجاع قاعدة البيانات لرسالة "function is not unique" وإصدار واجهة برمجة التطبيقات (API) لخطأ 400. يوضح هذا الحادث كيف يمكن لخطأ واحد في عملية الهجرة (migration) أن يؤدي إلى تخريب المخطط (schema) الحي بصمت، ولماذا لا تكفي عمليات التحقق على مستوى الكود وحدها.

ما الذي حدث بشكل خاطئ

احتاج الفريق إلى توسيع إجراء مخزن (stored procedure) بإضافة معامل إضافي اختياري. قاموا بتشغيل CREATE OR REPLACE FUNCTION … مفترضين أنه سيقوم باستبدال التعريف القديم. لكن PostgreSQL لا تستبدل الدالة إلا عندما تتطابق قائمة الوسائط بالكامل تماماً. يؤدي تغيير التوقيع (signature) إلى إنشاء إدخال دالة جديد تماماً مع ترك الأصلية دون تغيير.

بما أن الوسيط الجديد كان له قيمة افتراضية، فإن المستدعيين الذين قدموا العدد القديم من الوسائط يمكنهم مطابقة أي من التعريفين. لم تتمكن PostgreSQL من تحديد أيهما يجب استدعاؤه، فأطلقت خطأ "function is not unique"، والذي ظهر كاستجابة 400 من واجهة برمجة التطبيقات (API).

أظهر مستودع الكود تعريفاً واحداً، وأفاد نص برمجي مخصص قام بفحص شجرة المصدر بعدم وجود تكرارات. كان التكرار موجوداً فقط في قاعدة البيانات، حيث نتج عن إعادة تنفيذ ملف هجرة قديم على الخادم.

لماذا أفلتت عملية الهجرة من الفحص

عملية الهجرة التي أضافت المعامل الاختياري قامت ببساطة بتشغيل CREATE OR REPLACE FUNCTION. وعندما تم تشغيل الهجرة للمرة الثانية — ربما بعد عملية تراجع (rollback) أو أثناء إعادة النشر — تعاملت قاعدة البيانات مع الأمر على أنه "إضافة تحميل زائد جديد" (add a new overload) بدلاً من "استبدال الموجود". لم تتحقق عملية الهجرة من الحالة الناتجة، لذا استمر التكرار دون ملاحظة.

النص البرمجي الذي فحص المستودع فحص ملفات المصدر، وليس المخطط (schema) الحي. كان ينظر من الباب الأمامي بينما دخل الخطأ من الباب الخلفي.

المخاطر

يمكن لدالة واحدة غامضة أن تعطل أي خدمة تعتمد عليها. تضمن الحل في التراجع عن المعاملة (transaction) إذا كان عدد الدوال غير صحيح، وإخطار ذاكرة التخزين المؤقت للمخطط (schema cache) لإعادة التحميل.

كيفية تأمين عمليات الهجرة

أعاد الفريق بناء عملية الهجرة مع عمليات تحقق صريحة، محولين إياها إلى عملية ذاتية التحقق:

  • بدء معاملة (transaction) بحيث يؤدي أي فشل إلى التراجع عن التغيير بالكامل.
  • حذف الدالة القديمة صراحةً قبل إنشاء الإصدار الجديد، لضمان وجود تعريف واحد فقط.
  • إنشاء الدالة الجديدة بالتوقيع المطلوب.
  • عدّ الدوال التي تحمل الاسم المعطى في pg_catalog والتحقق من أن العدد هو واحد بالضبط.
  • التراجع (Roll back) عن المعاملة إذا اختلف العدد، لمنع استمرار التكرار.
  • إخطار ذاكرة التخزين المؤقت للمخطط (schema cache) لإعادة التحميل، لضمان أن الاستعلامات اللاحقة سترى التعريف المحدث.

من خلال سؤال قاعدة البيانات "ما هي الدوال الموجودة؟" بدلاً من افتراض صحة الكود، تصبح عملية الهجرة موثوقة ضد عمليات التشغيل المتكررة، أو عمليات النشر الجزئية، أو التعديلات اليدوية.

وجهة نظر أخرى: الراحة مقابل الأمان

يعتبر CREATE OR REPLACE FUNCTION جذاباً لأنه يسمح للمطورين بالتطوير بسرعة دون كتابة عبارات حذف (drop) منفصلة. في البيئات التي تُنفذ فيها عمليات الهجرة مرة واحدة ولا تُعاد أبداً، يعمل هذا الاختصار بشكل جيد. تظهر المخاطر عندما يتم إعادة تشغيل عمليات الهجرة — سواء بسبب خطوط أنابيب التكامل المستمر (CI pipelines) التي تعيد ضبط قواعد بيانات الاختبار، أو عمليات التراجع الآلية، أو إعادة التطبيق اليدوي في بيئة الإنتاج.

الخلاصة

تغيير توقيع الدالة باستخدام CREATE OR REPLACE لا يضمن الاستبدال — حيث ستقوم PostgreSQL بصمت بإنشاء تحميل زائد (overload) إذا اختلفت قائمة الوسائط. يجب على بيئات الإنتاج التي تعتمد على عمليات الهجرة التحقق من المخطط (schema) الناتج، وليس فقط كود المصدر. إن تضمين عمليات حذف صريحة، وتحققات قائمة على المعاملات (transactional checks)، وتأكيدات ما بعد الهجرة يحول الاختصار المريح إلى عملية موثوقة وقابلة للتكرار.