Script Python avanzato per migrare dati da MariaDB a PostgreSQL attraverso un tunnel SSH, con support per field mapping, key-based upsert (INSERT/UPDATE) e dry-run mode.
pip install mysql-connector-python psycopg2-binary paramiko sshtunnelpython migrate_db.py --create-config config.jsonQuesto crea un file config.json con i valori di default da compilare.
{
"source": {
"host": "mysql.example.com",
"port": 3306,
"user": "mysql_user",
"password": "mysql_password",
"database": "source_db",
"table": "users",
"fields": ["id", "email", "name", "surname"]
},
"ssh": {
"enabled": true,
"host": "bastion.example.com",
"port": 22,
"username": "ssh_user",
"password": null,
"key_file": "/path/to/private_key.pem",
"key_passphrase": null
},
"destination": {
"host": "postgres.internal.example.com",
"port": 5432,
"user": "postgres_user",
"password": "postgres_password",
"database": "destination_db",
"table": "users",
"key_field": "email",
"field_mapping": {
"id": "user_id",
"email": "email",
"name": "first_name",
"surname": "last_name"
}
},
"dry_run": true,
"batch_size": 100
}python migrate_db.py --config config.json --dry-runQuesto non modifica nulla e genera un report nel log con:
- ✓ Chiavi trovate in destination (verranno aggiornate)
- ✗ Chiavi nuove (verranno inserite)
- Valori di mapping per il primo record
- Statistiche complesse
Esempio output dry-run:
================================================================================
INIZIO MIGRAZIONE - Modalità: DRY-RUN
================================================================================
✓ Letti 1250 record da source
✓ Trovate 823 chiavi esistenti in destination
[DRY-RUN 1/1250] INSERT record chiave='user1@example.com'
[DRY-RUN 2/1250] UPDATE record chiave='existing@example.com'
[DRY-RUN 3/1250] INSERT record chiave='user3@example.com'
...
================================================================================
SUMMARY MIGRAZIONE
================================================================================
Record letti: 1250
Record inseriti: 427
Record aggiornati: 823
Record saltati: 0
Errori: 0
================================================================================
Chiavi INSERT: 427
Chiavi UPDATE: 823
DETTAGLIO CHIAVI (DRY-RUN):
existing@example.com: ✓ ESISTE → UPDATE
user1@example.com: ✗ NUOVO → INSERT
user3@example.com: ✗ NUOVO → INSERT
...
Una volta soddisfatto del dry-run:
python migrate_db.py --config config.jsonOppure rimuovi la flag "dry_run": true dal config e esegui senza --dry-run.
| Campo | Descrizione | Default |
|---|---|---|
host |
Hostname MariaDB | localhost |
port |
Porta MariaDB | 3306 |
user |
Username MySQL | root |
password |
Password MySQL | password |
database |
Nome database source | source_db |
table |
Nome tabella source | source_table |
fields |
Campi da esportare ([] = tutti) | [] |
Esempio: esportare solo alcuni campi
"fields": ["id", "email", "name", "phone"]| Campo | Descrizione |
|---|---|
enabled |
Abilita tunnel SSH (true/false) |
host |
Hostname del server SSH |
port |
Porta SSH |
username |
Username SSH |
password |
Password SSH (null se usi chiave) |
key_file |
Path alla chiave privata SSH |
key_passphrase |
Passphrase della chiave (se encrypted) |
Opzioni di autenticazione SSH:
{
"enabled": true,
"host": "bastion.example.com",
"port": 22,
"username": "ssh_user",
"password": "ssh_password",
"key_file": null,
"key_passphrase": null
}Oppure con chiave privata:
{
"enabled": true,
"host": "bastion.example.com",
"port": 22,
"username": "ssh_user",
"password": null,
"key_file": "/home/user/.ssh/id_rsa",
"key_passphrase": "passphrase_della_chiave"
}Se PostgreSQL è accessibile direttamente (no tunnel):
{
"enabled": false
}| Campo | Descrizione |
|---|---|
host |
Hostname PostgreSQL |
port |
Porta PostgreSQL |
user |
Username PostgreSQL |
password |
Password PostgreSQL |
database |
Database destination |
table |
Tabella destination |
key_field |
Campo chiave per upsert (es. email, id) |
field_mapping |
Mappa source → destination |
Field Mapping:
Mappa i nomi dei campi source ai nomi in destination. Se omesso, assume stessi nomi:
"field_mapping": {
"id": "user_id",
"email": "email_address",
"name": "first_name",
"surname": "last_name"
}Lo script usa il key_field per determinare se fare INSERT o UPDATE:
- Legge il valore del
key_fielddal record source - Verifica se esiste già in destination con quel valore di
key_field - Se esiste → UPDATE (aggiorna tutti i campi)
- Se non esiste → INSERT (inserisce nuovo record) OPPURE SKIP (se
skip_insert: true)
Esempio (skip_insert: false - default):
Source record: {id: 123, email: 'john@example.com', name: 'John', surname: 'Doe'}
key_field: 'email'
key_value: 'john@example.com'
→ Se 'john@example.com' esiste in destination → UPDATE
→ Se 'john@example.com' NON esiste → INSERT (nuovo record)
Esempio (skip_insert: true - solo UPDATE):
Source record: {id: 123, email: 'john@example.com', name: 'John', surname: 'Doe'}
key_field: 'email'
key_value: 'john@example.com'
→ Se 'john@example.com' esiste in destination → UPDATE
→ Se 'john@example.com' NON esiste → SKIP (non inserito)
Se true, salta gli INSERT - inserisce solo record su chiavi esatte che già esistono.
Uso case: Quando vuoi sincronizzare dati senza aggiungere nuovi record in destination.
"skip_insert": trueDefault: false (abilita sia INSERT che UPDATE)
I log sono salvati in migration.log (o file specificato con --log-file).
Ogni run contiene:
- Connection logs: dettagli connessioni source/destination/SSH
- Row-by-row logs: action (INSERT/UPDATE) per ogni record
- Summary: statistiche finali
- Key details: elenco di chiavi con azione prevista
Specifica file di log custom:
python migrate_db.py --config config.json --log-file migration_2024_01_15.logSource: users table in MariaDB
Destination: accounts table in PostgreSQL
Key field: email (univoco per utente)
{
"source": {
"table": "users",
"fields": ["email", "first_name", "last_name", "phone"]
},
"destination": {
"table": "accounts",
"key_field": "email",
"field_mapping": {
"email": "email_address",
"first_name": "fname",
"last_name": "lname"
}
}
}Source: products in MariaDB
Destination: products in PostgreSQL
Key field: product_id (ID univoco prodotto)
{
"source": {
"table": "products",
"fields": ["product_id", "name", "price", "stock"]
},
"destination": {
"table": "products",
"key_field": "product_id",
"field_mapping": {
"product_id": "id",
"name": "title"
}
}
}Source: users in MariaDB
Destination: accounts in PostgreSQL
Key field: email
Solo aggiornamenti, nessun nuovo record
{
"source": {
"table": "users",
"fields": ["email", "last_name", "phone", "updated_at"]
},
"destination": {
"table": "accounts",
"key_field": "email",
"field_mapping": {
"email": "email_address",
"last_name": "surname"
}
},
"skip_insert": true
}Con questa config:
- Solo i record con email già presente in
accountsvengono aggiornati - I record source con email non trovata vengono saltati
- Dry-run sempre prima: Sempre esegui
--dry-runprima della migrazione reale - Backup: Fai backup di destination prima di migrare in live
- Transazioni: Ogni batch (default 100 righe) viene committed separatamente
- Null values: NULL sono preservati nella migrazione
- Type conversion: I tipi vengono convertiti automaticamente (MySQL types → PostgreSQL types)
- SSH key permissions: La chiave SSH deve avere permessi
600(chmod 600 ~/.ssh/id_rsa)
Verifica il path della chiave privata. Deve essere path assoluto:
❌ "key_file": "~/.ssh/id_rsa"
✅ "key_file": "/home/username/.ssh/id_rsa"
Verifica che:
- Host SSH sia raggiungibile
- Porta 22 sia aperta (o la port custom configurata)
- Username e password/key siano corretti
Verifica che:
- Il campo destinazione esista nella tabella
- Il field_mapping sia corretto
- Gli accenti/maiuscole siano coerenti (PostgreSQL è case-sensitive)
Se destination ha FK constraints, potrebbero conflittare con l'upsert. Soluzioni:
- Disabilita FK temporaneamente:
SET CONSTRAINTS ALL DEFERRED; - Ordina i dati per rispettare le FK
- Usa
--dry-runper analizzare gli errori
# Gen config
python migrate_db.py --create-config test_config.json
# Edita test_config.json:
# - source: localhost MySQL con user=root, db=test_source, table=users
# - destination: localhost PostgreSQL con user=postgres, db=test_dest, table=users
# - ssh: disabled
# Dry-run
python migrate_db.py --config test_config.json --dry-run
# Real migration
python migrate_db.py --config test_config.json# Gen config
python migrate_db.py --create-config prod_config.json
# Edita prod_config.json con credenziali reali
# Dry-run (non modifica nulla)
python migrate_db.py --config prod_config.json --dry-run
# Verifica il log
tail -f migration.log
# Se soddisfatto, esegui live (modifica il config: "dry_run": false)
python migrate_db.py --config prod_config.json --log-file migration_prod_$(date +%Y%m%d_%H%M%S).logErrori comuni e soluzioni:
- PSQLError: Verifica credenziali PostgreSQL e SSH tunnel
- MySQLError: Verifica credenziali MariaDB e che la tabella/campi esistano
- paramiko errors: Verifica SSH key permissions e formato
Creato per Manuele @ Gter srl - 2024