Wdrożenie produkcyjne PostgreSQL uległo awarii po tym, jak programista dodał opcjonalny argument do istniejącej funkcji za pomocą CREATE OR REPLACE FUNCTION. Zmiana ta pozostawiła dwie funkcje o tej samej nazwie, co spowodowało, że baza danych zwróciła błąd „function is not unique”, a API zwróciło błąd 400. Incydent ten pokazuje, jak jeden błąd w migracji może po cichu uszkodzić aktywny schemat i dlaczego same sprawdzenia na poziomie kodu nie wystarczają.

Co poszło nie tak

Zespół musiał rozszerzyć procedurę składowaną o dodatkowy, opcjonalny parametr. Użyli CREATE OR REPLACE FUNCTION …, zakładając, że nadpisze to starą definicję. PostgreSQL zastępuje funkcję tylko wtedy, gdy pełna lista argumentów zgadza się dokładnie. Zmiana sygnatury tworzy zupełnie nowy wpis funkcji, pozostawiając oryginał bez zmian.

Ponieważ nowy argument miał wartość domyślną, wywołania przekazujące starą liczbę argumentów mogły pasować do którejkolwiek z definicji. PostgreSQL nie mógł zdecydować, którą z nich wywołać, i wyrzucił błąd „function is not unique”, który objawił się jako odpowiedź 400 z API.

Repozytorium kodu pokazywało pojedynczą definicję, a własny skrypt skanujący drzewo źródeł nie zgłaszał żadnych duplikatów. Duplikat istniał tylko w bazie danych, wprowadzony podczas ponownego wykonania starego pliku migracji na serwerze.

Dlaczego migracja przeszła niezauważona

Migracja, która dodała opcjonalny parametr, po prostu uruchomiła CREATE OR REPLACE FUNCTION. Gdy migracja uruchomiła się po raz drugi — być może po wycofaniu zmian (rollback) lub podczas powtórnego wdrożenia — baza danych potraktowała to polecenie jako „dodaj nowe przeciążenie”, a nie „zastąp istniejące”. Migracja nie zweryfikowała stanu końcowego, więc duplikat pozostał niezauważony.

Skrypt sprawdzający repozytorium analizował pliki źródłowe, a nie aktywny schemat. Patrzył na drzwi frontowe, podczas gdy błąd wszedł tylnymi drzwiami.

Konsekwencje

Jedna niejednoznaczna funkcja może położyć każdą usługę, która się na niej opiera. Rozwiązanie polegało na wycofaniu transakcji, jeśli liczba funkcji była błędna, oraz powiadomieniu pamięci podręcznej schematu o konieczności przeładowania.

Jak zabezpieczyć migracje

Zespół przebudował migrację, wprowadzając jawne sprawdziany, co zmieniło ją w operację samoweryfikującą się:

  • Rozpocznij transakcję, aby w przypadku jakiejkolwiek awarii wycofać całą zmianę.
  • Jawnie usuń (DROP) starą funkcję przed utworzeniem nowej wersji, co zagwarantuje istnienie tylko jednej definicji.
  • Utwórz nową funkcję z pożądaną sygnaturą.
  • Policz funkcje o danej nazwie w pg_catalog i zweryfikuj, czy ich liczba wynosi dokładnie jeden.
  • Wycofaj transakcję, jeśli liczba jest inna, co zapobiegnie pozostawieniu duplikatu.
  • Powiadom pamięć podręczną schematu o konieczności przeładowania, aby kolejne zapytania widziały zaktualizowaną definicję.

Zamiast zakładać, że kod jest poprawny, pytając bazę danych „jakie funkcje są obecne?”, migracja staje się odporna na wielokrotne uruchomienia, częściowe wdrożenia lub ręczne edycje.

Kontrargument: wygoda vs. bezpieczeństwo

CREATE OR REPLACE FUNCTION jest atrakcyjne, ponieważ pozwala programistom na szybką iterację bez konieczności pisania oddzielnych instrukcji DROP. W środowiskach, gdzie migracje są uruchamiane tylko raz i nigdy więcej, ten skrót sprawdza się dobrze. Ryzyko pojawia się, gdy migracje są uruchamiane ponownie — czy to z powodu potoków CI resetujących bazy testowe, automatycznych wycofań zmian (rollbacków), czy ręcznego ponownego aplikowania w środowisku produkcyjnym.

Wnioski

Zmiana sygnatury funkcji za pomocą CREATE OR REPLACE nie gwarantuje zastąpienia — PostgreSQL po cichu utworzy przeciążenie, jeśli lista argumentów będzie się różnić. Środowiska produkcyjne polegające na migracjach muszą weryfikować wynikowy schemat, a nie tylko kod źródłowy. Wprowadzenie jawnych instrukcji DROP, sprawdzania transakcyjnego i asercji po migracji zmienia wygodny skrót w niezawodny, powtarzalny proces.