Przejdź do treści
Utrzymanie i przekazanie

Repo nie ma ani jednej migracji, baza ma dane klienta. Reset to jedyna opcja, której nie wolno wybrać

Przejąłeś wygenerowany projekt i migracja chce zresetować bazę. Trzy polecenia tylko do czytania, baselining i bramka w CI, która wyłapie rozjazd.

Redakcjadeveloper front-end10 minkod + tabela

Klient ma działającą stronę, wykonawca nie odpowiada, a w repozytorium katalog prisma/migrations zawiera jeden plik sprzed ośmiu miesięcy albo nie istnieje wcale. Dopisujesz jedno pole do formularza kontaktowego, uruchamiasz prisma migrate dev i narzędzie proponuje reset bazy. To jest jedyne ostrzeżenie, jakie dostaniesz, że stan produkcyjnej bazy nie ma związku z historią w repo — i równocześnie jedna z niewielu rzeczy, których w tym projekcie nie wolno kliknąć.

Projekty generowane z modelem trafiają do przekazania z tą wadą częściej niż inne, bo droga „zmień schemat, wypchnij do bazy, idź dalej” jest w nich najkrótsza. Plik migracji nie powstaje, bo nikt go nie potrzebował. Następna osoba dostaje repo, które opisuje inny kształt bazy niż ten, na którym stoi strona.

Dryf to rozjazd bazy z historią migracji, nie z kodem aplikacji

Prisma definiuje stan, w który wszedłeś, jednym zdaniem: „Database schema drift occurs when your database schema is out of sync with your migration history”. Dokumentacja wskazuje też przyczynę: schemat bazy został zmieniony bez użycia migracji, na przykład przez prisma db push albo ręcznie.

Dwie konsekwencje tej definicji są ważniejsze od samego pojęcia. Pierwsza: punktem odniesienia nie jest kod aplikacji, tylko katalog migracji i tabela, w której baza zapisuje, co z tego katalogu już wykonała. Aplikacja może działać bez zarzutu przy całkowicie rozjechanej historii — zapytania trafiają w kolumny, które istnieją, niezależnie od tego, skąd się wzięły.

Druga jest operacyjna. Wykrycie dryfu wymaga osobnej bazy pomocniczej, a ta sama dokumentacja stwierdza, że „The shadow database is required to detect schema drift, and can therefore only be done in a development environment”. Mechanizm, który mówi ci o problemie, siedzi po stronie polecenia rozwojowego. Wdrożenie na produkcję wykonuje pliki oznaczone jako niezaaplikowane i na tym kończy swoją wiedzę — rozjazd, którego nie opisuje żaden plik, przechodzi przez deploy bez komunikatu.

Trzy polecenia tylko do czytania, zanim dotkniesz schematu

Kolejność jest tu całą treścią procedury. Polecenia, które zmieniają bazę, i polecenia, które ją tylko opisują, wyglądają w dokumentacji podobnie, a różnią się nieodwracalnością.

Polecenie Czyta czy zapisuje Co oddaje
prisma migrate status czyta porównanie katalogu migracji z tabelą _prisma_migrations: liczba znalezionych migracji, ostatnia wspólna, niezaaplikowane, obecne w bazie a brakujące lokalnie
prisma migrate diff --script czyta SQL, który przeprowadza jeden stan w drugi; bez --script czytelne dla człowieka podsumowanie
prisma db pull --print czyta schemat odtworzony z bazy wypisany na wyjście zamiast zapisany do pliku
prisma migrate dev zapisuje nowa migracja, a przy wykrytym rozjeździe propozycja resetu
prisma db push --accept-data-loss zapisuje stan schematu wepchnięty do bazy z pominięciem ostrzeżeń o utracie danych

Trzy pierwsze pozycje wykonujesz w przejętym projekcie. Dwie ostatnie dopiero wtedy, gdy wiesz, co jest w bazie, i masz odtworzoną kopię. Flaga --print przy introspekcji nie jest kosmetyką: dokumentacja CLI ostrzega, że db pull nadpisuje istniejący schema.prisma, a razem z nim ręczne zmiany, których w przejętym repo możesz jeszcze nie rozumieć. Opis flagi --accept-data-loss jest w referencji CLI równie bezpośredni — ignoruje ostrzeżenia o utracie danych i jest wymagana właśnie wtedy, gdy zmiana schematu może te dane skasować.

Przy migrate status przydaje się jeszcze jedna rzecz z referencji: od wersji 4.3.0 polecenie kończy się kodem 1 w pięciu sytuacjach — błąd połączenia, lokalne migracje niezaaplikowane, rozjechana historia, brak tabeli migracji i migracje zakończone błędem. To znaczy, że nadaje się do skryptu, nie tylko do czytania oczami.

Jak czytać wynik diffa: trzy klasy rozjazdu

Diff oddaje listę instrukcji, nie diagnozę. Kierunek różnicy mówi, co się stało i czy wolno ją po prostu dopisać do historii.

Co pokazuje diff Co to znaczy Pierwszy ruch
W bazie jest kolumna lub indeks, których nie ma w schemacie ktoś dodał obiekt poza migracjami: konsola hostingu, edytor tabel, ręczny SQL wciągnij do schematu introspekcją i zrób z tego migrację; nie usuwaj obiektu, bo może go używać kod, którego jeszcze nie przeczytałeś
W schemacie jest tabela, której nie ma w bazie migracja istnieje w repo, ale nigdy nie dotarła na produkcję sprawdź tabelę śledzącą, zanim wygenerujesz cokolwiek nowego — plik prawdopodobnie czeka niezaaplikowany
Ten sam obiekt, inny typ, nullowalność albo długość najdroższa klasa: rozjazd dotyczy kolumny, w której siedzą dane nie ruszaj poza oknem serwisowym i bez przywróconej kopii; zmiana typu na kolumnie z danymi bywa nieodwracalna

Oddzielnie potraktuj sytuację, w której diff jest pusty, a aplikacja i tak się wykrzacza na zapytaniu. Wtedy problem nie jest w schemacie, lecz w danych albo w uprawnieniach, a cała ta procedura nie dotyczy twojego objawu.

Baselining: historia zaczyna się tam, gdzie stoi produkcja

Kiedy w repo nie ma żadnej migracji, a baza ma dane, nie generujesz pierwszej migracji po to, żeby ją wykonać. Generujesz ją po to, żeby oznaczyć ją jako wykonaną. Dokumentacja Prismy nazywa to baseliningiem i podaje dokładny powód: pierwsza migracja odtwarzałaby bazę od zera, więc na istniejącej produkcji próbowałaby zakładać tabele i kolumny, które już istnieją, co najpewniej skończy się błędami.

Procedura z tej samej strony sprowadza się do czterech kroków: odłóż stary katalog migracji, utwórz podkatalog z prefiksem 0_, żeby sortował się pierwszy, zrzuć do niego pełny schemat i zarejestruj go jako zaaplikowany.

npx prisma migrate diff --from-empty --to-schema prisma/schema.prisma --script > prisma/migrations/0_init/migration.sql
npx prisma migrate resolve --applied 0_init

Drugie polecenie jest tu całą sztuczką: zapisuje migrację w tabeli _prisma_migrations jako zaaplikowaną, nie wykonując jej SQL-a. Od tego momentu wdrożenie pomija baseline i wykonuje tylko migracje utworzone po nim.

Dwie uwagi, bez których ta komenda zrobi szkodę. Pierwsza: nazwy flag zależą od wersji. Referencja CLI opisuje usunięcie w Prismie 7 flag z adresami połączeń — --from-url, --to-url i --shadow-database-url — na rzecz wariantów czytających źródło danych z pliku konfiguracyjnego. Zanim wkleisz cokolwiek z artykułu albo ze starszego wątku, uruchom prisma migrate diff --help u siebie i porównaj listę. Druga: baseline ma odpowiadać bieżącemu stanowi produkcji, a nie temu, co opisuje schema.prisma z repo. W przejętym projekcie te dwie rzeczy właśnie się nie zgadzają, więc schemat do zrzutu bierzesz z introspekcji bazy, nie z pliku po wykonawcy.

I zasada, która obowiązuje od tego momentu do końca projektu, sformułowana w dokumentacji bez miejsca na interpretację: „You should never purposefully delete or edit a migration, as this might result in discrepancies between development and production”. Poprawka do zaaplikowanej migracji to nowa migracja, nawet jeśli poprawiasz literówkę w nazwie kolumny.

Drizzle i Supabase: inne polecenia, ta sama diagnoza

Stack bywa inny, bo generatory podsuwają różne biblioteki. Objaw zostaje ten sam.

W Drizzle źródłem rozjazdu jest zwykle drizzle-kit push. Dokumentacja opisuje go jako wysłanie schematu i kolejnych zmian wprost do bazy, z pominięciem generowania plików SQL — czyli dokładnie to, co zostawia repo bez historii. W przejętym projekcie przyda się z tej samej strony flaga explain, która wypisuje planowane zmiany bez ich wykonania; to twój odpowiednik diffa do czytania. Odwrotność, flagę force, dokumentacja opisuje jako automatyczne przyjęcie wszystkich instrukcji prowadzących do utraty danych, więc w projekcie o nieznanym stanie nie ma dla niej zastosowania. Spójność samej historii sprawdza drizzle-kit check, przydatny, gdy schemat zmieniało kilka osób na różnych gałęziach.

W Supabase najczęstszą przyczyną jest edytor tabel i edytor SQL w panelu. Dokumentacja mówi wprost, że zmiany schematu wykonane na zdalnej bazie przez te narzędzia omijają historię migracji, co kończy się błędami synchronizacji przy db push. Do inwentaryzacji masz tam trzy polecenia: supabase migration list --remote pokazuje, co zastosowano lokalnie, co zdalnie i gdzie te listy się rozchodzą, supabase db pull --remote tworzy plik migracji odpowiadający bieżącemu stanowi zdalnej bazy, a supabase migration repair --status applied <znacznik> --remote porządkuje wpis w tabeli śledzącej supabase_migrations.schema_migrations. Przy ostatnim dokumentacja dodaje ostrzeżenie, które trzeba przeczytać dwa razy: naprawa aktualizuje wyłącznie tabelę śledzącą i nie wykonuje ani nie wycofuje żadnego SQL-a. Oznaczenie migracji jako zastosowanej nie tworzy niczego w bazie.

Bramka w CI, która wyłapie rozjazd przed wdrożeniem

Jednorazowe uporządkowanie historii nie trzyma się długo, jeśli dostęp do panelu bazy ma więcej niż jedna osoba. Tani mechanizm kontrolny daje flaga opisana w referencji CLI: --exit-code zmienia zachowanie migrate diff tak, że pusty diff zwraca 0, błąd 1, a niepusty 2.

# krok w CI: czy katalog migracji opisuje ten sam stan co schemat w repo
npx prisma migrate diff \
  --from-migrations ./prisma/migrations \
  --to-schema ./prisma/schema.prisma \
  --exit-code

Kod 2 na pull requeście znaczy, że ktoś zmienił schemat i nie dołożył migracji. To wyłapuje połowę przypadków — tę, która powstaje w repo. Drugiej połowy, czyli zmian wprowadzonych wprost w bazie, tym krokiem nie zobaczysz, bo porównuje dwa artefakty z repozytorium. Do niej potrzebujesz drugiego zadania, które porównuje historię migracji ze stanem rzeczywistej bazy, i tu jedna decyzja jest istotniejsza od samej komendy: celuj w odtworzoną kopię produkcji, nie w produkcję. Dane logowania do żywej bazy w zmiennych środowiskowych CI to osobne ryzyko, które właśnie przejąłeś razem z projektem.

W Drizzle odpowiednikiem pierwszego kroku jest wygenerowanie migracji w CI i sprawdzenie, czy katalog wyjściowy nie urósł o plik, którego nie ma w commicie — jeśli urósł, schemat wyprzedził historię. Jako drugi stopień dołóż migrate status z jego kodem wyjścia; piąty z wymienionych wcześniej warunków, migracje zakończone błędem, jest tym, o którym nikt nie pamięta, dopóki nie zobaczy go na produkcji. Błąd nieudanej migracji Prisma zapisuje w kolumnie logs tabeli _prisma_migrations, więc jest gdzie zajrzeć.

Co dopisać do protokołu przekazania

Przy odbiorze projektu żadna z tych rzeczy nie jest widoczna z zewnątrz. Strona działa, build przechodzi, a stan bazy to pytanie, które zadaje się dopiero przy pierwszej zmianie schematu. Lista poniżej jest krótka celowo — każdy punkt da się sprawdzić jednym poleceniem albo jednym zrzutem ekranu:

  • katalog migracji istnieje, a tabela śledząca w bazie zawiera wpisy odpowiadające jego zawartości;
  • migrate status albo migration list --remote kończy się bez rozjazdu, a wynik jest załączony do protokołu;
  • diff między historią a schematem jest pusty i ktoś to wykonał przy świadkach, a nie zadeklarował;
  • wiadomo, kto ma dostęp do panelu bazy i edytora SQL, czyli kto może ominąć historię po przekazaniu;
  • istnieje kopia, z której ktoś już raz odtworzył bazę — kopia nieprzetestowana przy odtwarzaniu nie jest kopią;
  • jeśli w projekcie użyto baseline, w repo leży notatka, od jakiego stanu i z jakiej daty historia się zaczyna.

Ostatni punkt bywa pomijany, a jest jedyny, którego nie odtworzysz z narzędzi. Baseline wygląda w katalogu jak zwykła pierwsza migracja i niczym nie zdradza, że nigdy nie została wykonana na produkcji.

Jeśli takiej listy nie ma w firmie w ogóle, pytanie o to, które procesy opisać najpierw, jest praktyczniejsze od pisania kompletnego regulaminu odbioru. Protokół przekazania bazy jest niezłym kandydatem na pierwszy wpis, bo jego brak wychodzi już przy pierwszym zleceniu po poprzednim wykonawcy.

Jeśli po migrate status, diffie i introspekcji potrafisz powiedzieć, skąd wziął się każdy obiekt w bazie, masz projekt do rozbudowy i wyceniasz zlecenie, o które klient poprosił. Jeśli nie potrafisz, masz projekt do inwentaryzacji, a to inna pozycja na fakturze i inny termin. Policz na nią osobno czas na baseline, na kopię odtworzoną na czystej instancji i na bramkę w CI, i przedstaw klientowi jako warunek wejścia w schemat bazy, nie jako dodatek do formularza kontaktowego.

Pokusa jest odwrotna: dopisać to pole, bo „to dziesięć minut”. Dziesięć minut to prawda dla kodu i nieprawda dla bazy, w której nie wiesz, co jest. Reset, który proponuje narzędzie, nie wie, że po drugiej stronie są dane klienta.