Verifica dello schema PostgreSQL prima della prima migrazione
La verifica dello schema PostgreSQL individua mappature errate, vincoli deboli, indici mancanti e modifiche non sicure prima che la prima migrazione tocchi i dati.

Un builder AI può produrre SQL PostgreSQL valido e comunque dedurre il database sbagliato. La sintassi è la parte semplice. Gli errori pericolosi sono plausibili: una relazione facoltativa diventa obbligatoria, una stringa di stato riceve un vincolo CHECK incompleto, un'eliminazione si propaga a record che dovrebbero rimanere, oppure una migrazione ricrea una tabella e perde silenziosamente una colonna.
La verifica di uno schema PostgreSQL deve quindi testare separatamente il significato, il comportamento della migrazione e il ripristino. Approvo uno schema dedotto solo dopo che ha superato un set di dati noto, invarianti esplicite, query rappresentative, la revisione delle modifiche distruttive e una prova di ripristino. Se manca anche solo uno di questi elementi, la migrazione è ancora una proposta.
La prima migrazione merita questo controllo anche quando il database di produzione è vuoto. Gli errori iniziali nello schema diventano rapidamente rigidi, perché codice dell'applicazione, dati seed, report e migrazioni successive iniziano a dipendere da essi. Un controllo di quindici minuti prima della prima esecuzione costa quasi sempre meno che spiegare, sei mesi dopo, perché due concetti diversi condividono una sola colonna di testo nullable.
Uno schema dedotto è una specifica non attendibile
Tratta lo schema dedotto come una bozza di specifica, non come una verità eseguibile. Il builder ha visto prompt, schermate di esempio, record importati o codice dell'applicazione generato. Non ha visto ogni eccezione aziendale, regola di conservazione, importazione di massa, correzione dell'assistenza e pagamento non riuscito che il database finirà per contenere.
Inizia separando tre domande che i team confondono di frequente. La correttezza dello schema chiede se tabelle e vincoli modellano il dominio. La sicurezza della migrazione chiede se le operazioni proposte conservano i dati esistenti e mantengono utilizzabile il database mentre vengono eseguite. La preparazione al ripristino chiede se puoi tornare a uno stato noto dopo una modifica parziale o semanticamente errata. Superarne una dice poco sulle altre due.
Un'istruzione CREATE TABLE può descrivere la struttura finale desiderata e arrivarci comunque con operazioni non sicure. Supponiamo che il builder trasformi customer_name text in customer_id bigint. La chiave esterna finale può essere sensata, ma una migrazione che elimina la colonna del nome prima di associare i nomi storici ai clienti distrugge l'unica prova necessaria per quell'associazione. La revisione dello schema approva la destinazione, la revisione della migrazione esamina il viaggio.
Leggi il modello proposto ad alta voce nel linguaggio del dominio. Dì che ogni fattura appartiene a un solo cliente legale, invece di dire che invoices.customer_id fa riferimento a customers.id. La prima frase invita a obiezioni utili: possono esistere bozze prima della selezione del cliente, le fatture importate possono riferirsi a clienti archiviati e i documenti legali possono dover congelare il nome del cliente al momento dell'emissione. Il vocabolario SQL può nascondere questi disaccordi.
Richiedo una nota sulle ipotesi accanto a ogni tabella dedotta. Dovrebbe indicare cosa rappresenta una riga, come viene identificata, chi ne è proprietario, se può esistere senza il suo apparente padre e cosa significa eliminarla. Se il team non sa rispondere a questi punti, il builder ha indovinato un database che il team non ha progettato.
I record noti rivelano mappature errate delle tabelle
Un set di dati noto dovrebbe contenere record scelti per coprire i significati, perché un grande campione casuale spesso ripete lo stesso caso facile. Dieci record selezionati con cura possono rivelare più di diecimila righe quasi identiche del percorso felice.
Crea una matrice di mappatura prima di eseguire il DDL. Ogni riga della matrice dovrebbe seguire ciascun concetto di origine fino alla destinazione proposta e registrare il conteggio o il valore previsto. Per un'applicazione di ordini, l'artefatto potrebbe essere questo:
| Fatto noto | Destinazione proposta | Risultato previsto |
|---|---|---|
| L'ordine A ha due righe | orders e order_items | Una riga ordine e due righe figlie |
| L'ordine B non ha un account assegnato | orders.account_id | Una riga con account NULL |
| Due persone condividono un'email | contacts.email | Entrambe le righe rimangono, a meno che l'unicità non sia una regola dichiarata |
| Il codice prodotto ha zeri iniziali | products.code | Il valore di testo 00417 rimane invariato |
| Un ordine annullato conserva gli addebiti | orders e charges | Le righe degli addebiti rimangono dopo l'annullamento |
Questo intercetta gli errori di mappatura delle tabelle prima che i dettagli dei vincoli distolgano la revisione. I builder AI normalizzano spesso oggetti ripetuti in tabelle separate, scelta in genere sensata, ma la ripetizione non dimostra l'identità. Due indirizzi di spedizione con lo stesso testo possono essere istantanee storiche anziché riferimenti a una riga di indirizzo modificabile. Unirli significa che una modifica successiva dell'indirizzo riscrive la storia.
Si verifica anche l'errore opposto. Un builder può copiare i campi del cliente in ogni ordine perché la schermata li mostra insieme. Alcuni valori appartengono al cliente, mentre altri devono restare un'istantanea dell'ordine. Il progetto corretto può contenere sia customer_id sia campi del documento emesso come billing_name. Definire questa duplicazione ed eliminare una delle due parti fa perdere l'identità corrente o la verità storica.
Carica il set di dati noto in un database usa e getta usando lo stesso percorso di importazione o seed che userà l'applicazione. Poi scrivi asserzioni sui fatti, non solo sui conteggi di righe:
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;
Una forma superata dovrebbe essere esplicita:
order_a | order_a_items | order_b_unassigned
---------+---------------+---------------------
1 | 2 | t
Non accettare una differenza non spiegata solo perché l'applicazione generata continua a essere visualizzata. Un'interfaccia può nascondere padri duplicati, figli eliminati, codici troncati e valori predefiniti inventati. Riconcilia ogni fixture intenzionale prima di discutere il rilascio in produzione.
I vincoli devono codificare verità di dominio
Un vincolo del database dovrebbe rifiutare uno stato sempre non valido, indipendentemente dalla schermata, dall'API, dall'importazione o dallo script di riparazione che scrive la riga. Se una regola ha eccezioni o dipende da fatti esterni mutevoli, costringerla in un vincolo semplice spesso produce lavoro bloccato o dati inesatti.
Le chiavi primarie identificano le righe, ma non forniscono automaticamente un'identità aziendale significativa. Un ID bigint interno può coesistere con un numero d'ordine univoco nell'ambito del tenant. Se l'azienda afferma che i numeri d'ordine sono univoci per tenant, UNIQUE (tenant_id, order_number) esprime quella regola. Un vincolo univoco globale rifiuterebbe record legittimi, mentre l'assenza di vincolo consentirebbe ambiguità durante i tentativi ripetuti.
I vincoli CHECK sono adatti a fatti stabili della riga, come quantity > 0 o finished_at >= started_at. Il manuale di PostgreSQL spiega che il database presume che un'espressione CHECK sia immutabile per tutta la vita del vincolo. Per questo un CHECK che chiama una funzione il cui comportamento cambia in seguito può lasciare vecchie righe in violazione della regola apparente. Usa un'espressione fissa per una verità fissa. Inserisci una politica mutevole, come un insieme di valori attualmente consentiti gestito dagli amministratori, in una tabella referenziata o nel flusso dell'applicazione.
I vincoli di stato generati meritano sospetto. Un builder può esaminare gli esempi attuali e produrre:
status text NOT NULL
CHECK (status IN ('draft', 'active', 'closed'))
È corretto solo se questi sono tutti gli stati durevoli. Chiedi informazioni su record non riusciti, annullati, sospesi, importati e legacy sconosciuti. Se la macchina a stati sta ancora cambiando, una tabella di lookup può rendere esplicite le aggiunte, ma non sostituisce la convalida delle transizioni. Una riga autorizzata a contenere closed non dice nulla sul fatto che possa passare direttamente da draft a closed.
Usa l'unicità con intenzione. PostgreSQL implementa un vincolo univoco con un indice B-tree univoco, ma un indice univoco parziale esprime una regola diversa. L'eliminazione logica richiede spesso l'unicità solo tra le righe attive:
CREATE UNIQUE INDEX users_tenant_email_live_uq
ON users (tenant_id, lower(email))
WHERE deleted_at IS NULL;
Questo non è intercambiabile con UNIQUE (tenant_id, email, deleted_at). PostgreSQL tratta i valori NULL secondo le proprie regole di unicità, e aggiungere il timestamp di eliminazione modifica l'identità che viene imposta. Esamina gli esatti casi di duplicato con fixture invece di dedurre il comportamento dall'elenco delle colonne.
La nullabilità è una decisione aziendale
Imposta una colonna su NOT NULL solo quando il dominio richiede un valore per ogni riga legittima e ogni percorso di scrittura può fornirlo. Il progetto della schermata è una prova debole. Un campo obbligatorio nel modulo attuale non dice nulla su importazioni, bozze, righe generate dal sistema o record storici.
Esamina separatamente quattro stati: la fonte ha omesso il campo, la fonte ha inviato esplicitamente null, la fonte ha inviato un valore vuoto e la fonte ha fornito un valore significativo. API JSON, moduli, importazioni CSV e PostgreSQL possono gestire questi stati in modo diverso. Se l'applicazione riduce tutti e quattro a uno solo prima dell'inserimento, la revisione dello schema dovrebbe rendere visibile quella decisione invece di fingere che il database l'abbia risolta.
I valori predefiniti meritano la stessa attenzione. Un valore predefinito fornisce un valore quando un INSERT omette la colonna, non corregge un NULL esplicito e non dimostra che il valore sia vero. country_code DEFAULT 'US' è pericoloso se è possibile un paese sconosciuto. La riga ora contiene una menzogna sicura di sé di cui report e logiche di conformità potrebbero fidarsi.
Una migrazione generata comune aggiunge una colonna obbligatoria in una sola istruzione:
ALTER TABLE customers
ADD COLUMN account_type text NOT NULL DEFAULT 'standard';
L'istruzione può essere eseguita, ma ogni cliente storico diventa standard senza prove. Una sequenza più sicura aggiunge la colonna nullable, deriva i valori dai dati noti, misura le righe non risolte, blocca le nuove omissioni nelle scritture dell'applicazione e solo allora aggiunge NOT NULL, se il dominio lo supporta. Se lo stato sconosciuto rimane legittimo, conserva NULL e definisci come query e interfacce lo mostrano.
PostgreSQL offre una separazione utile per alcuni vincoli. Un CHECK o una chiave esterna possono essere aggiunti come NOT VALID, evitando di convalidare tutte le righe esistenti durante la creazione, e controllati in seguito con VALIDATE CONSTRAINT. Il manuale documenta questa possibilità per rinviare la scansione iniziale della tabella. Non è un permesso per ignorare le vecchie violazioni: le nuove scritture sono soggette al vincolo, mentre la convalida deve comunque riuscire prima dell'approvazione.
Prima di rendere più rigida la nullabilità, esegui una query di distribuzione che mostri le categorie effettive:
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;
Un valore predefinito generato dall'AI può far apparire pulita questa query dopo la migrazione. Eseguila anche prima del backfill e conserva il risultato. Altrimenti perdi le prove necessarie per distinguere i valori derivati da quelli inventati.
Gli indici devono rispondere a modelli di accesso osservati
Approva un indice quando supporta una query nota, impone una regola di unicità dichiarata o soddisfa un requisito operativo. Indicizzare ogni colonna che sembra un identificatore spreca spazio e lavoro di scrittura, mentre la mancanza di un importante indice composto può trasformare una normale pagina elenco in una scansione sempre più onerosa.
Inizia dalle query che l'applicazione generata esegue davvero. Registra le colonne di filtro, il confine del tenant, le colonne di join, l'ordinamento e la dimensione prevista del risultato. Per una pagina di ordini recenti, questa forma di query conta più del diagramma della tabella:
SELECT id, order_number, status, created_at
FROM orders
WHERE tenant_id = $1
AND status = $2
ORDER BY created_at DESC
LIMIT 50;
Un indice sul solo tenant_id potrebbe comunque esaminare molte righe del tenant e ordinarle. Un indice su (tenant_id, status, created_at DESC) corrisponde più da vicino a questo modello di accesso. L'ordine delle colonne non è una gara di popolarità: segue condizioni di uguaglianza, condizioni di intervallo, ordinamento e selettività nella query reale.
Esegui EXPLAIN (ANALYZE, BUFFERS) su dati rappresentativi, ma non considerare una piccola fixture come prova delle prestazioni. PostgreSQL può giustamente preferire una scansione sequenziale su una tabella minuscola. La verifica dovrebbe confermare che l'indice previsto esiste e che una prova con dimensioni simili alla produzione offre al planner una scelta realistica. Non disabilitare mai le scansioni sequenziali per produrre artificialmente una scansione dell'indice da approvare.
Le chiavi esterne creano un'altra sorpresa ricorrente: PostgreSQL indicizza le colonne primarie o univoche referenziate, ma non crea automaticamente un indice sulle colonne figlie referenzianti. L'eliminazione o l'aggiornamento di un padre potrebbe quindi analizzare la tabella figlia per controllare i riferimenti. Anche i join dal figlio al padre possono richiedere quell'indice figlio. Esamina ogni relazione in base alle letture previste e alle modifiche del padre.
Rifiuta indici duplicati e inutilizzati nella proposta iniziale. (tenant_id, status) può essere ridondante quando esiste già un indice adatto su (tenant_id, status, created_at), anche se i dettagli del carico di lavoro possono cambiare questa valutazione. Confronta le definizioni, non i nomi. I builder AI creano spesso un indice per funzionalità e non notano che diverse funzionalità hanno richiesto le stesse colonne iniziali.
Per database esistenti e molto attivi, ricorda che CREATE INDEX CONCURRENTLY non può essere eseguito dentro un blocco di transazione, richiede più lavoro e può lasciare un indice non valido in caso di errore. Il manuale di PostgreSQL illustra queste differenze operative. Un framework di migrazione che racchiude ogni migrazione in una transazione richiede un'eccezione esplicita e una procedura di pulizia, non una speranzosa sostituzione di parole chiave.
Le chiavi esterne richiedono proprietà e regole di eliminazione
Una chiave esterna è corretta solo dopo che il team ha deciso se la relazione esprime proprietà, riferimento, contesto facoltativo o attribuzione storica. Colonne dall'aspetto simile possono richiedere comportamenti opposti in caso di eliminazione.
Considera projects.owner_user_id, invoices.customer_id e audit_events.actor_user_id. Un progetto può cambiare proprietario. Una fattura potrebbe dover sopravvivere alla chiusura dell'account cliente. Un evento di audit può dover conservare l'identificatore precedente dell'autore anche dopo la rimozione dei dati di identità. Applicare ON DELETE CASCADE a tutti e tre perché fanno riferimento a users codificherebbe una finzione distruttiva.
Usa CASCADE quando il figlio non ha significato senza il padre e l'eliminazione del padre significa davvero eliminare l'intero aggregato. Le righe d'ordine spesso rientrano in questo caso. Record di pagamento, documenti emessi, importazioni, log e prove di moderazione spesso no. Per questi, possono essere più adatti il rifiuto, l'archiviazione, l'anonimizzazione controllata o un riferimento nullable abbinato a campi di istantanea conservati.
Anche SET NULL richiede una revisione semantica. Conserva la riga figlia ma cancella la relazione diretta. Se in seguito il personale deve spiegare quale account ha creato un report, un riferimento nullo potrebbe non bastare. Conservare un token storico non identificativo o un'istantanea può preservare la responsabilità senza mantenere tutti i dati personali, ma la scelta esatta di conservazione appartiene alla politica del prodotto, non a un'ipotesi dell'AI.
Controlla la cardinalità in entrambe le direzioni. Un builder può modellare una relazione uno a uno inserendo una chiave esterna senza un vincolo univoco, consentendo silenziosamente molte righe figlie. Può anche imporre unicità quando la cronologia richiede più versioni. Scrivi casi fixture per un padre con zero, uno e più figli, poi indica quali inserimenti devono riuscire.
I vincoli differibili richiedono una ragione specifica. Possono aiutare quando una transazione deve violare temporaneamente l'ordine dei riferimenti o aggiornare righe reciprocamente dipendenti, ma rendere differibile ogni chiave esterna sposta gli errori al commit e rende più difficile individuarli. Mantieni il controllo immediato, salvo quando una sequenza reale di transazioni richieda il rinvio.
Esamina il catalogo dopo aver applicato la migrazione al database di prova:
SELECT
conname,
contype,
convalidated,
pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'public.orders'::regclass
ORDER BY conname;
Un output rappresentativo ha una forma simile:
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))
Confronta le definizioni restituite con le regole di proprietà approvate. Il successo della migrazione da solo non rivela un'azione mancante, un rinvio imprevisto o un vincolo non convalidato.
Le modifiche distruttive si nascondono in SQL ragionevole
Esamina la migrazione come una trasformazione dei dati, perché un DDL apparentemente ordinato può scartare significato senza usare un evidente DROP TABLE. Cerca prima la distruzione diretta, poi esamina conversioni, backfill, riscritture, rinomini e sostituzioni di vincoli.
Un rinomina e un'eliminazione sono operativamente diversi anche quando lo schema finale sembra identico. Se surname diventa family_name, un rinomina conserva più fedelmente dati e dipendenze. Eliminare la vecchia colonna e aggiungere quella nuova produce lo stesso diagramma, ma svuota ogni valore. Le migrazioni generate deducono spesso lo stato finale senza comprendere la continuità.
Le modifiche di tipo richiedono conversioni di esempio e casi di rifiuto. Trasformare identificatori testuali in interi può rimuovere zeri iniziali o rifiutare identificatori misti. Ridurre la precisione numerica può arrotondare i valori. Convertire timestamp richiede un'ipotesi esplicita sul fuso orario. Testa l'espressione USING effettiva con valori minimi, massimi, null, non validi e storicamente insoliti prima di modificare la colonna.
Considera queste operazioni come bisognose di giustificazione scritta: eliminare una tabella o una colonna, cambiare un tipo mediante una conversione con perdita, sostituire una colonna popolata, aggiungere CASCADE, impostare NOT NULL dopo un backfill generato e ricostruire l'unicità con colonne diverse. Esamina anche il SQL grezzo incorporato in funzioni generate o callback di migrazione. La ricerca nel testo è un filtro iniziale, non l'intera revisione.
Un modello di errore ricorre spesso. I dati noti contengono contatti con aziende facoltative, ma la schermata di esempio mostra solo contatti aziendali. Il builder rende contacts.company_id NOT NULL e inserisce un'azienda generata chiamata Unknown per le righe senza corrispondenza. La migrazione riesce, i conteggi coincidono e ogni chiave esterna viene convalidata. I dati sono comunque errati: i contatti individuali ora sembrano appartenere a un'azienda, i report raggruppano insieme persone non correlate e l'eliminazione del segnaposto potrebbe propagarsi a contatti reali.
La soluzione non è un altro valore predefinito. Ripristina lo stato di origine, rendi nullable la relazione, migra solo le corrispondenze sostenute da prove e aggiungi un'asserzione che il set senza corrispondenza coincida con i contatti individuali noti. Ecco perché le fixture semantiche devono registrare le relazioni previste, non solo i conteggi attesi.
Gli strumenti di diff dello schema aiutano, ma sconsiglio di approvare basandosi solo su un diff. La raccomandazione è diffusa perché un diff è compatto e facile da esaminare. Come unico controllo è insufficiente: mostra la modifica strutturale, non la provenienza dei valori di backfill, i confini delle transazioni, il comportamento dei lock o la verità dopo la migrazione.
Una prova deve dimostrare i risultati e il comportamento in caso di errore
Esegui la migrazione completa su un ripristino usa e getta del set di dati noto, poi testa sia il risultato previsto sia un percorso interrotto o rifiutato. Un database nuovo e vuoto è utile per rilevare errori di ordinamento, ma non può esporre conversioni con perdita, righe storiche non valide o convalide lente.
Usa questa sequenza di prova come artefatto di rilascio:
- Ripristina il set di dati precedente alla modifica in un database isolato e registra i conteggi delle righe insieme alle asserzioni semantiche.
- Acquisisci lo schema corrente, applica l'artefatto esatto della migrazione e salva tutto l'output con tempi e confini delle transazioni.
- Esegui controlli del catalogo, asserzioni di mappatura, test di rifiuto dei vincoli e query rappresentative dell'applicazione.
- Confronta i valori importanti con le aspettative registrate, incluse le categorie senza corrispondenza e quelle null.
- Esegui il metodo di ripristino documentato, poi ripeti le asserzioni precedenti alla migrazione sul database ripristinato.
Acquisisci un dump del solo schema prima e dopo:
pg_dump --schema-only --no-owner --no-privileges \
--dbname "$DATABASE_URL" > schema.sql
Esamina tabelle, sequenze, indici, vincoli, funzioni, trigger, estensioni e privilegi rilevanti per l'applicazione. Un diff del modello ORM può omettere oggetti del database che l'applicazione non modella, in particolare trigger, indici di espressione, indici parziali e funzioni installate manualmente.
Aggiungi test negativi che dimostrino che i vincoli rifiutano stati non validi. Una transazione di test può tentare un inserimento non valido e fare rollback indipendentemente dall'esito:
BEGIN;
INSERT INTO order_items (order_id, quantity, unit_price_cents)
VALUES (1001, 0, 2500);
ROLLBACK;
L'output previsto dovrebbe indicare il vincolo violato, per esempio:
ERROR: new row for relation "order_items" violates check constraint "order_items_quantity_check"
DETAIL: Failing row contains (..., 0, 2500, ...).
Non confrontare l'intero testo dell'errore in ogni ambiente, perché i dettagli possono variare. Nelle prove automatizzate verifica lo SQLSTATE o l'identità del vincolo e conserva un output leggibile per il revisore.
Misura lock e durata su un set di dati abbastanza grande da somigliare alla distribuzione prevista. Un'operazione che termina subito su cinquanta righe può bloccare le scritture quando convalida milioni di righe. Per un database di produzione iniziale vuoto, il rischio immediato è minore, ma la prova testa comunque i dati seed importati e crea una base di riferimento per le modifiche successive.
Il ripristino richiede più di una migrazione inversa
Il ripristino è credibile solo quando riporta dati e compatibilità dell'applicazione entro il tempo che il servizio può tollerare. Una migrazione inversa che ricrea colonne eliminate non recupera i loro valori precedenti.
Scegli l'unità di ripristino prima dell'esecuzione. Per un database iniziale vuoto, eliminare e ricreare il database può essere accettabile se non sono iniziate scritture degli utenti. Quando esistono scritture reali, il ripristino può richiedere uno snapshot del database, un backup logico, vecchie colonne mantenute o una riparazione in avanti. Il metodo corretto dipende da quanti nuovi dati possono arrivare durante e dopo la migrazione.
Testa i comandi e le credenziali di ripristino prima di farvi affidamento. Un backup esistente che l'operatore di distribuzione non riesce a ripristinare non è un piano di recupero. Ripristina in un database separato, verifica proprietà ed estensioni, quindi esegui le stesse asserzioni note usate prima della migrazione.
Snapshot e rollback delle transazioni risolvono errori diversi. Una transazione può annullare le istruzioni quando una migrazione fallisce prima del commit, purché ogni operazione partecipi a quella transazione. Uno snapshot può riportare l'intero database a uno stato precedente, ma così facendo potrebbe scartare scritture legittime effettuate dopo lo snapshot. Nessuno dei due meccanismi riconcilia automaticamente tali scritture.
Preferisci modifiche additive quando rimane incertezza. Aggiungi la nuova colonna o tabella, copia i dati con regole misurabili, esegui entrambi i percorsi del codice per un periodo controllato se necessario e rimuovi la vecchia struttura solo dopo la verifica. Questo approccio di espansione e contrazione richiede lavoro aggiuntivo, ma conserva le prove. Mantenere una vecchia colonna rinominata per una release costa spesso meno che ricostruirla dai log.
Scrivi in anticipo le condizioni che attivano il ripristino. Alcuni esempi: un'asserzione semantica non riuscita, record senza corrispondenza inattesi, un vincolo non valido, una migrazione che supera la finestra di lock approvata o errori dell'applicazione causati da una mancata corrispondenza di versione. L'operatore non dovrebbe inventare la decisione mentre gli utenti aspettano.
Registra il momento dopo il quale il ripristino del vecchio database richiede anche il ripristino della vecchia applicazione. Una nuova applicazione può dipendere da una nuova colonna, mentre una vecchia applicazione può rifiutare un nuovo valore enum o scrivere nella vecchia forma. Il ripristino del database e dell'applicazione deve usare versioni compatibili.
L'approvazione richiede prove, non fiducia
Approva la prima migrazione solo quando un'altra persona può riprodurre, dagli artefatti conservati, il motivo per cui è sicura. La fiducia data da una revisione del codice pulita o da un'interfaccia generata ben rifinita non resiste alla prima discrepanza di dati senza spiegazione.
Il record di approvazione dovrebbe contenere le ipotesi dedotte, la matrice di mappatura delle tabelle, l'identità del set di dati noto, il diff dello schema, la migrazione esatta, query e risultati di convalida, test sugli input rifiutati, ragionamento sugli indici, giustificazioni per le operazioni distruttive e procedura di ripristino testata. Indica il revisore e conserva le decisioni irrisolte come blocchi invece di nasconderle nella cronologia della chat.
Quando un'applicazione viene generata in Koder.ai, usa la modalità di pianificazione per annotare queste decisioni sullo schema prima di consentire la migrazione, esporta il codice sorgente per la revisione e tratta snapshot e rollback come strumenti di ripristino che richiedono comunque una prova sul set di dati noto.
Non lasciare che il builder approvi la propria deduzione rigenerando il codice finché i test non riescono. Questo ciclo può far sì che l'applicazione si conformi a uno schema errato invece di correggere il modello. Una persona deve decidere se il database corrisponde al dominio, soprattutto su identità, eliminazione, conservazione e valori sconosciuti.
La query finale di approvazione dovrebbe essere noiosa. Ogni fatto noto corrisponde a un risultato previsto, ogni vincolo rifiuta il controesempio previsto, ogni operazione distruttiva ha una ragione e il ripristino riproduce le asserzioni precedenti alla migrazione. Se le prove richiedono una spiegazione persuasiva per giustificare una discrepanza, interrompi la migrazione. PostgreSQL applicherà lo schema con precisione, comprese le parti che il builder ha indovinato in modo errato.
Domande frequenti
Cosa devo controllare in uno schema PostgreSQL generato dall'AI?
Controlla il DDL generato, le operazioni della migrazione e le ipotesi alla base di entrambi. Uno schema finale corretto può comunque essere raggiunto con una migrazione che elimina dati, blocca le scritture o inventa valori predefiniti fuorvianti.
Quanto deve essere grande un set di dati per verificare lo schema?
Usa un piccolo set di dati che includa righe normali, valori limite, relazioni mancanti, duplicati, valori null, stringhe vuote e anomalie storiche. Il suo scopo non è il volume, ma confutare le ipotesi del builder prima che lo facciano i dati di produzione.
Una migrazione di test riuscita dimostra che lo schema è sicuro?
No. Una migrazione riuscita dimostra che PostgreSQL ha accettato le istruzioni in quello stato del database. Non dimostra che le mappature delle tabelle siano corrette, che i dati abbiano mantenuto il loro significato, che gli indici supportino le query reali o che il ripristino funzioni.
Quando una colonna PostgreSQL dovrebbe essere NOT NULL?
Un campo dovrebbe essere NOT NULL solo se ogni record legittimo ha un valore e l'applicazione può fornirlo in ogni percorso di scrittura. Non usare un valore predefinito inventato solo per soddisfare il vincolo: sostituisce dati mancanti visibili con dati falsi ma credibili.
Devo usare un vincolo univoco o un indice univoco?
Un vincolo univoco esprime una regola a cui possono fare riferimento altri oggetti del database, e PostgreSQL lo supporta con un indice. Un indice univoco è utile quando l'unicità si applica solo a righe o espressioni selezionate, come record non eliminati o indirizzi email normalizzati.
Le chiavi esterne PostgreSQL creano automaticamente indici?
Indicizza le colonne usate per trovare righe padre, filtrare query frequenti, unire tabelle grandi o imporre l'unicità. PostgreSQL non indicizza automaticamente il lato referenziante di una chiave esterna, quindi esamina separatamente le colonne figlie invece di presumere che il vincolo abbia già risolto il problema.
Quando ON DELETE CASCADE è sicuro?
Usa CASCADE solo quando la riga figlia non ha alcun significato indipendente dopo la scomparsa della riga padre. Se l'eliminazione è una decisione aziendale o la riga figlia è una prova, come una fattura o un record di audit, rifiuta o gestisci esplicitamente l'eliminazione.
Come posso rilevare modifiche distruttive in una migrazione?
Considera potenzialmente distruttivi ogni DROP, conversione con perdita di dati, riscrittura di tabella, nuova colonna obbligatoria e vincolo sostituito. Cerca nel testo della migrazione, ma controlla anche funzioni generate e SQL grezzo, perché possono nascondere comportamenti distruttivi.
Qual è il modo più sicuro per testare il ripristino dopo una migrazione?
Ripristina il database precedente alla modifica in un ambiente separato, esegui lì la migrazione, lancia query di verifica semantica e confronta i risultati con le aspettative registrate. Testare solo la migrazione inversa non rileva i dati eliminati e può dare una falsa sicurezza.
Quali prove dovrei conservare dopo aver approvato uno schema?
Conserva insieme il DDL generato, il testo della migrazione, il diff dello schema, le query e i risultati di verifica, la procedura di ripristino e l'identità del revisore. Questo archivio spiega cosa è stato approvato e permette alla revisione della migrazione successiva di rilevare ipotesi cambiate.