Unione a tre vie
Un’unione a tre vie confronta due insiemi di modifiche con un antenato comune e li combina in un unico risultato. merql applica questo algoritmo alle righe del database invece che alle righe dei file.
use Merql\Merge\ThreeWayMerge;
$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);
echo $result->operationCount(); // resolved operations
echo $result->conflictCount(); // unresolved conflicts
I tre stati
Ogni unione richiede esattamente tre snapshot:
- Base -- lo stato del database nel momento in cui i due branch si sono separati.
- Nostro -- un branch dopo le sue modifiche, per esempio una copia sandbox modificata.
- Altrui -- l’altro branch dopo le sue modifiche, per esempio il database live che ha continuato a funzionare.
Nostro e altrui devono essere acquisiti ciascuno dal database che contiene quel branch. Le regole di unione trattano i due lati allo stesso modo; i nomi determinano l’source di ogni operazione, il nome dei tipi di conflitto (update_delete significa che noi abbiamo aggiornato e gli altri hanno eliminato) e quale lato ConflictPolicy::OursWins e TheirsWins favoriscono.
Base (snapshot at fork time)
/ \
Ours (our changes) Theirs (their changes)
\ /
--------- MERGE ----------
|
Merged result
+ conflicts (if any)
Senza una base comune, non è possibile distinguere tra «aggiunto» e «invariato». Un confronto a due vie mostra che il lato A ha un valore e il lato B no, ma non può stabilire se A lo abbia aggiunto o B lo abbia eliminato. La base risponde a questa domanda.
Come vengono calcolati i changeset
Differ confronta ogni snapshot con la base per produrre due changeset. Ogni changeset contiene tre tipi di operazioni:
| Base | Stato corrente | Operazione |
|---|---|---|
| La riga esiste | La riga esiste, stessa impronta | Nessuna modifica |
| La riga esiste | La riga esiste, impronta diversa | UPDATE (diff per colonna) |
| La riga esiste | La riga non esiste | DELETE |
| La riga non esiste | La riga esiste | INSERT |
Le impronte (SHA-256 hash del contenuto normalizzato della riga) offrono un percorso rapido. Se due impronte coincidono, la riga è invariata e non serve un confronto a livello di colonna.
use Merql\Diff\Differ;
$differ = new Differ();
$oursChangeset = $differ->diff($base, $ours);
$theirsChangeset = $differ->diff($base, $theirs);
Regole di unione
I due changeset vengono quindi uniti operazione per operazione. Questa tabella definisce il risultato per ogni combinazione:
| Nostro | Altrui | Risultato |
|---|---|---|
| Nessuna modifica | Nessuna modifica | Mantieni la base |
| Nessuna modifica | Aggiornata | Accetta altrui |
| Aggiornata | Nessuna modifica | Accetta nostro |
| Aggiornata (stesso valore) | Aggiornata (stesso valore) | Accetta (accordo tra entrambi) |
| Aggiornata (valore diverso) | Aggiornata (valore diverso) | Conflitto |
| Nessuna modifica | Eliminata | Accetta l’eliminazione |
| Eliminata | Nessuna modifica | Accetta l’eliminazione |
| Eliminata | Eliminata | Accetta l’eliminazione (accordo tra entrambi) |
| Aggiornata | Eliminata | Conflitto |
| Eliminata | Aggiornata | Conflitto |
| Inserita (solo nostro) | -- | Accetta l’inserimento |
| -- | Inserita (solo altrui) | Accetta l’inserimento |
| Inserita (stessa PK) | Inserita (stessa PK) | Conflitto |
Quando entrambi i lati aggiornano la stessa riga, merql non dichiara subito un conflitto. Passa all’unione a livello di colonna, confrontando ogni colonna singolarmente. Per i dettagli, consultate Unione a livello di colonna.
Uso della facade
La facade Merql opera su snapshot nominati salvati su disco. Chiamate Merql::init() con il database che contiene ciascuno stato prima di acquisire lo snapshot:
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');
Merql::init($live);
Merql::snapshot('theirs');
$result = Merql::merge('base', 'ours', 'theirs');
Non acquisite tutti e tre gli snapshot dallo stesso database, uno dopo l’altro. Lo snapshot altrui conterrebbe anche le nostre modifiche, quindi ogni riga inserita da noi sembrerebbe inserita da entrambi i lati e verrebbe segnalata come conflitto insert_insert.
Merql::merge(), Merql::patch() e Merql::plan() creano un ThreeWayMerge senza un CellMergeConfig, quindi uniscono solo a livello di colonna. Per l’unione a livello di cella, caricate gli snapshot e usate direttamente la classe, come mostrato di seguito.
Uso diretto delle classi
Per un controllo maggiore, istanziate voi i componenti. Snapshotter::capture() non salva nulla, quindi conservate gli snapshot in memoria o salvateli con SnapshotStore::save():
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Snapshot\Snapshotter;
use Merql\Snapshot\SnapshotStore;
$base = (new Snapshotter($live))->capture('base');
// ... copy live to the sandbox, then both databases change ...
$ours = (new Snapshotter($sandbox))->capture('ours');
$theirs = (new Snapshotter($live))->capture('theirs');
$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);
// Snapshots saved earlier, merged with cell-level merge for TEXT and JSON columns.
$merge = new ThreeWayMerge(CellMergeConfig::auto());
$result = $merge->merge(
SnapshotStore::load('base'),
SnapshotStore::load('ours'),
SnapshotStore::load('theirs'),
);
La scorciatoia patch
Quando solo un lato contiene modifiche (senza modifiche simultanee), usate patch(). È un’unione a due vie in cui nostro equivale alla base, quindi non possono verificarsi conflitti:
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.
Tramite la facade:
$result = Merql::patch('base', 'changes');
patch() equivale a merge($base, $base, $changes). Poiché nostro non si è mai discostato dalla base, ogni modifica altrui viene accettata senza conflitti.
Rilevamento delle incompatibilità dello schema
Se tra gli snapshot sono state aggiunte o rimosse colonne da una tabella, l’unione procede comunque ma registra le incompatibilità. I tipi di colonna modificati non vengono confrontati e una colonna rinominata viene rilevata come una rimossa e una aggiunta; consultate Validazione dello schema.
$result = $merge->merge($base, $ours, $theirs);
if ($result->hasSchemaMismatches()) {
foreach ($result->schemaMismatches() as $mismatch) {
echo $mismatch->getMessage() . "\n";
}
}
merql non esegue migrazioni automatiche dello schema. Le modifiche allo schema sono responsabilità di chi chiama la libreria. L’unione le segnala perché possiate decidere come procedere.
MergeResult
L’oggetto risultato contiene tutto ciò che serve:
$result->isClean(); // true if no conflicts
$result->operations(); // list of MergeOperation objects
$result->conflicts(); // list of Conflict objects
$result->operationCount(); // number of resolved operations
$result->conflictCount(); // number of unresolved conflicts
$result->hasSchemaMismatches(); // true if schemas differ
$result->baseSnapshot(); // the base snapshot used for the merge
Ogni MergeOperation ha un tipo (insert, update o delete), un nome di tabella, una chiave della riga, i valori delle colonne e un’origine (ours, theirs o merged). merged indica una riga aggiornata da entrambi i lati (con le modifiche combinate colonna per colonna) o eliminata da entrambi.
Dove si applica un risultato di unione
Un risultato di unione descrive come trasformare la base nello stato unito. Le sue operazioni includono le modifiche di entrambi i lati: un inserimento effettuato da noi e uno effettuato dagli altri compaiono entrambi come inserimenti.
Merql::apply()(oApplier) esegue ogni operazione sul database connesso. Si applica senza problemi a un database che contiene ancora la base, per esempio una copia appena creata dello stato di base. Su un database che contiene già le modifiche di un lato, l’inserimento di una riga già presente fallisce e la transazione viene annullata.- Per riportare le nostre modifiche in un database che contiene già quelle altrui, per esempio promuovendo modifiche sandbox nell’ambiente live, applicate solo le operazioni mancanti: quelle con origine
ourse gli aggiornamentimerged. Usate un piano di unione per selezionarle e Applicazione protetta per scriverle con precondizioni sulle righe live.