Guida introduttiva
Questa guida illustra l'installazione, la connessione a un database, l'acquisizione di snapshot, il calcolo dei diff e l'esecuzione di un merge a tre vie.
Installazione
composer require merql/merql
merql richiede PHP 8.2+ con ext-mbstring e ext-zlib (entrambi richiesti dalla dipendenza pitmaster/pitmaster). Serve anche l'estensione per l'adapter in uso: ext-pdo con ext-pdo_sqlite o ext-pdo_mysql per l'adapter PDO, oppure ext-mysqli per l'adapter mysqli. Composer si limita a suggerirli, quindi verificate che sia installata l'estensione necessaria.
Il pacchetto installa CLI come vendor/bin/merql. Usate merql 0.4.1 o versioni successive per CLI; nelle versioni precedenti il binario non riusciva a trovare l'autoloader quando merql era installato come dipendenza.
Connessione a un database
merql funziona tramite una piccola interfaccia adapter per le connessioni. Sono inclusi adapter per PDO e mysqli e driver di dialetto per MySQL e SQLite.
use Merql\Connection;
// SQLite (in-memory or file-based, through PDO).
$connection = Connection::sqlite(':memory:');
$connection = Connection::sqlite('/path/to/database.sqlite');
// MySQL through PDO.
$connection = Connection::mysql('127.0.0.1', 'my_database', 'root', 'password');
// MySQL through mysqli.
$connection = Connection::mysqli('127.0.0.1', 'my_database', 'root', 'password');
// Any MySQL or SQLite PDO DSN.
$connection = Connection::fromDsn('mysql:host=127.0.0.1;dbname=my_database;charset=utf8mb4', 'root', 'password');
fromDsn() apre qualsiasi DSN supportato da PDO, ma merql include driver di dialetto solo per MySQL e SQLite. Con un altro motore, come pgsql:, la connessione si apre, ma Merql::init() genera Unsupported database driver: pgsql finché non registrate un driver per quel motore. Consultate Driver per database.
Acquisizione di uno snapshot
Uno snapshot registra ogni riga di ogni tabella, o di un sottoinsieme di tabelle, sotto forma di impronte digitali e dati. Le impronte consentono di rilevare rapidamente le modifiche; i dati completi delle righe sono disponibili quando serve un diff.
use Merql\Merql;
Merql::init($connection);
// Snapshot all tables.
$snapshot = Merql::snapshot('baseline');
// Snapshot specific tables.
$snapshot = Merql::snapshot('baseline', ['posts', 'comments', 'users']);
Gli snapshot vengono salvati per impostazione predefinita come file JSON in .merql/snapshots/. Potete cambiare questa posizione:
use Merql\Snapshot\SnapshotStore;
SnapshotStore::setDirectory('/var/data/merql-snapshots');
Calcolo di un diff
Un diff confronta due snapshot e restituisce un insieme di modifiche composto da inserimenti, aggiornamenti ed eliminazioni. Entrambi gli snapshot devono già esistere; qui current è un secondo snapshot acquisito dopo le modifiche al database.
$changeset = Merql::diff('baseline', 'current');
echo "Inserts: " . count($changeset->inserts()) . "\n";
echo "Updates: " . count($changeset->updates()) . "\n";
echo "Deletes: " . count($changeset->deletes()) . "\n";
// Inspect changes for a single table.
$postChanges = $changeset->forTable('posts');
// Check per-column diffs on updates.
foreach ($changeset->updates() as $update) {
echo "Row {$update->rowKey} in {$update->table}:\n";
foreach ($update->columnDiffs as $diff) {
echo " {$diff->column}: {$diff->oldValue} -> {$diff->newValue}\n";
}
}
Merge a tre vie
È l'operazione principale. Prende tre snapshot con nome (base, ours, theirs) e unisce i due insiemi di modifiche.
Base è lo stato iniziale comune delle due copie. Ours e theirs sono le due copie dopo le rispettive modifiche indipendenti; ciascuno snapshot deve quindi essere acquisito dal database che contiene la copia corrispondente. Per esempio, se un ambiente sandbox è stato copiato da un database live:
use Merql\Connection;
use Merql\Merql;
$live = Connection::sqlite('/data/live.sqlite');
$sandbox = Connection::sqlite('/data/sandbox.sqlite');
Merql::init($live);
Merql::snapshot('base'); // before the sandbox is copied from live
// ... copy live.sqlite to sandbox.sqlite, then both databases change ...
Merql::init($sandbox);
Merql::snapshot('ours'); // the sandbox with our edits
Merql::init($live);
Merql::snapshot('theirs'); // live with its own changes
$result = Merql::merge('base', 'ours', 'theirs');
echo "Operations: {$result->operationCount()}\n";
echo "Conflicts: {$result->conflictCount()}\n";
echo "Clean: " . ($result->isClean() ? 'yes' : 'no') . "\n";
Non acquisite tutti e tre gli snapshot dallo stesso database, uno dopo l'altro. Theirs conterrebbe già le modifiche di ours e ogni riga inserita da ours risulterebbe un conflitto insert_insert.
Merql::merge() esegue il merge a livello di colonna. Per unire anche i valori TEXT e JSON al loro interno, usate ThreeWayMerge con un CellMergeConfig; consultate Merge a livello di cella.
Gestione dei conflitti
Se entrambi i lati modificano la stessa colonna assegnandole valori diversi, merql genera un conflitto. Potete risolvere i conflitti automaticamente oppure esaminarli manualmente.
use Merql\Merge\ConflictPolicy;
use Merql\Merge\ConflictResolver;
if (!$result->isClean()) {
// Auto-resolve: one side wins.
$resolved = ConflictResolver::resolve($result, ConflictPolicy::TheirsWins);
// Or inspect each conflict.
foreach ($result->conflicts() as $conflict) {
echo "Conflict on {$conflict->table()} row {$conflict->rowKey()}:\n";
echo " Type: {$conflict->type()}\n";
echo " Column: {$conflict->column()}\n";
echo " Ours: " . var_export($conflict->oursValue(), true) . "\n";
echo " Theirs: " . var_export($conflict->theirsValue(), true) . "\n";
}
}
Applicazione del merge
Una volta ottenuto un risultato di merge pulito, senza conflitti irrisolti, applicatelo al database passato a Merql::init().
Le operazioni del risultato del merge trasformano lo stato base nello stato unito e includono le modifiche di entrambi i lati. Merql::apply() le esegue tutte, quindi si applicano correttamente a un database che contiene ancora lo stato base. Se applicate a un database che contiene già le modifiche di uno dei due lati, un inserimento di quel lato trova una riga già esistente, fallisce e annulla l'intero merge. Per trasferire le modifiche di un lato in un database che contiene già quelle dell'altro, ad esempio le modifiche della sandbox nel database live, applicate solo le operazioni mancanti con Applicazione condizionale.
$applied = Merql::apply($result);
echo "Rows affected: {$applied->rowsAffected()}\n";
if ($applied->hasErrors()) {
foreach ($applied->errors() as $error) {
echo "Error: {$error}\n";
}
}
L'applicatore racchiude tutte le operazioni in una transazione. Se un'istruzione non riesce, l'intero merge viene annullato e l'errore viene restituito in errors(). In tal caso, rowsAffected() conta comunque le istruzioni eseguite prima dell'errore, anche se sono state annullate (#4); controllate prima hasErrors().
Esecuzione di prova
Visualizzate in anteprima le SQL che un merge genererebbe, senza eseguirlo.
use Merql\Apply\DryRun;
$statements = DryRun::generate($result);
foreach ($statements as $sql) {
echo "{$sql};\n";
}
Patch a due vie
Se disponete solo di una base e di un insieme di modifiche, senza modifiche concorrenti di «ours», usate patch(). Equivale a un merge in cui ours è invariato rispetto alla base.
$result = Merql::patch('base', 'changes');
// Always clean: no concurrent edits means no conflicts.
Merql::apply($result);
Uso di CLI
Tutte queste operazioni sono disponibili anche dalla riga di comando. Consultate il CLI di riferimento per tutti i dettagli.
export MERQL_DB_DSN="sqlite:mydb.sqlite"
vendor/bin/merql snapshot base
# ... change the database ...
vendor/bin/merql snapshot current
vendor/bin/merql diff base current
Per un merge a tre vie, acquisite ogni snapshot dal database che contiene lo stato corrispondente, poi eseguite il merge:
MERQL_DB_DSN="sqlite:/data/live.sqlite" vendor/bin/merql snapshot base
MERQL_DB_DSN="sqlite:/data/sandbox.sqlite" vendor/bin/merql snapshot ours
MERQL_DB_DSN="sqlite:/data/live.sqlite" vendor/bin/merql snapshot theirs
vendor/bin/merql merge base ours theirs --dry-run
Filtri per tabelle e colonne
Gli snapshot possono escludere tabelle, come quelle di cache o sessione, e ignorare colonne volatili, come i timestamp updated_at.
use Merql\Filter\TableFilter;
use Merql\Filter\ColumnFilter;
use Merql\Snapshot\Snapshotter;
$snapshotter = new Snapshotter($connection);
$snapshot = $snapshotter->capture('baseline', filters: [
TableFilter::exclude(['sessions', 'cache_*']),
ColumnFilter::ignore(['updated_at', 'modified_date']),
]);
Passaggi successivi
- Consultate il CLI di riferimento per l'intera interfaccia a riga di comando.
- Consultate il PHP API di riferimento per la documentazione dettagliata di ogni classe e metodo.