LLM per la generazione di migration SQL: gestire l'evoluzione dello schema senza errori
A gennaio 2026 stavo lavorando a una migrazione di schema particolarmente complessa per un cliente del settore distribuzione: il database MySQL aveva 217 tabelle, 89 foreign key, e dovevo aggiungere un sistema di multi-tenancy retroattivo - il che significava aggiungere una colonna tenant_id a 43 tabelle, creare le foreign key verso la nuova tabella tenants, popolare i dati esistenti con il tenant corretto, e modificare tutti gli indici composti per includere il tenant_id. A mano, stimavo 8-10 ore di lavoro solo per scrivere le migration Laravel, più altre 4-6 ore per testarle su un dump di produzione. Ho deciso di usare Claude come co-pilota per la generazione delle migration, fornendogli il dump dello schema e le specifiche della modifica. Il risultato: le migration sono state generate in 45 minuti di interazione con l'LLM (incluse tre iterazioni di correzione), testate in 2 ore su un dump di produzione, e deployate in produzione con zero errori e zero perdita di dati. Un lavoro di 14 ore compresso in meno di 3 - ma con una nota critica: nessuna delle migration generate dall'LLM era corretta al primo tentativo senza supervisione umana.
Questo è il punto che voglio chiarire subito, perché il marketing dell'AI tende a nasconderlo: un LLM può generare migration SQL che sembrano corrette e che quasi sempre funzionano su un database vuoto, ma che falliscono miserabilmente su un database di produzione con 12 milioni di righe, vincoli di integrità referenziale, trigger, stored procedure e dati che non rispettano perfettamente le constraint dichiarate. L'LLM è un acceleratore potentissimo, non un sostituto del ragionamento umano sullo schema. In questo articolo ti mostro il workflow completo che uso - dal contesto da fornire al modello, alla validazione sistematica, fino ai pattern di errore che l'AI commette più frequentemente.
Come si fornisce il contesto dello schema a un LLM in modo efficace?
La qualità dell'output di un LLM dipende direttamente dalla qualità del contesto che gli fornisci. Per la generazione di migration SQL, il contesto minimo è il dump dello schema corrente - ma non basta passargli l'intero output di mysqldump --no-data. Un database con 217 tabelle produce uno schema di 3.000-5.000 righe di DDL che satura rapidamente la context window del modello e diluisce l'attenzione sulle tabelle rilevanti.
Il workflow che ho sviluppato prevede tre passaggi di preparazione del contesto:
Primo: estrarre solo le tabelle coinvolte nella modifica. Se devo aggiungere multi-tenancy a 43 tabelle, estraggo lo schema di quelle 43 tabelle più tutte le tabelle referenziate dalle loro foreign key. Questo riduce il contesto da 5.000 a 800-1.000 righe di DDL - abbastanza per dare al modello una visione completa delle relazioni, senza rumore.
# Estrae lo schema delle tabelle coinvolte nella modifica
# e delle tabelle referenziate dalle foreign key
TABELLE="ordini clienti prodotti giacenze listini fatture righe_ordine"
for tabella in ${TABELLE}; do
mysqldump --no-data --routines=false \
--skip-add-drop-table --skip-add-locks \
"${DB_NAME}" "${tabella}" >> /tmp/schema-contesto.sql
done
# Aggiungi le FK referenziate
mysql -N -e "
SELECT DISTINCT REFERENCED_TABLE_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = '${DB_NAME}'
AND TABLE_NAME IN ($(echo ${TABELLE} | sed "s/ /','/g" | sed "s/^/'/;s/$/'/"))
AND REFERENCED_TABLE_NAME IS NOT NULL
" >> /tmp/tabelle-ref.txtSecondo: aggiungere i dati di contesto numerico. L'LLM deve sapere quante righe ha ogni tabella, perché una ALTER TABLE su una tabella con 100 righe è istantanea, mentre sulla stessa tabella con 12 milioni di righe può bloccare il database per ore. Aggiungo al contesto l'output di SELECT TABLE_NAME, TABLE_ROWS FROM information_schema.TABLES per le tabelle coinvolte.
Terzo: specificare le regole di generazione. Questo è il prompt di sistema che uso per le migration Laravel:
Genera migration Laravel per le seguenti modifiche allo schema.
Regole obbligatorie:
1. Ogni migration deve avere un metodo down() funzionante che annulli
esattamente le modifiche del metodo up()
2. Per tabelle con più di 100.000 righe, usa ALTER TABLE con ALGORITHM=INPLACE
e LOCK=NONE dove possibile per evitare lock esclusivi
3. Per l'aggiunta di colonne NOT NULL con default, aggiungi prima la colonna
come nullable, popola i dati esistenti con UPDATE, poi cambia in NOT NULL
4. Le foreign key devono avere nomi espliciti con il pattern
fk_{tabella}_{colonna}_{tabella_ref}
5. Genera migration separate per: struttura (DDL), dati (DML), indici
6. Non usare Schema::rename() per colonne - usa raw ALTER TABLEQueste sei regole nascono da errori reali che ho incontrato in produzione e sono il risultato di un processo iterativo di calibrazione durato tre mesi. Inizialmente avevo un prompt con solo due regole (down funzionante e FK con nomi espliciti), e le migration generate erano sintatticamente corrette ma operativamente pericolose. Ogni volta che una migration generata dall'LLM causava un problema in test (lock prolungato, constraint violation, encoding mismatch), aggiungevo una regola al prompt. Dopo sei iterazioni, il set di sei regole produce migration che richiedono correzioni manuali nel 15-20% dei casi, rispetto al 70-80% iniziale. Il punto chiave è che il prompt non è un artefatto statico - è un documento vivente che evolve con l'esperienza del progetto, e la sua qualità è direttamente proporzionale al numero di errori che hai già incontrato e codificato come vincoli espliciti. La regola 3 è la più importante: un LLM tende a generare $table->unsignedBigInteger('tenant_id')->default(1) che su MySQL 8 con 12 milioni di righe significa riscrivere l'intera tabella con un lock esclusivo che blocca tutte le operazioni per 20-45 minuti. La sequenza corretta (aggiungi nullable, popola, converti a NOT NULL) mantiene il lock per millisecondi sulla modifica strutturale e gestisce il popolamento con UPDATE in batch senza lock.
I pattern di errore che l'LLM commette più frequentemente
Dopo sei mesi di utilizzo di Claude per la generazione di migration su cinque progetti diversi, ho catalogato i pattern di errore ricorrenti. Conoscerli è essenziale perché ti permette di fare review mirata sull'output del modello invece di leggere ogni riga come se fosse codice scritto da uno sconosciuto.
- Ordine delle operazioni con foreign key: l'LLM genera la creazione della colonna e della FK nella stessa migration, senza considerare che la colonna deve essere popolata con valori validi prima di aggiungere il vincolo FK. Se la colonna è nullable e la FK punta a una tabella dove l'id 0 non esiste, la migration fallisce con un errore di constraint violation. La soluzione è sempre: migration 1 per la colonna, migration 2 per il popolamento dati, migration 3 per la FK
- Lock impliciti sulle tabelle grandi: l'LLM non conosce il volume dei dati reali e genera
$table->index('tenant_id')su una tabella da milioni di righe senza specificareALGORITHM=INPLACE. Su MySQL 8 con InnoDB, la maggior parte delleADD INDEXusa INPLACE di default, maADD COLUMNcon default non banale può richiedere una copia della tabella. La verifica è: eseguire la migration su un dump di produzione e misurare il tempo con lock - Down() non funzionante: il metodo
down()generato dall'LLM è spesso sintattico ma non semantico - rimuove la colonna ma non gestisce il caso in cui la rimozione rompa un vincolo FK su un'altra tabella, o rimuove un indice che era usato da una query critica - Encoding e collation: l'LLM ignora quasi sempre che la colonna aggiunta deve avere la stessa collation delle colonne con cui verrà usata in JOIN e WHERE. Se la tabella esistente usa
utf8mb4_unicode_cie l'LLM genera la colonna con il default del database (che potrebbe essereutf8mb4_general_ci), i JOIN tra le due colonne saranno lenti perché MySQL deve fare una conversione implicita su ogni confronto
Un quinto pattern di errore che merita menzione separata riguarda i dati orfani. Quando l'LLM genera una FK su una colonna esistente che contiene valori che non esistono nella tabella referenziata (ad esempio un cliente_id = 0 che non corrisponde a nessun record nella tabella clienti), la migration fallisce con un errore di constraint violation. L'LLM non può sapere se i dati esistenti rispettano i vincoli che sta per creare, perché vede solo lo schema DDL, non i dati. La soluzione è aggiungere sempre una migration di pulizia prima della migration che crea la FK: un UPDATE che corregge o nullifica i valori orfani, seguito da un commento che spiega perché quei valori esistevano e cosa fare con i record coinvolti. Questa è esattamente il tipo di decisione di business che un LLM non può prendere e che richiede una conversazione con il team o con il cliente: "Ci sono 47 ordini con cliente_id = 0. Cosa rappresentano? Li assegniamo a un cliente generico, li eliminiamo, o li trattiamo come un caso speciale?"
La regola operativa è: non eseguire mai in produzione una migration generata da un LLM senza averla prima testata su un dump di produzione. Il test su un dump aggiornato di produzione non è opzionale e non è sostituibile con test su un database vuoto o con dati fittizi - è il gate di qualità che impedisce di scoprire i problemi elencati sopra quando il database di produzione è bloccato e gli utenti sono fermi. Nel mio profilo professionale trovi il dettaglio dell'esperienza nella gestione di schemi database complessi e nella pianificazione di migration zero-downtime, che è il contesto in cui l'LLM produce il massimo valore come acceleratore.
Il workflow completo: dal prompt alla produzione
Il ciclo che uso per ogni migration complessa segue questi passaggi in ordine rigoroso:
- Estrai il contesto dello schema (script bash sopra)
- Scrivi la specifica della modifica in linguaggio naturale ("Aggiungi multi-tenancy alle tabelle X, Y, Z con FK verso la tabella tenants. Il tenant_id per i dati esistenti è 1.")
- Fornisci contesto + specifica + regole all'LLM e richiedi le migration
- Review manuale dell'output: verifica ordine delle operazioni, presenza di down() funzionante, gestione dei lock su tabelle grandi, encoding
- Esegui le migration su un dump di produzione locale
- Misura il tempo di esecuzione di ogni singola migration - se qualcuna supera i 60 secondi, ottimizza con batch UPDATE o ALGORITHM=INPLACE
- Solo dopo che tutte le migration passano sul dump, esegui in staging
- Solo dopo staging validato, esegui in produzione con una finestra di manutenzione pianificata
Nel progetto di multi-tenancy, il ciclo completo - dalla prima interazione con l'LLM al deploy in produzione - ha richiesto meno di 3 ore. La stessa operazione senza LLM avrebbe richiesto 14 ore di lavoro manuale, con un rischio di errore umano significativamente più alto su 43 tabelle e 89 foreign key. L'LLM non ha eliminato la necessità di competenza sullo schema - ha eliminato il lavoro meccanico di scrivere DDL ripetitivo, permettendomi di concentrare il tempo sulla validazione e sul testing, dove la competenza umana è insostituibile. Ho descritto un approccio complementare nel mio articolo sul refactoring di codice PHP legacy, dove l'LLM è uno strumento di accelerazione anche per la riscrittura di controller e service class - sempre con supervisione umana, sempre con test, mai con fiducia cieca.
Se gestisci database MySQL o PostgreSQL con schemi complessi e hai in programma migration strutturali (aggiunta multi-tenancy, normalizzazione, partizionamento, ristrutturazione delle relazioni), l'approccio LLM-assistito può ridurre il tempo di sviluppo del 60-80% mantenendo la sicurezza del dato. Contattami per pianificare insieme la migration: partiamo dall'analisi dello schema esistente, definiamo la strategia di modifica, e generiamo le migration con validazione su dump di produzione prima di toccare un singolo byte in produzione.