Продакшн-розгортання PostgreSQL зазнало збою після того, як розробник додав необов'язковий аргумент до існуючої функції за допомогою CREATE OR REPLACE FUNCTION. Ця зміна призвела до появи двох функцій з однаковою назвою, через що база даних почала повертати помилку “function is not unique”, а API — помилку 400. Цей інцидент демонструє, як одна помилка в міграції може непомітно пошкодити живу схему, і чому перевірок на рівні коду недостатньо.

Що пішло не так

Команді потрібно було розширити збережену процедуру додатковим необов'язковим параметром. Вони виконали CREATE OR REPLACE FUNCTION …, припускаючи, що це перезапише старе визначення. Проте PostgreSQL замінює функцію лише тоді, коли повний список аргументів збігається точно. Зміна сигнатури створює абсолютно новий запис функції, залишаючи оригінальну без змін.

Оскільки новий аргумент мав значення за замовчуванням, виклики, що передавали стару кількість аргументів, могли відповідати будь-якому з двох визначень. PostgreSQL не зміг вирішити, яку саме функцію викликати, і видав помилку “function is not unique”, яка відобразилася як відповідь 400 від API.

У репозиторії коду було лише одне визначення, а спеціальний скрипт, який сканував дерево вихідного коду, не виявив дублікатів. Дублікат існував лише в базі даних, з'явившись після повторного виконання старого файлу міграції на сервері.

Чому міграція пройшла непоміченою

Міграція, що додавала необов'язковий параметр, просто виконувала CREATE OR REPLACE FUNCTION. Коли міграція запустилася вдруге — можливо, після відкату або під час повторного розгортання — база даних сприйняла команду не як «замінити існуючу», а як «додати нове перевантаження». Міграція не перевіряла кінцевий стан, тому дублікат залишився непоміченим.

Скрипт, який перевіряв репозиторій, аналізував вихідні файли, а не живу схему. Він стежив за вхідними дверима, тоді як помилка просочилася через задні.

Ставки

Одна неоднозначна функція може зупинити будь-який сервіс, який від неї залежить. Виправлення полягало в тому, щоб відкотити транзакцію, якщо кількість функцій була неправильною, і повідомити кеш схеми про необхідність перезавантаження.

Як захистити міграції

Команда переробила міграцію, додавши явні перевірки та перетворивши її на операцію із самоперевіркою:

  • Розпочніть транзакцію, щоб будь-який збій відкотив усі зміни.
  • Явно видаліть стару функцію перед створенням нової версії, щоб гарантувати наявність лише одного визначення.
  • Створіть нову функцію з потрібною сигнатурою.
  • Підрахуйте кількість функцій із цією назвою в pg_catalog і переконайтеся, що їх рівно одна.
  • Відкотіть транзакцію, якщо кількість не збігається, щоб запобігти появі дубліката.
  • Повідомте кеш схеми про необхідність перезавантаження, щоб наступні запити бачили оновлене визначення.

Замість того, щоб припускати правильність коду, запитуючи базу даних «які функції присутні?», ви робите міграцію надійною щодо повторних запусків, часткових розгортань або ручних правок.

Контраргумент: зручність проти безпеки

CREATE OR REPLACE FUNCTION є привабливою командою, оскільки дозволяє розробникам швидко вносити зміни, не пишучи окремих інструкцій DROP. У середовищах, де міграції запускаються лише один раз і ніколи не повторюються, такий підхід працює добре. Ризик виникає, коли міграції виконуються повторно — чи то через CI-пайплайни, які скидають тестові бази даних, чи то через автоматичні відкати, чи то через ручне повторне застосування в продакшні.

Висновок

Зміна сигнатури функції за допомогою CREATE OR REPLACE не гарантує заміни — PostgreSQL непомітно створить перевантаження, якщо список аргументів відрізняється. Продакшн-середовища, що покладаються на міграції, повинні перевіряти кінцеву схему, а не лише вихідний код. Додавання явних DROP, транзакційних перевірок та перевірок після міграції перетворює зручний скорочений шлях на надійний і повторюваний процес.