8 min

Weryfikacja schematu PostgreSQL przed pierwszą migracją

Weryfikacja schematu PostgreSQL wykrywa błędne mapowania, słabe ograniczenia, brakujące indeksy i niebezpieczne zmiany, zanim pierwsza migracja dotknie danych.

Weryfikacja schematu PostgreSQL przed pierwszą migracją

Generator AI może utworzyć poprawny kod PostgreSQL, a mimo to wywnioskować niewłaściwą bazę danych. Składnia jest łatwa. Groźne błędy brzmią wiarygodnie: opcjonalna relacja staje się wymagana, łańcuch statusu otrzymuje niepełne ograniczenie CHECK, usunięcie przechodzi kaskadowo przez rekordy, które powinny przetrwać, albo migracja odtwarza tabelę i po cichu gubi kolumnę.

Weryfikacja schematu PostgreSQL musi więc osobno sprawdzać znaczenie danych, zachowanie migracji i możliwość odzyskania stanu. Zatwierdzam wywnioskowany schemat dopiero wtedy, gdy przejdzie test na znanym zbiorze danych, jawnych niezmiennikach, reprezentatywnych zapytaniach, przeglądzie zmian destrukcyjnych i próbie odtworzenia. Jeśli brakuje choć jednego z tych elementów, migracja pozostaje propozycją.

Pierwsza migracja zasługuje na taką kontrolę, nawet gdy baza produkcyjna jest pusta. Wczesne błędy schematu szybko się utrwalają, ponieważ zaczynają od nich zależeć kod aplikacji, dane początkowe, raporty i późniejsze migracje. Piętnaście minut przeglądu przed pierwszym uruchomieniem zwykle kosztuje mniej niż wyjaśnianie pół roku później, dlaczego dwa różne pojęcia dzielą jedną dopuszczającą NULL kolumnę tekstową.

Wywnioskowany schemat to niezaufana specyfikacja

Traktuj wywnioskowany schemat jak roboczą specyfikację, a nie wykonywalną prawdę. Generator widział prompty, przykładowe ekrany, zaimportowane rekordy albo wygenerowany kod aplikacji. Nie poznał każdego wyjątku biznesowego, reguły retencji, importu zbiorczego, naprawy przez wsparcie i nieudanej płatności, które z czasem znajdą się w bazie.

Zacznij od rozdzielenia trzech pytań, które zespoły często mieszają. Poprawność schematu dotyczy tego, czy tabele i ograniczenia modelują domenę. Bezpieczeństwo migracji dotyczy tego, czy proponowane operacje zachowują istniejące dane i pozwalają korzystać z bazy w trakcie ich działania. Gotowość do odzyskiwania dotyczy tego, czy po częściowej albo błędnej znaczeniowo zmianie potrafisz wrócić do znanego stanu. Zaliczony jeden obszar niewiele mówi o pozostałych dwóch.

Instrukcja CREATE TABLE może opisywać docelową strukturę, a jednocześnie dojść do niej niebezpiecznymi operacjami. Załóżmy, że generator zmienia customer_name text na customer_id bigint. Końcowy klucz obcy może mieć sens, lecz migracja, która usuwa kolumnę z nazwą przed dopasowaniem historycznych nazw do klientów, niszczy jedyny dowód potrzebny do tego dopasowania. Przegląd schematu zatwierdza cel, przegląd migracji bada drogę.

Przeczytaj proponowany model na głos językiem domeny. Powiedz, że każda faktura należy do dokładnie jednego klienta będącego podmiotem prawnym, zamiast mówić, że invoices.customer_id odwołuje się do customers.id. Pierwsze zdanie zachęca do przydatnych zastrzeżeń: wersje robocze mogą istnieć przed wyborem klienta, importowane faktury mogą wskazywać zarchiwizowanych klientów, a dokumenty prawne mogą wymagać utrwalenia nazwy klienta z chwili wystawienia. Słownictwo SQL potrafi ukryć takie różnice zdań.

Wymagam notatki z założeniami obok każdej wywnioskowanej tabeli. Powinna wyjaśniać, co oznacza jeden wiersz, jak go identyfikujemy, kto go posiada, czy może istnieć bez pozornego rodzica i co oznacza jego usunięcie. Jeżeli zespół nie potrafi odpowiedzieć na te pytania, generator zgaduje bazę danych, której zespół nie zaprojektował.

Znane rekordy ujawniają błędne mapowanie tabel

Znany zestaw danych powinien zawierać rekordy wybrane dla pokrycia znaczeniowego, ponieważ duża losowa próbka często powtarza ten sam łatwy przypadek. Dziesięć starannie dobranych rekordów może ujawnić więcej niż dziesięć tysięcy niemal identycznych wierszy z poprawnej ścieżki.

Zbuduj macierz mapowania przed uruchomieniem DDL. Każdy wiersz macierzy powinien prześledzić dane pojęcie źródłowe przez proponowany cel i zapisać oczekiwaną liczbę lub wartość. Dla aplikacji obsługującej zamówienia może wyglądać tak:

Znany faktProponowany celOczekiwany wynik
Zamówienie A ma dwie pozycjeorders i order_itemsJeden wiersz zamówienia i dwa wiersze podrzędne
Zamówienie B nie ma przypisanego kontaorders.account_idJeden wiersz z kontem NULL
Dwie osoby mają ten sam e-mailcontacts.emailOba wiersze pozostają, chyba że unikalność jest jawną zasadą
Kod produktu zaczyna się od zerproducts.codeWartość tekstowa 00417 pozostaje bez zmian
Anulowane zamówienie zachowuje obciążeniaorders i chargesWiersze obciążeń pozostają po anulowaniu

To wychwytuje błędy mapowania tabel, zanim szczegóły ograniczeń odciągną uwagę od przeglądu. Generatory AI często normalizują powtarzające się obiekty do osobnych tabel, co zwykle jest rozsądne, ale powtarzalność nie dowodzi tożsamości. Dwa adresy wysyłki o tym samym tekście mogą być historycznymi migawkami, a nie odwołaniami do jednego edytowalnego wiersza adresu. Ich połączenie sprawia, że późniejsza zmiana adresu przepisuje historię.

Występuje też odwrotny błąd. Generator może kopiować pola klienta do każdego zamówienia, bo ekran pokazuje je razem. Część wartości należy do klienta, a inne muszą pozostać migawką zamówienia. Właściwy projekt może zawierać zarówno customer_id, jak i pola wystawionego dokumentu, takie jak billing_name. Nazwanie tego duplikacją i usunięcie jednej strony oznacza utratę bieżącej tożsamości albo prawdy historycznej.

Załaduj znany zestaw danych do jednorazowej bazy, używając tej samej ścieżki importu lub danych początkowych, z której będzie korzystać aplikacja. Następnie napisz asercje dotyczące faktów, a nie samych liczności wierszy:

SELECT
    (SELECT count(*) FROM orders WHERE external_id = 'ORDER-A') AS order_a,
    (SELECT count(*) FROM order_items i
       JOIN orders o ON o.id = i.order_id
      WHERE o.external_id = 'ORDER-A') AS order_a_items,
    (SELECT account_id IS NULL FROM orders
      WHERE external_id = 'ORDER-B') AS order_b_unassigned;

Poprawny wynik powinien być jawny:

 order_a | order_a_items | order_b_unassigned
---------+---------------+---------------------
       1 |             2 | t

Nie akceptuj niewyjaśnionej różnicy tylko dlatego, że wygenerowana aplikacja nadal się wyświetla. Interfejs może ukryć zduplikowanych rodziców, usunięte dzieci, skrócone kody i wymyślone wartości domyślne. Uzgodnij każdy celowo przygotowany przypadek przed rozmową o wdrożeniu produkcyjnym.

Ograniczenia muszą odzwierciedlać prawdy domenowe

Ograniczenie bazy danych powinno odrzucać stan zawsze nieprawidłowy, niezależnie od tego, który ekran, API, import czy skrypt naprawczy zapisuje wiersz. Jeżeli reguła ma wyjątki albo zależy od zmiennych faktów zewnętrznych, wymuszanie jej prostym ograniczeniem często prowadzi do zablokowanej pracy albo nieuczciwych danych.

Klucze główne identyfikują wiersze, ale nie zapewniają automatycznie sensownej tożsamości biznesowej. Wewnętrzne ID bigint może współistnieć z unikalnym numerem zamówienia w obrębie dzierżawcy. Jeśli biznes mówi, że numery zamówień są unikalne w obrębie dzierżawcy, UNIQUE (tenant_id, order_number) wyraża tę zasadę. Globalne ograniczenie unikalności odrzuciłoby prawidłowe rekordy, a brak ograniczenia pozwalałby na niejednoznaczność podczas ponawiania operacji.

Ograniczenia CHECK pasują do stałych faktów wiersza, takich jak quantity > 0 czy finished_at >= started_at. Dokumentacja PostgreSQL wyjaśnia, że baza zakłada niezmienność wyrażenia CHECK przez cały czas obowiązywania ograniczenia. Dlatego CHECK wywołujący funkcję, której działanie później się zmieni, może pozostawić stare wiersze naruszające pozorną regułę. Używaj stałego wyrażenia dla stałej prawdy. Zmienną politykę, taką jak aktualnie dozwolony zbiór kontrolowany przez administratorów, umieść w tabeli referencyjnej albo w procesie aplikacji.

Wygenerowane ograniczenia statusu wymagają ostrożności. Generator może przejrzeć obecne przykłady i utworzyć:

status text NOT NULL
    CHECK (status IN ('draft', 'active', 'closed'))

To jest poprawne tylko wtedy, gdy są to kompletne, trwałe stany. Zapytaj o rekordy nieudane, anulowane, zawieszone, zaimportowane i nieznane rekordy historyczne. Jeśli maszyna stanów nadal się zmienia, tabela słownikowa może uczynić dodawanie stanów jawnym, ale nie zastępuje sprawdzania przejść. Wiersz, który może zawierać closed, nic nie mówi o tym, czy może przejść bezpośrednio z draft do closed.

Stosuj unikalność świadomie. PostgreSQL realizuje ograniczenie unikalności za pomocą unikalnego indeksu B-tree, lecz częściowy indeks unikalny wyraża inną regułę. Miękkie usuwanie często wymaga unikalności tylko wśród aktywnych wierszy:

CREATE UNIQUE INDEX users_tenant_email_live_uq
    ON users (tenant_id, lower(email))
    WHERE deleted_at IS NULL;

Nie jest to równoważne z UNIQUE (tenant_id, email, deleted_at). PostgreSQL traktuje wartości NULL zgodnie ze swoimi zasadami unikalności, a dodanie znacznika czasu usunięcia zmienia wymuszaną tożsamość. Sprawdź dokładne przypadki duplikatów na przygotowanych danych, zamiast wyciągać wnioski z listy kolumn.

Dopuszczalność NULL to decyzja biznesowa

Ustawiaj NOT NULL tylko wtedy, gdy domena wymaga wartości dla każdego prawidłowego wiersza i każda ścieżka zapisu potrafi ją dostarczyć. Projekt ekranu to słaby dowód. Wymagane pole w obecnym formularzu nic nie mówi o importach, wersjach roboczych, wierszach tworzonych przez system ani danych historycznych.

Sprawdź osobno cztery stany: źródło pominęło pole, źródło jawnie wysłało null, źródło wysłało pustą wartość i źródło podało wartość znaczącą. API JSON, formularze, importy CSV i PostgreSQL mogą traktować te stany inaczej. Jeśli aplikacja scala wszystkie cztery przed wstawieniem rekordu, przegląd schematu powinien ujawnić tę decyzję, zamiast udawać, że rozstrzygnęła ją baza.

Wartości domyślne wymagają takiej samej uwagi. Domyślna wartość jest używana, gdy INSERT pomija kolumnę. Nie naprawia jawnego NULL i nie dowodzi prawdziwości wartości. country_code DEFAULT 'US' jest niebezpieczne, jeśli kraj może być nieznany. Wiersz zawiera wtedy pewne kłamstwo, któremu mogą zaufać raporty i logika zgodności.

Częsta wygenerowana migracja dodaje wymaganą kolumnę jedną instrukcją:

ALTER TABLE customers
    ADD COLUMN account_type text NOT NULL DEFAULT 'standard';

Instrukcja może się wykonać, lecz każdy historyczny klient staje się standardowy bez żadnego dowodu. Bezpieczniejsza sekwencja dodaje kolumnę dopuszczającą NULL, wyprowadza wartości ze znanych danych, mierzy nierozstrzygnięte wiersze, blokuje nowe pominięcia w zapisach aplikacji i dopiero wtedy dodaje NOT NULL, jeśli pozwala na to domena. Jeżeli nieznana wartość nadal jest prawidłowa, zachowaj NULL i określ, jak zapytania oraz interfejsy mają ją wyświetlać.

PostgreSQL oferuje przydatne rozdzielenie dla części ograniczeń. CHECK lub klucz obcy można dodać jako NOT VALID, co pozwala uniknąć sprawdzania wszystkich istniejących wierszy podczas tworzenia, a później zweryfikować je przez VALIDATE CONSTRAINT. Dokumentacja opisuje to jako sposób odroczenia początkowego skanowania tabeli. Nie jest to pozwolenie na ignorowanie starych naruszeń: nowe zapisy są egzekwowane, a krok walidacji nadal musi się udać przed zatwierdzeniem.

Przed zaostrzeniem dopuszczalności NULL uruchom zapytanie rozkładu, które pokaże rzeczywiste kategorie:

SELECT
    count(*) AS total,
    count(*) FILTER (WHERE account_type IS NULL) AS nulls,
    count(*) FILTER (WHERE account_type = '') AS empty_strings,
    count(*) FILTER (WHERE account_type NOT IN
        ('standard', 'partner', 'internal')) AS unexpected
FROM customers;

Wartość domyślna wygenerowana przez AI może sprawić, że po migracji to zapytanie będzie wyglądało dobrze. Uruchom je także przed uzupełnieniem danych i zachowaj wynik. W przeciwnym razie stracisz dowód potrzebny do odróżnienia wartości wyprowadzonych od wymyślonych.

Indeksy powinny odpowiadać obserwowanym wzorcom dostępu

Zachowaj drogę powrotu
Zachowaj możliwość wycofania zmian, gdy wygenerowana modyfikacja musi wrócić do znanego stanu aplikacji.

Zatwierdzaj indeks, gdy wspiera znane zapytanie, wymusza jawną zasadę unikalności albo spełnia wymóg operacyjny. Indeksowanie każdej kolumny wyglądającej na identyfikator marnuje miejsce i zwiększa pracę przy zapisach, a brak jednego ważnego indeksu złożonego może zamienić zwykłą stronę listy w coraz dłuższe skanowanie.

Zacznij od zapytań, które wygenerowana aplikacja faktycznie wysyła. Zapisz kolumny filtrów, granicę dzierżawcy, kolumny złączeń, sortowanie i oczekiwaną wielkość wyniku. Dla strony ostatnich zamówień ten kształt zapytania ma większe znaczenie niż diagram tabel:

SELECT id, order_number, status, created_at
FROM orders
WHERE tenant_id = $1
  AND status = $2
ORDER BY created_at DESC
LIMIT 50;

Indeks tylko na tenant_id może nadal przeglądać wiele wierszy dzierżawcy i je sortować. Indeks na (tenant_id, status, created_at DESC) lepiej odpowiada temu wzorcowi dostępu. Kolejność kolumn nie jest konkursem popularności, lecz wynika z warunków równości, warunków zakresowych, sortowania i selektywności rzeczywistego zapytania.

Uruchamiaj EXPLAIN (ANALYZE, BUFFERS) na reprezentatywnych danych, ale nie traktuj jednej małej próbki jako dowodu wydajności. PostgreSQL może słusznie wybrać skan sekwencyjny dla małej tabeli. Weryfikacja powinna potwierdzić, że zamierzony indeks istnieje i że próba w skali produkcyjnej daje planerowi realistyczny wybór. Nigdy nie wyłączaj skanów sekwencyjnych, by sztucznie uzyskać skan indeksu na potrzeby zatwierdzenia.

Klucze obce niosą jeszcze jedną częstą niespodziankę: PostgreSQL indeksuje wskazywane kolumny klucza głównego lub unikalnego, ale nie tworzy automatycznie indeksu na odwołujących się kolumnach podrzędnych. Usunięcie lub aktualizacja rodzica może więc skanować tabelę podrzędną, aby sprawdzić odwołania. Złączenia z dziecka do rodzica także mogą potrzebować tego indeksu. Oceń każdą relację według spodziewanych odczytów i zmian rodzica.

Odrzucaj zduplikowane i nieużywane indeksy w początkowej propozycji. (tenant_id, status) może być zbędne, gdy istnieje już odpowiedni indeks (tenant_id, status, created_at), choć szczegóły obciążenia mogą zmienić tę ocenę. Porównuj definicje, a nie nazwy. Generatory AI często tworzą jeden indeks na funkcję i nie zauważają, że kilka funkcji potrzebowało tych samych początkowych kolumn.

W przypadku istniejących, obciążonych baz pamiętaj, że CREATE INDEX CONCURRENTLY nie może działać wewnątrz bloku transakcji, wymaga więcej pracy i po niepowodzeniu może pozostawić nieprawidłowy indeks. Dokumentacja PostgreSQL opisuje te różnice operacyjne. Framework migracji, który owija każdą migrację w transakcję, potrzebuje jawnego wyjątku i procedury sprzątania, a nie optymistycznej zamiany słowa kluczowego.

Klucze obce wymagają zasad własności i usuwania

Klucz obcy jest poprawny dopiero po ustaleniu przez zespół, czy relacja oznacza własność, odwołanie, opcjonalny kontekst czy przypisanie historyczne. Podobnie wyglądające kolumny mogą wymagać przeciwnych zachowań przy usuwaniu.

Rozważ projects.owner_user_id, invoices.customer_id i audit_events.actor_user_id. Projekt może zmienić właściciela. Faktura może musieć przetrwać zamknięcie konta klienta. Zdarzenie audytowe może zachować dawny identyfikator osoby nawet po usunięciu danych tożsamości. Zastosowanie ON DELETE CASCADE do wszystkich trzech tylko dlatego, że odwołują się do users, zapisałoby niszczącą fikcję.

Stosuj CASCADE, gdy dziecko nie ma znaczenia bez rodzica, a usunięcie rodzica rzeczywiście oznacza usunięcie całej całości. Pozycje zamówienia często spełniają ten warunek. Rekordy płatności, wystawione dokumenty, importy, logi i dowody moderacji często go nie spełniają. Dla nich lepsze może być odrzucenie, archiwizacja, kontrolowana anonimizacja albo referencja dopuszczająca NULL wraz z zachowanymi polami migawki.

SET NULL także wymaga przeglądu znaczeniowego. Zachowuje wiersz podrzędny, ale usuwa bezpośrednią relację. Jeśli pracownicy muszą później wyjaśnić, które konto utworzyło raport, referencja null może nie wystarczyć. Zachowanie nieidentyfikującego tokenu historycznego albo migawki może utrzymać rozliczalność bez zachowywania wszystkich danych osobowych, lecz dokładny wybór retencji należy do polityki produktu, a nie do zgadywania przez AI.

Sprawdź kardynalność w obu kierunkach. Generator może zamodelować jeden do jednego przez umieszczenie klucza obcego bez ograniczenia unikalności, po cichu pozwalając na wiele wierszy podrzędnych. Może też wymusić unikalność tam, gdzie historia wymaga kilku wersji. Napisz przypadki testowe dla rodzica z zerem, jednym i kilkoma dziećmi, a następnie określ, które inserty powinny przejść.

Ograniczenia odraczalne wymagają konkretnego powodu. Mogą pomóc, gdy transakcja musi chwilowo naruszyć kolejność referencji albo aktualizować wzajemnie zależne wiersze, lecz ustawianie każdego klucza obcego jako odroczonego przesuwa błędy do momentu zatwierdzenia i utrudnia ich znalezienie. Zachowaj natychmiastowe egzekwowanie, jeśli rzeczywista sekwencja transakcji nie wymaga odroczenia.

Po zastosowaniu migracji w bazie próbnej sprawdź katalog:

SELECT
    conname,
    contype,
    convalidated,
    pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'public.orders'::regclass
ORDER BY conname;

Reprezentatywny wynik ma taki kształt:

       conname        | contype | convalidated | definition
----------------------+---------+--------------+---------------------------------------
 orders_pkey          | p       | t            | PRIMARY KEY (id)
 orders_customer_fk   | f       | t            | FOREIGN KEY (customer_id) REFERENCES customers(id)
 orders_total_check   | c       | t            | CHECK ((total_cents >= 0))

Porównaj zwrócone definicje z zatwierdzonymi zasadami własności. Sam sukces migracji nie ujawnia brakującej akcji, nieoczekiwanego odroczenia ani niezaweryfikowanego ograniczenia.

Destrukcyjne zmiany ukrywają się w rozsądnym SQL

Dbaj o możliwość przeglądu migracji
Eksportuj wygenerowany kod źródłowy, aby zespół mógł sprawdzać migracje razem z kodem aplikacji.

Sprawdzaj migrację jak przekształcenie danych, ponieważ pozornie schludny DDL może odrzucić znaczenie bez użycia oczywistego DROP TABLE. Najpierw szukaj bezpośredniego niszczenia, potem sprawdzaj rzutowania, uzupełnianie danych, przepisywanie, zmiany nazw i zastępowanie ograniczeń.

Zmiana nazwy i usunięcie są operacyjnie różne, nawet gdy końcowy schemat wygląda identycznie. Jeśli surname zmienia się w family_name, zmiana nazwy wierniej zachowuje dane i zależności. Usunięcie starej kolumny i dodanie nowej daje ten sam diagram, lecz zeruje wszystkie wartości. Wygenerowane migracje często wyciągają wniosek o stanie końcowym bez rozumienia ciągłości.

Zmiany typów wymagają przykładowych konwersji i przypadków odrzucenia. Zmiana identyfikatorów tekstowych na liczby całkowite może usunąć zera wiodące albo odrzucić identyfikatory mieszane. Zmniejszenie precyzji liczbowej może zaokrąglić wartości. Konwersja znaczników czasu wymaga jawnego założenia strefy czasowej. Przetestuj rzeczywiste wyrażenie USING na wartościach minimalnych, maksymalnych, null, nieprawidłowych i historycznie nietypowych przed zmianą kolumny.

Traktuj te operacje jako wymagające pisemnego uzasadnienia: usunięcie tabeli lub kolumny, zmianę typu przez stratne rzutowanie, zastąpienie wypełnionej kolumny, dodanie CASCADE, ustawienie NOT NULL po wygenerowanym uzupełnieniu danych oraz przebudowę unikalności z innymi kolumnami. Sprawdzaj też surowy SQL osadzony w wygenerowanych funkcjach lub wywołaniach migracji. Wyszukiwanie tekstu jest filtrem początkowym, a nie całym przeglądem.

Jeden wzorzec błędu pojawia się wielokrotnie. Znane dane zawierają kontakty z opcjonalnymi firmami, ale przykładowy ekran pokazuje tylko kontakty biznesowe. Generator ustawia contacts.company_id jako NOT NULL i wstawia wygenerowaną firmę o nazwie Unknown dla niedopasowanych wierszy. Migracja przechodzi, liczności się zgadzają, a każdy klucz obcy się waliduje. Dane są nadal błędne: kontakty indywidualne wyglądają teraz jak należące do firmy, raporty grupują niepowiązane osoby, a usunięcie zastępczej firmy może kaskadowo usunąć prawdziwe kontakty.

Naprawą nie jest kolejna wartość domyślna. Przywróć stan źródłowy, ustaw relację jako dopuszczającą NULL, migruj wyłącznie dopasowania oparte na dowodach i dodaj asercję, że zbiór niedopasowanych pozycji jest równy znanym kontaktom indywidualnym. Dlatego przypadki znaczeniowe muszą zapisywać oczekiwane relacje, a nie tylko liczności wierszy.

Narzędzia do porównywania schematów pomagają, ale sprzeciwiam się zatwierdzaniu na podstawie samej różnicy. Takie zalecenie jest popularne, bo różnica jest zwięzła i łatwa do sprawdzenia. Nie może być jedyną bramką, ponieważ pokazuje zmianę strukturalną, a nie pochodzenie uzupełnionych wartości, granice transakcji, zachowanie blokad ani prawdę po migracji.

Próba musi udowodnić wyniki i zachowanie przy błędach

Uruchom pełną migrację na jednorazowym odtworzeniu znanego zestawu danych, a następnie przetestuj zarówno zamierzony wynik, jak i ścieżkę przerwaną lub odrzuconą. Nowa pusta baza pomaga wychwycić błędy kolejności, lecz nie ujawni stratnej konwersji, nieprawidłowych danych historycznych ani wolnej walidacji.

Użyj tej sekwencji próby jako artefaktu wydania:

  1. Przywróć zestaw danych sprzed zmiany do odizolowanej bazy i zapisz liczności wierszy oraz asercje znaczeniowe.
  2. Zapisz bieżący schemat, zastosuj dokładny artefakt migracji i zachowaj cały wynik wraz z czasem oraz granicami transakcji.
  3. Uruchom kontrole katalogu, asercje mapowania, testy odrzucania przez ograniczenia oraz reprezentatywne zapytania aplikacji.
  4. Porównaj ważne wartości z zapisanymi oczekiwaniami, w tym kategorie niedopasowane i NULL.
  5. Przećwicz udokumentowaną metodę odzyskiwania, a potem ponownie uruchom asercje sprzed migracji na odzyskanej bazie.

Zapisz zrzut samego schematu przed i po:

pg_dump --schema-only --no-owner --no-privileges \
  --dbname "$DATABASE_URL" > schema.sql

Sprawdź tabele, sekwencje, indeksy, ograniczenia, funkcje, wyzwalacze, rozszerzenia i uprawnienia istotne dla aplikacji. Różnica modeli ORM może pominąć obiekty bazy, których aplikacja nie modeluje, szczególnie wyzwalacze, indeksy wyrażeń, indeksy częściowe i ręcznie zainstalowane funkcje.

Dodaj testy negatywne dowodzące, że ograniczenia odrzucają nieprawidłowe stany. Transakcja testowa może spróbować nieprawidłowego insertu, a potem wycofać go niezależnie od wyniku:

BEGIN;

INSERT INTO order_items (order_id, quantity, unit_price_cents)
VALUES (1001, 0, 2500);

ROLLBACK;

Oczekiwany wynik powinien wskazywać naruszone ograniczenie, na przykład:

ERROR:  new row for relation "order_items" violates check constraint "order_items_quantity_check"
DETAIL:  Failing row contains (..., 0, 2500, ...).

Nie porównuj całego tekstu błędu we wszystkich środowiskach, ponieważ szczegóły mogą się różnić. W automatycznych testach sprawdzaj SQLSTATE albo tożsamość ograniczenia, a dla recenzenta zachowaj czytelny wynik.

Zmierz blokady i czas trwania na zestawie danych wystarczająco dużym, by przypominał planowane wdrożenie. Operacja, która kończy się natychmiast na pięćdziesięciu wierszach, może blokować zapisy przy walidacji milionów. W przypadku pierwszej pustej bazy produkcyjnej bezpośrednie ryzyko jest mniejsze, ale próba nadal testuje importowane dane początkowe i tworzy punkt odniesienia dla późniejszych zmian.

Odzyskiwanie wymaga czegoś więcej niż migracji wstecznej

Przećwicz odzyskiwanie danych
Korzystaj ze snapshotów i wycofywania zmian jako elementów przetestowanej ścieżki odzyskiwania aplikacji wygenerowanych automatycznie.

Odzyskiwanie jest wiarygodne tylko wtedy, gdy przywraca dane i zgodność aplikacji w czasie, na jaki usługa może sobie pozwolić. Migracja wsteczna, która odtwarza usunięte kolumny, nie przywraca ich dawnych wartości.

Wybierz jednostkę odzyskiwania przed wykonaniem migracji. Dla pustej początkowej bazy może być dopuszczalne usunięcie i ponowne utworzenie bazy, jeśli nie zaczęły się jeszcze zapisy użytkowników. Gdy istnieją prawdziwe zapisy, odzyskiwanie może wymagać migawki bazy, kopii logicznej, zachowanych starych kolumn albo naprawy w przód. Właściwa metoda zależy od tego, ile nowych danych może nadejść w trakcie i po migracji.

Przetestuj polecenia odtwarzania i poświadczenia, zanim na nich polegasz. Kopia zapasowa, której operator wdrożenia nie potrafi przywrócić, nie jest planem odzyskiwania. Odtwórz ją do osobnej bazy, zweryfikuj własność i rozszerzenia, a potem uruchom te same znane asercje, których użyto przed migracją.

Migawki i wycofywanie transakcji rozwiązują różne awarie. Transakcja może cofnąć instrukcje, gdy migracja nie powiedzie się przed zatwierdzeniem, pod warunkiem że każda operacja uczestniczy w tej transakcji. Migawka może przywrócić całą bazę do wcześniejszego stanu, ale może przy tym odrzucić prawidłowe zapisy dokonane po migawce. Żaden z tych mechanizmów nie uzgadnia automatycznie tych zapisów.

Gdy niepewność pozostaje, preferuj zmiany addytywne. Dodaj nową kolumnę lub tabelę, skopiuj dane według mierzalnych zasad, w razie potrzeby uruchom obie ścieżki kodu przez kontrolowany okres i usuń starą strukturę dopiero po weryfikacji. Podejście rozszerzania i kurczenia wymaga dodatkowej pracy, ale zachowuje dowody. Pozostawienie kolumny pod zmienioną nazwą przez jedno wydanie często kosztuje mniej niż odtwarzanie jej z logów.

Zapisz z wyprzedzeniem warunki uruchamiające odzyskiwanie. Przykłady to nieudana asercja znaczeniowa, nieoczekiwane niedopasowane rekordy, nieprawidłowe ograniczenie, migracja przekraczająca zatwierdzone okno blokady albo błędy aplikacji spowodowane niezgodnością wersji. Operator nie powinien wymyślać decyzji, gdy użytkownicy czekają.

Zapisz moment, po którym przywrócenie starej bazy wymaga też przywrócenia starej aplikacji. Nowa aplikacja może zależeć od nowej kolumny, a stara aplikacja może odrzucić nową wartość enum albo zapisać stary kształt. Odzyskiwanie bazy i aplikacji musi używać zgodnych wersji.

Zatwierdzenie wymaga dowodów, nie pewności siebie

Zatwierdź pierwszą migrację tylko wtedy, gdy inna osoba potrafi na podstawie zachowanych artefaktów odtworzyć, dlaczego jest bezpieczna. Pewność wynikająca z czystego przeglądu kodu albo dopracowanego wygenerowanego interfejsu nie przetrwa pierwszej niewyjaśnionej rozbieżności danych.

Zapis zatwierdzenia powinien zawierać wywnioskowane założenia, macierz mapowania tabel, tożsamość znanego zestawu danych, różnicę schematu, dokładną migrację, zapytania walidacyjne i wyniki, testy odrzuconych danych wejściowych, uzasadnienie indeksów, uzasadnienia operacji destrukcyjnych oraz przetestowaną procedurę odzyskiwania. Wskaż recenzenta i zachowaj nierozstrzygnięte decyzje jako blokady, zamiast ukrywać je w historii czatu.

Gdy aplikacja powstaje w Koder.ai, użyj trybu planowania, aby zapisać te decyzje dotyczące schematu przed zezwoleniem na migrację, eksportuj kod źródłowy do przeglądu i traktuj snapshoty oraz wycofywanie zmian jako narzędzia odzyskiwania, które nadal wymagają próby na znanym zestawie danych.

Nie pozwalaj generatorowi zatwierdzać własnych wniosków przez ponowne generowanie kodu, dopóki testy nie przejdą. Taka pętla może dopasować aplikację do błędnego schematu, zamiast poprawić model. Człowiek musi zdecydować, czy baza odpowiada domenie, szczególnie w kwestiach tożsamości, usuwania, retencji i nieznanych wartości.

Końcowe zapytanie zatwierdzające powinno być nudne. Każdy znany fakt prowadzi do jednego oczekiwanego wyniku, każde ograniczenie odrzuca zamierzony kontrprzykład, każda operacja destrukcyjna ma uzasadnienie, a odtworzenie reprodukuje asercje sprzed migracji. Jeśli dowody potrzebują przekonującego wyjaśnienia, aby usprawiedliwić rozbieżność, zatrzymaj migrację. PostgreSQL będzie precyzyjnie egzekwować schemat, także te części, które generator odgadł błędnie.

Często zadawane pytania

Co sprawdzić w schemacie PostgreSQL wygenerowanym przez AI?

Sprawdź wygenerowany DDL, operacje migracji oraz założenia stojące za jednym i drugim. Poprawny końcowy schemat może powstać przez migrację, która usuwa dane, blokuje zapisy lub tworzy mylące wartości domyślne.

Jak duży powinien być zestaw danych do weryfikacji schematu?

Użyj małego zestawu danych zawierającego zwykłe rekordy, wartości graniczne, brakujące relacje, duplikaty, wartości NULL, puste ciągi i nietypowe dane historyczne. Jego celem nie jest skala, lecz podważenie założeń generatora, zanim zrobią to dane produkcyjne.

Czy udana testowa migracja dowodzi, że schemat jest bezpieczny?

Nie. Udana migracja dowodzi tylko, że PostgreSQL zaakceptował instrukcje dla tego stanu bazy. Nie dowodzi, że mapowanie tabel jest poprawne, dane zachowały znaczenie, indeksy obsługują rzeczywiste zapytania ani że odzyskiwanie działa.

Kiedy kolumna PostgreSQL powinna mieć NOT NULL?

Pole powinno mieć NOT NULL wyłącznie wtedy, gdy każda prawidłowa pozycja ma wartość, a aplikacja potrafi ją podać przy każdej ścieżce zapisu. Nie stosuj zmyślonej wartości domyślnej tylko po to, by spełnić ograniczenie, bo zastępuje ona widoczny brak danych wiarygodnie wyglądającą nieprawdą.

Lepiej użyć ograniczenia UNIQUE czy unikalnego indeksu?

Ograniczenie unikalności wyraża zasadę, do której mogą odwoływać się inne obiekty bazy, a PostgreSQL wspiera je indeksem. Unikalny indeks przydaje się, gdy unikalność dotyczy tylko wybranych wierszy lub wyrażeń, na przykład nieusuniętych rekordów albo znormalizowanych adresów e-mail.

Czy klucze obce PostgreSQL automatycznie tworzą indeksy?

Indeksuj kolumny używane do znajdowania wierszy nadrzędnych, filtrowania częstych zapytań, łączenia dużych tabel lub wymuszania unikalności. PostgreSQL nie tworzy automatycznie indeksu po stronie odwołującej się klucza obcego, dlatego sprawdzaj osobno kolumny tabel podrzędnych.

Kiedy ON DELETE CASCADE jest bezpieczne?

Używaj CASCADE tylko wtedy, gdy wiersz podrzędny nie ma samodzielnego znaczenia po zniknięciu wiersza nadrzędnego. Jeżeli usunięcie jest decyzją biznesową albo wiersz podrzędny jest dowodem, takim jak faktura czy wpis audytowy, odrzuć usunięcie albo obsłuż je jawnie.

Jak wykryć destrukcyjne zmiany w migracji?

Traktuj każde DROP, zawężające rzutowanie, przebudowę tabeli, nową wymaganą kolumnę i zastąpione ograniczenie jako potencjalnie niszczące dane. Przeszukaj tekst migracji, ale sprawdź też wygenerowane funkcje i surowy SQL, bo destrukcyjne działanie może się w nich ukrywać.

Jaki jest najbezpieczniejszy sposób testowania odzyskiwania po migracji?

Przywróć bazę sprzed zmiany w osobnej lokalizacji, uruchom tam migrację, wykonaj zapytania weryfikujące znaczenie danych i porównaj wyniki z zapisanymi oczekiwaniami. Testowanie wyłącznie migracji wstecznej nie wykryje utraconych danych i może dać fałszywe poczucie bezpieczeństwa.

Jakie dowody zachować po zatwierdzeniu schematu?

Przechowuj razem wygenerowany DDL, tekst migracji, różnicę schematu, zapytania weryfikacyjne i ich wyniki, procedurę odzyskiwania oraz dane recenzenta. Ten zapis wyjaśnia, co zatwierdzono, i pozwala przy kolejnej migracji wykryć zmienione założenia.

Related posts