Impara come scrivere prompt per Claude Code per migrazioni PostgreSQL sicure: pattern expand-contract, backfill, rollback e cosa verificare in staging prima del rilascio.

Una modifica di schema PostgreSQL sembra semplice finché non incontra traffico reale e dati reali. La parte rischiosa di solito non è l'SQL in sé. È quando il codice dell'app, lo stato del database e la tempistica del deployment smettono di essere allineati.
La maggior parte dei guasti è pratica e dolorosa: un deploy si rompe perché codice vecchio tocca una nuova colonna, una migrazione prende un lock su una tabella calda e i timeout aumentano, oppure una modifica “veloce” elimina o riscrive dati senza farsi notare. Anche quando nulla va in crash, puoi spedire bug sottili come default sbagliati, vincoli rotti o indici che non hanno mai finito di essere creati.
Le migrazioni generate dall'AI aggiungono un altro livello di rischio. Gli strumenti possono produrre SQL valido che è comunque non sicuro per il tuo carico, per il volume dei dati o per il tuo processo di rilascio. Possono anche indovinare nomi di tabelle, non considerare lock di lunga durata o minimizzare il rollback perché le down migration sono difficili. Se usi Claude Code per le migrazioni, servono dei guardrail e un contesto concreto.
Quando in questo post si dice che una modifica è “sicura”, significa tre cose:
L'obiettivo è che le migrazioni diventino lavoro di routine: prevedibili, testabili e noiose.
Inizia con poche regole non negoziabili. Mantengono il modello focalizzato e ti impediscono di spedire una modifica che funziona solo sul tuo laptop.
Suddividi il lavoro in piccoli passi. Una modifica di schema, un backfill dei dati, un cambiamento dell'app e un passaggio di pulizia sono rischi diversi. Raggrupparli rende più difficile vedere cosa si è rotto e più difficile rollbackare.
Preferisci cambi additivi prima di quelli distruttivi. Aggiungere una colonna, un indice o una tabella è di solito a basso rischio. Rinominare o droppare oggetti è dove avvengono gli outage. Fai prima la parte sicura, sposta l'app e poi rimuovi la cosa vecchia solo quando sei sicuro che non venga più usata.
Fai in modo che l'app tolleri entrambe le forme per un po'. Il codice dovrebbe saper leggere sia la colonna vecchia che quella nuova durante il rollout. Questo evita la corsa comune in cui alcuni server eseguono codice nuovo mentre il database è ancora vecchio (o viceversa).
Tratta le migrazioni come codice di produzione, non come uno script rapido. Anche se stai costruendo con una piattaforma come Koder.ai (backend in Go con PostgreSQL, più client React o Flutter), il database è condiviso da tutto. Gli errori costano.
Se vuoi un set compatto di regole da mettere all'inizio di ogni richiesta SQL, usa qualcosa come:
Un esempio pratico: invece di rinominare una colonna da cui dipende l'app, aggiungi la nuova colonna, esegui lentamente il backfill, distribuisci codice che legge la nuova e poi la vecchia, e solo più tardi rimuovi la colonna vecchia.
Claude può scrivere SQL decente da una richiesta vaga, ma le migrazioni sicure hanno bisogno di contesto. Tratta il tuo prompt come un mini brief di progettazione: mostra ciò che esiste, spiega cosa non deve rompersi e definisci cosa significa “sicuro” per il tuo rollout.
Inizia incollando solo i fatti del database che contano. Includi la definizione della tabella più gli indici e i vincoli rilevanti (primary key, unique, foreign key, check, trigger). Se sono coinvolte tabelle correlate, includi anche quegli estratti. Un piccolo estratto accurato impedisce al modello di inventare nomi o di perdere un vincolo importante.
Aggiungi la scala reale. Conteggi di righe, dimensione delle tabelle, tasso di scrittura e traffico di picco dovrebbero cambiare il piano. “200M righe e 1k scritture/sec” è una migrazione diversa da “20k righe e soprattutto letture.” Includi anche la versione di Postgres e come le migrazioni girano nel tuo sistema (singola transazione vs passi multipli).
Descrivi come l'app usa i dati: le letture importanti, le scritture e i job in background. Esempi: “API legge per email”, “i worker aggiornano lo status”, o “i report scansionano per created_at”. Questo determina se ti serve expand/contract, feature flag e quanto è sicuro un backfill.
Infine, sii esplicito su vincoli e deliverable. Una struttura semplice funziona bene:
Chiedere sia l'SQL che un piano d'esecuzione forza il modello a pensare a ordine, rischi e cosa controllare prima del deploy.
Il pattern expand/contract cambia un database PostgreSQL senza rompere l'app mentre la modifica è in corso. Invece di uno switch rischioso singolo, fai in modo che il database supporti sia la forma vecchia che quella nuova per un periodo.
Pensalo come: aggiungi nuove cose in sicurezza (expand), sposta traffico e dati gradualmente, e solo dopo rimuovi i pezzi vecchi (contract). Questo è particolarmente utile per lavori assistiti dall'AI perché ti costringe a pianificare il mezzo caotico.
Un flusso pratico è:
Usa questo pattern ogni volta che gli utenti potrebbero essere ancora su codice vecchio mentre il database cambia. Include deployment multi-instance, app mobile che aggiornano lentamente o qualsiasi rilascio in cui una migrazione può durare minuti o ore.
Una tattica utile è pianificare due release. La Release 1 fa expand più compatibility così nulla si rompe se il backfill è incompleto. La Release 2 fa il contract solo dopo aver confermato nuovo codice e nuovi dati in posizione.
Copia questo template e riempi le parentesi. Spinge Claude Code a produrre SQL eseguibile, controlli per provarne il successo e un piano di rollback che puoi davvero seguire.
You are helping me plan a PostgreSQL expand-contract migration.
Context
- App: [what the feature does, who uses it]
- Database: PostgreSQL [version if known]
- Table sizes: [rough row counts], write rate: [low/medium/high]
- Zero/near-zero downtime required: [yes/no]
Goal
- Change: [describe the schema change]
- Current schema (relevant parts):
[paste CREATE TABLE or \d output]
- How the app will change (expand phase and contract phase):
- Expand: [new columns/indexes/triggers, dual-write, read preference]
- Contract: [when/how we stop writing old fields and remove them]
Hard safety requirements
- Prefer lock-safe operations. Avoid full table rewrites on large tables when possible.
- If any step can block writes, call it out explicitly and suggest alternatives.
- Use small, reversible steps. No “big bang” changes.
Deliverables
1) UP migration SQL (expand)
- Use clear comments.
- If you propose indexes, tell me if they should be created CONCURRENTLY.
- If you propose constraints, tell me whether to add them NOT VALID then VALIDATE.
2) Verification queries
- Queries to confirm the new schema exists.
- Queries to confirm data is being written to both old and new structures (if dual-write).
- Queries to estimate whether the change caused bloat/slow queries/locks.
3) Rollback plan (realistic)
- DOWN migration SQL (only if it is truly safe).
- If down is not safe, write a rollback runbook:
- how to stop the app change
- how to switch reads back
- what data might be lost or need re-backfill
4) Runbook notes
- Exact order of operations (including app deploy steps).
- What to monitor during the run (errors, latency, deadlocks, lock waits).
- “Stop/continue” checkpoints.
Output format
- Separate sections titled: UP.sql, VERIFY.sql, DOWN.sql (or ROLLBACK.md), RUNBOOK.md
Due righe extra che aiutano nella pratica:
RISK: blocks writes, più quando eseguirlo (fuori picco vs in qualsiasi momento).Piccole modifiche di schema possono comunque ferire se prendono lock lunghi, riscrivono grandi tabelle o falliscono a metà. Quando usi Claude Code per le migrazioni, chiedi SQL che eviti riscritture e mantenga l'app funzionante mentre il database recupera.
Aggiungere una colonna nullable è di solito sicuro. Aggiungere una colonna con default non-null può essere rischioso su versioni più vecchie di Postgres perché può riscrivere l'intera tabella.
Un approccio più sicuro è una modifica in due passi: aggiungi la colonna come NULL senza default, backfilla a batch, poi imposta il default per le nuove righe e aggiungi NOT NULL una volta che i dati sono puliti.
Se devi imporre un default immediatamente, richiedi una spiegazione sul comportamento dei lock per la tua versione di Postgres e un piano di fallback se l'esecuzione dura più del previsto.
Per indici su tabelle grandi, richiedi CREATE INDEX CONCURRENTLY così letture e scritture continuano. Richiedi anche la nota che non può essere eseguito dentro una transaction block, il che significa che il tuo strumento di migrazione deve supportare un passo non transazionale.
Per le foreign key, la strada più sicura è di solito aggiungere il vincolo come NOT VALID prima, poi validarlo più tardi. Questo rende il cambiamento iniziale più veloce pur imponendo la FK per le nuove scritture.
Quando rendi i vincoli più severi (NOT NULL, UNIQUE, CHECK), chiedi “clean first, enforce second.” La migrazione dovrebbe rilevare le righe bad, correggerle e solo dopo abilitare la regola più severa.
Se vuoi una checklist corta da incollare nei prompt, tienila stretta:
I backfill sono dove si vede la maggior parte dei dolori di migrazione, non l'ALTER TABLE. I prompt più sicuri trattano i backfill come job controllati: misurabili, riavviabili e gentili con la produzione.
Inizia con controlli di accettazione facili da eseguire e difficili da discutere: conteggi righe attesi, un tasso di null target e alcuni spot check (per esempio confrontare vecchio vs nuovo valore per 20 ID casuali).
Poi chiedi un piano di batching. I batch mantengono i lock brevi e riducono le sorprese. Una buona richiesta specifica:
Richiedi idempotenza perché i backfill falliscono a metà. L'SQL dovrebbe essere sicuro da rieseguire senza duplicare o corrompere i dati. Pattern tipici sono “update solo dove la nuova colonna è NULL” o una regola deterministica dove lo stesso input produce sempre lo stesso output.
Specificare come l'app resta corretta mentre il backfill gira. Se le nuove scritture continuano ad arrivare, ti serve un ponte: dual-write nel codice dell'app, un trigger temporaneo o logica di read-fallback (leggi nuovo quando presente, altrimenti vecchio). Dì quale approccio puoi distribuire in sicurezza.
Infine, costruisci pausa e ripresa nel design. Chiedi tracking del progresso e checkpoint, come una piccola tabella che memorizza l'ultima ID processata e una query che riporta progresso (righe aggiornate, ultima ID, ora di inizio).
Esempio: aggiungi users.full_name derivato da first_name e last_name. Un backfill sicuro aggiorna solo righe dove full_name IS NULL, gira per range di ID, registra l'ultima ID aggiornata e mantiene le nuove iscrizioni corrette tramite dual-write fino al completamento dello switch-over.
Un piano di rollback non è solo “scrivi una down migration.” Sono due problemi: annullare il cambiamento di schema e gestire i dati che sono cambiati mentre la nuova versione era live. Il rollback dello schema è spesso possibile. Il rollback dei dati spesso no, a meno che tu non lo abbia pianificato.
Sii esplicito su cosa significa rollback per la tua modifica. Se droppi una colonna o riscrivi valori in-place, richiedi una risposta realistica come: “Il rollback ripristina la compatibilità dell'app, ma i dati originali non possono essere recuperati senza uno snapshot.” Quell'onestà è ciò che ti salva.
Chiedi trigger di rollback chiari così nessuno discute durante un incidente. Esempi:
Richiedi l'intero pacchetto di rollback, non solo SQL: DOWN SQL (solo se sicuro), passi di app/config per restare compatibili e come fermare i job in background.
Questo pattern di prompt è di solito sufficiente:
Produce a rollback plan for this migration.
Include: down migration SQL, app config/code switches needed for compatibility, and the exact order of steps.
State what can be rolled back (schema) vs what cannot (data) and what evidence we need before deciding.
Include rollback triggers with thresholds.
Prima di spedire, cattura un leggero “safety snapshot” per poter confrontare prima/dopo:
Sii anche chiaro su quando non rollbackare. Se hai solo aggiunto una colonna nullable e l'app fa dual-write, una correzione forward (hotfix codice, pausa del backfill, poi riprendi) è spesso più sicura che revertare e creare più drift.
L'AI può scrivere SQL velocemente, ma non vede il tuo database di produzione. La maggior parte dei fallimenti avviene quando il prompt è vago e il modello riempie i vuoti.
Una trappola comune è saltare lo schema corrente. Se non incolli la definizione di tabella, indici e vincoli, l'SQL può mirare a colonne inesistenti o ignorare una regola di unicità che trasforma un backfill in un'operazione lenta e che prende lock.
Un altro errore è spedire expand, backfill e contract in un unico deploy. Questo toglie il tuo zoccolo di sicurezza. Se il backfill dura a lungo o fallisce a metà, ti ritrovi con un'app che si aspetta lo stato finale.
I problemi che emergono più spesso:
Un esempio concreto: “rinomina una colonna e aggiorna l'app.” Se il piano generato rinomina e backfilla in una sola transazione, un backfill lento può tenere lock e rompere il traffico live. Un prompt più sicuro forza batch piccoli, timeouts espliciti e query di verifica prima di rimuovere il percorso vecchio.
Staging è dove trovi problemi che non compaiono su un piccolo DB dev: lock lunghi, null inaspettati, indici mancanti e code path dimenticate.
Per prima cosa, controlla che lo schema corrisponda al piano dopo la migrazione: colonne, tipi, default, vincoli e indici. Uno sguardo veloce non basta. Un indice mancante può trasformare un backfill sicuro in un disastro.
Poi esegui la migrazione su un dataset realistico. Idealmente è una copia recente di produzione con campi sensibili mascherati. Se non puoi farlo, almeno imita il volume di produzione e i punti caldi (tabelle grandi, righe larghe, tabelle molto indicizzate). Registra i tempi di ogni passo così sai cosa aspettarti in produzione.
Una checklist breve per staging:
Infine, testa flussi utente reali, non solo SQL. Crea, aggiorna e leggi record toccati dalla modifica. Se expand/contract è il piano, conferma che entrambe le forme funzionano fino al cleanup finale.
Immagina di avere una colonna users.name che contiene nomi completi come “Ada Lovelace.” Vuoi first_name e last_name, ma non puoi rompere le iscrizioni, i profili o le schermate admin durante il rollout.
Inizia con un passo di expand che sia sicuro anche se non viene distribuito codice: aggiungi colonne nullable, conserva la colonna vecchia ed evita lock lunghi.
ALTER TABLE users ADD COLUMN first_name text;
ALTER TABLE users ADD COLUMN last_name text;
Poi aggiorna il comportamento dell'app per supportare entrambi gli schemi. Nella Release 1, l'app dovrebbe leggere dalle nuove colonne quando presenti, ricadere su name quando sono null e scrivere in entrambi così i nuovi dati rimangono consistenti.
Dopo viene il backfill. Esegui un job a batch che aggiorna piccoli blocchi di righe per esecuzione, registra progresso e può essere messo in pausa in sicurezza. Ad esempio: aggiorna gli users dove first_name è null in ordine crescente di ID, 1.000 alla volta, e registra quante righe sono cambiate.
Prima di stringere le regole, valida in staging:
first_name e last_name e continuano a impostare namenameusers non risultano sensibilmente più lenteLa Release 2 sposta le letture sulle nuove colonne solo. Solo dopo dovresti aggiungere vincoli (come SET NOT NULL) e droppare name, idealmente in un deploy separato più tardi.
Per il rollback, mantienilo semplice. L'app continua a leggere name durante la transizione e il backfill è fermabile. Se devi rollbackare la Release 2, torna a leggere name e lascia le nuove colonne finché non si è stabilizzato tutto.
Tratta ogni modifica come un piccolo runbook. Lo scopo non è un prompt perfetto. È una routine che impone i dettagli giusti: schema, vincoli, piano di esecuzione e rollback.
Standardizza cosa deve includere ogni richiesta di migrazione:
Decidi chi possiede ogni passo prima che qualcuno esegua SQL. Una semplice divisione evita “tutti pensavano che qualcun altro lo facesse”: gli sviluppatori possiedono il prompt e il codice di migrazione, ops gestisce la tempistica e il monitoraggio in produzione, QA verifica il comportamento in staging e i casi limite, e una persona è il go/no-go finale.
Se costruisci app via chat, può aiutare delineare la sequenza prima di generare SQL. Per i team che usano Koder.ai, Planning Mode è un posto naturale per scrivere quella sequenza e snapshot più rollback possono ridurre l'impatto se qualcosa di inaspettato accade durante il rollout.
Dopo il deploy, pianifica il cleanup di contract subito mentre il contesto è fresco, così colonne vecchie e codice di compatibilità temporaneo non restano per mesi.
Una modifica di schema è rischiosa quando codice dell'app, stato del database e tempistica di deployment smettono di essere allineati.
Modalità comuni di fallimento:
Usa un approccio expand/contract:
Perché il modello può generare SQL che è valido ma insicuro per il tuo carico di lavoro.
Rischi tipici legati all'AI:
Tratta l'output dell'AI come una bozza e richiedi piano di esecuzione, controlli e passi di rollback.
Includi solo i fatti da cui dipende la migrazione:
CREATE TABLE rilevanti (più indici, FK, UNIQUE/CHECK, trigger)Regola predefinita: separa.
Una divisione pratica:
NOT VALID)Accorpare tutto rende i guasti più difficili da diagnosticare e rollbackare.
Preferisci questo schema:
ADD COLUMN ... NULL senza default (veloce)NOT NULL solo dopo verificaAggiungere un default non-null immediato può essere rischioso su alcune versioni perché potrebbe riscrivere l'intera tabella. Se serve un default immediato, richiedi note sul comportamento dei lock e un fallback più sicuro.
Chiedi:
CREATE INDEX CONCURRENTLY per tabelle grandi/caldePer la verifica, includi un controllo rapido che l'indice esista e venga usato (per esempio confrontare un EXPLAIN prima/dopo in staging).
Usa NOT VALID prima, poi valida:
NOT VALID così il passo iniziale è meno disruptivoVALIDATE CONSTRAINT in un passo separato quando puoi monitorarloQuesto continua a imporre la FK per le nuove scritture, lasciandoti controllare quando avviene la validazione costosa.
Un buon backfill è a batch, idempotente e riavviabile.
Requisiti pratici:
WHERE new_col IS NULL)Obiettivo di rollback predefinito: ripristinare rapidamente la compatibilità dell'app, anche se i dati non vengono perfettamente annullati.
Un piano di rollback pratico deve includere:
Spesso il rollback più sicuro è tornare a leggere il campo vecchio lasciando le nuove colonne in posto.
Questo mantiene sia le versioni vecchie che quelle nuove dell'app funzionanti durante il rollout.
Questo evita supposizioni e forza il giusto ordine.
Questo rende i backfill sopravvivibili sotto traffico reale.