Migrazione dati — script e validazioni

Trasferire 62K analisi, 11K lotti, 109K conte senza perdere nulla

Mapping campo-per-campo

semiorto_analisi → test_germinabilita

LegacyTopSeedTrasformazione
idlegacy_idcopy + set source='diego'
id_lottolot_idlookup via mapLotto[legacy_id]
id_tecnicouser_idlookup via mapTecnico
id_tipologia_semetipologia_seme_idlookup via mapTipologia
ftftdirect
lotto (denormalizzato)SKIP (redundant: già in lots.num)
provalookup in tipologie_provamapping ENUM → id
tipologiaAnalisireplica_analisidirect (int 1/2/3)
tipologiaSeme (denormalizzato)tipologia_seme_legacycopy come backup (FK è in tipologia_seme_id)
calibraturacalibraturadirect
datadata_chiusuradirect (NULL=in corso)
dataArrivoLaboratoriodata_arrivo_laboratoriodirect
dataIniziodata_iniziodirect
scostamentoscostamentodirect ENUM
ururdirect
totaleGiorniDiProvatotale_giorni_provadirect
rifNumero/rifData/rifGerminabilita/rifProvaLaboratorio/germinabilitaProvaLaboratorioidem snake_casedirect
risultatorisultatodirect ENUM
vigorevigoredirect ENUM
tecnico (denormalizzato)SKIP
pillolepilloledirect
osservazioniosservazioniCONVERT(... USING utf8mb4)
semePuro/materialeInerte/altriSemi/altriSemiStringidem snake_casedirect
categoriacategoria_analisidirect ENUM
osservazioniPurezzaosservazioni_purezzadirect
germinabilitagerminabilitadirect (valore pre-calcolato)
cancellacancella_notedirect (NULL=attiva)

semiorto_conte → test_germinabilita_conte (1:1)

id(auto)nuovo AUTO_INCREMENT
analisianalisi_idmapAnalisi[legacy_analisi_id]
letteraletteraA/B/C/D
giorni_1..morti_1, vigore_1idemdirect
giorni_2..morti_2, vigore_2idemdirect

semiorto_lotto → lots

id_lottolegacy_idcopy + legacy_source='diego'
id (codice testo)numdirect
varietaseed_idmapVarieta[legacy_id]
id_produttoreproduttore_idmapProduttore[legacy_id]
esauritoesauritodirect (0/1)
provenienzaprovenienzadirect
kgkg / qty (unified)direct
dataArrivo/dataProssimaAnalisi/dataEsauritoidem snake_casedirect

semiorto_specie → categories

Match fuzzy su italianocategories.name. Se match: UPDATE con campi aggiuntivi. Se no match: INSERT nuova.

semiorto_varieta → seeds

idlegacy_idcopy
nomenamedirect
codicecr (cod ricerca)direct
speciecategoryIdmapSpecie[legacy_id]
typeHARDCODED 'op' (assumiamo varietà legacy sono OP)
legacy_sourceHARDCODED 'diego'

semiorto_produttore → produttori

Match fuzzy Levenshtein su nome (soglia distanza ≤ 2). Se match: aggiungi solo codice, legacy_id. Se no: INSERT nuovo.

semiorto_tecnico → users

idlegacy_idcopy, legacy_source='diego_tecnico'
nomenomedirect (split nome+cognome su spazio)
usernameauto = lowercase(nome senza spazi)
roleHARDCODED 'GENETISTA'
attivo1

utente → users (merge)

Per ciascun utente Semiorto, trova il corrispondente TopSeed (se esiste) o crea nuovo. Mapping ruoli:

Amministrazione + MagazzinoADMIN
AmministrazioneADMIN
LaboratorioGENETISTA
MagazzinoMAGAZZINIERE
multi-ruoloADMIN (conservativo)

Password SHA-1 salvato come legacy_sha_hash. Al primo login, rehash a bcrypt.

semiorto_map_specie_analisi_ripetizione_tipologia_seme → specie_calendario_analisi

id_speciecategoria_idmapSpecie[legacy_id]
id_analisi_ripetizionemesedirect (1-12)
id_tipologia_semetipologia_seme_idmapTipologia[legacy_id]

Script migrate_semiorto.php (pseudo-codice)

<?php
// migrate_semiorto_to_topseed.php
// Esegue una tantum quando si è pronti per il taglio finale.

// Connessioni
$aws = new PDO('mysql:host=127.0.0.1;port=3307;dbname=semiorto;charset=utf8',
               'root', 'iron666M', [
                 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                 PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
               ]);
$ts = new PDO('mysql:host=localhost;dbname=topseed_db;charset=utf8mb4',
              'topseed_db', '@Korea2022',
              [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

$ts->beginTransaction();

try {
    echo "1. Importing specie...\n";
    $mapSpecie = importSpecie($aws, $ts);

    echo "2. Importing varieta...\n";
    $mapVarieta = importVarieta($aws, $ts, $mapSpecie);

    echo "3. Importing produttori...\n";
    $mapProduttore = importProduttori($aws, $ts);

    echo "4. Importing tecnici...\n";
    $mapTecnico = importTecnici($aws, $ts);

    echo "5. Importing utenti...\n";
    importUtenti($aws, $ts);

    echo "6. Importing tipologie seme...\n";
    $mapTipologia = importTipologieSeme($aws, $ts);

    echo "7. Importing lotti...\n";
    $mapLotto = importLotti($aws, $ts, $mapVarieta, $mapProduttore);

    echo "8. Importing analisi...\n";
    $mapAnalisi = importAnalisi($aws, $ts, $mapLotto, $mapTecnico, $mapTipologia);

    echo "9. Importing conte...\n";
    importConte($aws, $ts, $mapAnalisi);

    echo "10. Importing calendario specie...\n";
    importCalendario($aws, $ts, $mapSpecie, $mapTipologia);

    $ts->commit();
    echo "OK. Migration completed.\n";

} catch (Throwable $e) {
    $ts->rollBack();
    echo "FAILED: " . $e->getMessage() . "\n";
    exit(1);
}

// ==========================================
function importSpecie(PDO $aws, PDO $ts): array {
    $map = [];
    foreach ($aws->query("SELECT * FROM semiorto_specie") as $s) {
        $name = trim($s['italiano']);
        $stmt = $ts->prepare("SELECT id FROM categories WHERE LOWER(name) = LOWER(?)");
        $stmt->execute([$name]);
        $existing = $stmt->fetchColumn();

        if ($existing) {
            $ts->prepare("UPDATE categories SET italiano=?, inglese=?, latino=?,
                codice_specie=?, giorni_1_conta=?, giorni_tot_conta=?, ur_target=?,
                note_specie=?, legacy_source='diego', legacy_id=? WHERE id=?
            ")->execute([$s['italiano'], $s['inglese'], $s['latino'],
                $s['codice'], $s['giorni1Conta'], $s['giorniTConta'], $s['ur'],
                $s['note'], $s['id'], $existing]);
            $map[$s['id']] = $existing;
        } else {
            $ts->prepare("INSERT INTO categories
                (name, italiano, inglese, latino, codice_specie,
                 giorni_1_conta, giorni_tot_conta, ur_target, note_specie,
                 legacy_source, legacy_id)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'diego', ?)
            ")->execute([$name, $s['italiano'], $s['inglese'], $s['latino'],
                $s['codice'], $s['giorni1Conta'], $s['giorniTConta'], $s['ur'],
                $s['note'], $s['id']]);
            $map[$s['id']] = (int)$ts->lastInsertId();
        }
    }
    return $map;
}

function importAnalisi(PDO $aws, PDO $ts, array $mapLotto,
                       array $mapTecnico, array $mapTipologia): array {
    $map = [];
    $stmt = $ts->prepare("INSERT INTO test_germinabilita (
        legacy_id, source, lot_id, user_id, tipologia_seme_id,
        ft, prova, replica_analisi, tipologia_seme_legacy, calibratura,
        data_chiusura, data_arrivo_laboratorio, data_inizio,
        scostamento, ur, totale_giorni_prova,
        rif_numero, rif_data, rif_germinabilita, rif_prova_laboratorio,
        germinabilita_prova_laboratorio,
        risultato, vigore, pillole, osservazioni,
        seme_puro, materiale_inerte, altri_semi, altri_semi_string,
        categoria_analisi, osservazioni_purezza, germinabilita, cancella_note
    ) VALUES (
        ?, 'diego', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
        ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
    )");
    foreach ($aws->query("SELECT * FROM semiorto_analisi") as $a) {
        $stmt->execute([
            $a['id'],
            $mapLotto[$a['id_lotto']] ?? null,
            $mapTecnico[$a['id_tecnico']] ?? null,
            $mapTipologia[$a['id_tipologia_seme']] ?? null,
            $a['ft'], $a['prova'], $a['tipologiaAnalisi'], $a['tipologiaSeme'],
            $a['calibratura'], $a['data'], $a['dataArrivoLaboratorio'], $a['dataInizio'],
            $a['scostamento'], $a['ur'], $a['totaleGiorniDiProva'],
            $a['rifNumero'], $a['rifData'], $a['rifGerminabilita'], $a['rifProvaLaboratorio'],
            $a['germinabilitaProvaLaboratorio'],
            $a['risultato'], $a['vigore'], $a['pillole'], $a['osservazioni'],
            $a['semePuro'], $a['materialeInerte'], $a['altriSemi'], $a['altriSemiString'],
            $a['categoria'], $a['osservazioniPurezza'], $a['germinabilita'], $a['cancella']
        ]);
        $map[$a['id']] = (int)$ts->lastInsertId();
    }
    return $map;
}

// ...idem per altre entità

Query di validazione post-migrazione

-- 1. Counts identici
SELECT 'legacy' src, COUNT(*) FROM semiorto.semiorto_analisi
UNION ALL
SELECT 'topseed', COUNT(*) FROM topseed_db.test_germinabilita WHERE source='diego';
-- Expected: entrambi 61998

SELECT 'legacy', COUNT(*) FROM semiorto.semiorto_lotto
UNION ALL
SELECT 'topseed', COUNT(*) FROM topseed_db.lots WHERE legacy_source='diego';
-- Expected: 11138

SELECT 'legacy', COUNT(*) FROM semiorto.semiorto_conte
UNION ALL
SELECT 'topseed', COUNT(*) FROM topseed_db.test_germinabilita_conte c
  JOIN topseed_db.test_germinabilita a ON a.id=c.analisi_id WHERE a.source='diego';
-- Expected: 109204

-- 2. Media germinabilità (deve coincidere)
SELECT
  (SELECT AVG(germinabilita) FROM semiorto.semiorto_analisi WHERE germinabilita IS NOT NULL),
  (SELECT AVG(germinabilita) FROM topseed_db.test_germinabilita WHERE source='diego' AND germinabilita IS NOT NULL);
-- Expected: entrambi ≈ 77.8

-- 3. Spot check su 50 analisi random
SELECT
  l.id legacy_id, l.ft legacy_ft, l.germinabilita legacy_germ,
  t.legacy_id topseed_legacy_id, t.ft topseed_ft, t.germinabilita topseed_germ
FROM (SELECT * FROM semiorto.semiorto_analisi ORDER BY RAND() LIMIT 50) l
LEFT JOIN topseed_db.test_germinabilita t ON t.legacy_id = l.id AND t.source='diego'
WHERE l.ft != t.ft OR l.germinabilita != t.germinabilita OR t.legacy_id IS NULL;
-- Expected: 0 righe (tutto match)

-- 4. FK integrity
SELECT COUNT(*) FROM topseed_db.test_germinabilita WHERE source='diego' AND lot_id IS NULL;
-- Expected: 0 (ogni analisi ha un lotto valido)

SELECT COUNT(*) FROM topseed_db.test_germinabilita_conte c
WHERE NOT EXISTS (SELECT 1 FROM topseed_db.test_germinabilita a WHERE a.id = c.analisi_id);
-- Expected: 0 (nessuna conta orfana)

-- 5. Data range
SELECT MIN(data_chiusura), MAX(data_chiusura) FROM topseed_db.test_germinabilita WHERE source='diego';
-- Expected: 2013-09-10 → 2026-04-20

Rollback

Se la migrazione fallisce validazione:

BEGIN;
DELETE FROM topseed_db.test_germinabilita_conte
  WHERE analisi_id IN (SELECT id FROM topseed_db.test_germinabilita WHERE source='diego');
DELETE FROM topseed_db.test_germinabilita WHERE source='diego';
DELETE FROM topseed_db.lots WHERE legacy_source='diego';
DELETE FROM topseed_db.seeds WHERE legacy_source='diego';
DELETE FROM topseed_db.produttori WHERE legacy_source='diego';
DELETE FROM topseed_db.users WHERE legacy_source LIKE 'diego%';
DELETE FROM topseed_db.specie_calendario_analisi;
-- categories: UPDATE categories SET italiano=NULL, ... WHERE legacy_source='diego';
COMMIT;

La transazione unica nello script fa già rollback automatico in caso di errore durante l'esecuzione. Questo è un secondo fallback per dopo-commit se serve.

Stima durata

Lo script è bulk insert, non record-by-record, quindi scala lineare.