פריסת PostgreSQL בסביבת ייצור קרסה לאחר שמפתח הוסיף ארגומנט אופציונלי לפונקציה קיימת באמצעות CREATE OR REPLACE FUNCTION. השינוי הותיר שתי פונקציות עם אותו שם, מה שגרם למסד הנתונים להחזיר את השגיאה “function is not unique” ול-API להחזיר שגיאת 400. האירוע מדגים כיצד טעות בודדת במיגרציה יכולה להרוס בשקט סכימה פעילה (live schema), ומדוע בדיקות ברמת הקוד לבד אינן מספיקות.

מה השתבש

הצוות היה צריך להרחיב פרוצדורה מאוחסנת (stored procedure) עם פרמטר אופציונלי נוסף. הם הריצו CREATE OR REPLACE FUNCTION …, מתוך הנחה שהפקודה תדרוס את ההגדרה הישנה. PostgreSQL מחליפה פונקציה רק כאשר רשימת הארגומנטים המלאה תואמת בדיוק. שינוי החתימה (signature) יוצר רשומה חדשה לחלוטין של פונקציה, תוך השארת המקורית ללא שינוי.

בשל העובדה שלארגומנט החדש היה ערך ברירת מחדל, הקוראים (callers) שסיפקו את מספר הארגומנטים הישן יכלו להתאים לשתי ההגדרות. PostgreSQL לא הצליחה להחליט איזו מהן להפעיל והשליכה את השגיאה “function is not unique”, שהופיעה כתגובת 400 מה-API.

מאגר הקוד הראה הגדרה אחת בלבד, וסקריפט מותאם אישית שסרק את עץ המקור (source tree) לא דיווח על כפילויות. הכפילות התקיימה רק במסד הנתונים, והוכנסה כאשר קובץ מיגרציה ישן הורץ מחדש על השרת.

כיצד המיגרציה חמקה

המיגרציה שהוסיפה את הפרמטר האופציונלי פשוט הריצה CREATE OR REPLACE FUNCTION. כאשר המיגרציה הורצה בפעם השנייה — אולי לאחר rollback או במהלך פריסה חוזרת — מסד הנתונים התייחס לפקודה כאל "הוספת overload חדש" במקום כאל "החלפת הקיים". המיגרציה לא אימתה את המצב שנוצר, ולכן הכפילות נותרה מבלי שנתפסה.

הסקריפט שבדק את המאגר בחן קבצי מקור, לא את הסכימה הפעילה. הוא הסתכל בדלת הקדמית בזמן שהבאג נכנס מהדלת האחורית.

הסיכונים

פונקציה אחת מעורפלת יכולה להפיל כל שירות שמסתמך עליה. התיקון כלל ביצוע rollback לטרנזקציה אם מספר הפונקציות היה שגוי, ועדכון מטמון הסכימה (schema cache) כדי שיטען מחדש את הנתונים.

כיצד להגן על מיגרציות

הצוות בנה מחדש את המיגרציה עם בדיקות מפורשות, והפך אותה לפעולה בעלת אימות עצמי (self-asserting):

  • פתח טרנזקציה כדי שכל כישלון יבצע rollback לכל השינוי.
  • מחק את הפונקציה הישנה במפורש (Drop) לפני יצירת הגרסה החדשה, כדי להבטיח שקיימת רק הגדרה אחת.
  • צור את הפונקציה החדשה עם החתימה הרצויה.
  • ספור את הפונקציות בעלות אותו שם ב-pg_catalog וודא שהספירה היא בדיוק אחת.
  • בצע rollback לטרנזקציה אם הספירה שונה, כדי למנוע מהכפילות להישאר.
  • עדכן את מטמון הסכימה (schema cache) כדי שיטען מחדש, מה שיבטיח ששאילתות עוקבות יראו את ההגדרה המעודכנת.

על ידי שאילת מסד הנתונים "אילו פונקציות קיימות?" במקום להניח שהקוד תקין, המיגרציה הופכת לאמינה גם מול הרצות חוזרות, פריסות חלקיות או עריכות ידניות.

טיעון נגד: נוחות מול בטיחות

CREATE OR REPLACE FUNCTION הוא פקודה אטרקטיבית כי היא מאפשרת למפתחים לעבוד במהירות (iterate) מבלי לכתוב פקודות drop נפרדות. בסביבות שבהן מיגרציות רצות פעם אחת ולא מורצות שוב לעולם, הקיצור הזה עובד מצוין. הסיכון מופיע כאשר מריצים מחדש מיגרציות — בין אם בשל צינורות CI שמאתחלים מסדי נתונים לבדיקה, rollback אוטומטי, או הרצות ידניות מחדש בסביבת הייצור.

שורה תחתונה

שינוי החתימה של פונקציה באמצעות CREATE OR REPLACE אינו מבטיח החלפה — PostgreSQL ייצור בשקט overload אם רשימת הארגומנטים שונה. סביבות ייצור (production) המסתמכות על מיגרציות חייבות לאמת את הסכימה שנוצרת, ולא רק את קוד המקור. שילוב של פקודות drop מפורשות, בדיקות טרנזקציוניות ואימותים (assertions) לאחר המיגרציה הופך קיצור דרך נוח לתהליך אמין וניתן לשחזור.