Drei-Wege-Zusammenführung
Eine Drei-Wege-Zusammenführung vergleicht zwei Änderungssätze mit einem gemeinsamen Vorfahren und führt sie zu einem Ergebnis zusammen. merql wendet diesen Algorithmus auf Datenbankzeilen statt auf Dateizeilen an.
use Merql\Merge\ThreeWayMerge;
$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);
echo $result->operationCount(); // resolved operations
echo $result->conflictCount(); // unresolved conflicts
Die drei Zustände
Für jede Zusammenführung sind genau drei Snapshots erforderlich:
- Basis -- der Datenbankzustand zum Zeitpunkt, an dem sich beide Zweige getrennt haben.
- Unsere Seite -- ein Zweig nach seinen Änderungen, zum Beispiel eine bearbeitete Sandbox-Kopie.
- Gegenseite -- der andere Zweig nach seinen Änderungen, zum Beispiel die weiterlaufende Live-Datenbank.
Unsere Seite und die Gegenseite müssen jeweils aus der Datenbank erfasst werden, die den jeweiligen Zweig enthält. Die Zusammenführungsregeln behandeln beide Seiten gleich; die Namen bestimmen die source jeder Operation, die Benennung der Konflikttypen (update_delete bedeutet, dass unsere Seite aktualisiert und die Gegenseite gelöscht hat) und welche Seite ConflictPolicy::OursWins und TheirsWins bevorzugen.
Base (snapshot at fork time)
/ \
Ours (our changes) Theirs (their changes)
\ /
--------- MERGE ----------
|
Merged result
+ conflicts (if any)
Ohne gemeinsame Basis kannst du nicht zwischen „hinzugefügt“ und „unverändert“ unterscheiden. Ein Zwei-Wege-Vergleich zeigt, dass Seite A einen Wert hat und Seite B nicht, kann aber nicht feststellen, ob A ihn hinzugefügt oder B ihn gelöscht hat. Die Basis beantwortet diese Frage.
So werden Änderungssätze berechnet
Differ vergleicht jeden Snapshot mit der Basis und erstellt zwei Änderungssätze. Jeder Änderungssatz enthält drei Arten von Operationen:
| Basis | Aktuell | Operation |
|---|---|---|
| Zeile vorhanden | Zeile vorhanden, gleicher Fingerabdruck | Keine Änderung |
| Zeile vorhanden | Zeile vorhanden, anderer Fingerabdruck | UPDATE (Differenz pro Spalte) |
| Zeile vorhanden | Zeile fehlt | DELETE |
| Zeile fehlt | Zeile vorhanden | INSERT |
Fingerabdrücke (SHA-256-Hashes normalisierter Zeileninhalte) bieten einen schnellen Weg. Stimmen zwei Fingerabdrücke überein, ist die Zeile unverändert und ein Vergleich auf Spaltenebene erübrigt sich.
use Merql\Diff\Differ;
$differ = new Differ();
$oursChangeset = $differ->diff($base, $ours);
$theirsChangeset = $differ->diff($base, $theirs);
Zusammenführungsregeln
Anschließend werden die beiden Änderungssätze Operation für Operation zusammengeführt. Diese Tabelle zeigt das Ergebnis für jede Kombination:
| Unsere Seite | Gegenseite | Ergebnis |
|---|---|---|
| Keine Änderung | Keine Änderung | Basis beibehalten |
| Keine Änderung | Aktualisiert | Gegenseite übernehmen |
| Aktualisiert | Keine Änderung | Unsere Seite übernehmen |
| Aktualisiert (gleicher Wert) | Aktualisiert (gleicher Wert) | Übernehmen (beide stimmen überein) |
| Aktualisiert (anderer Wert) | Aktualisiert (anderer Wert) | Konflikt |
| Keine Änderung | Gelöscht | Löschung übernehmen |
| Gelöscht | Keine Änderung | Löschung übernehmen |
| Gelöscht | Gelöscht | Löschung übernehmen (beide stimmen überein) |
| Aktualisiert | Gelöscht | Konflikt |
| Gelöscht | Aktualisiert | Konflikt |
| Eingefügt (nur unsere Seite) | -- | Einfügung übernehmen |
| -- | Eingefügt (nur Gegenseite) | Einfügung übernehmen |
| Eingefügt (gleicher Primärschlüssel) | Eingefügt (gleicher Primärschlüssel) | Konflikt |
Wenn beide Seiten dieselbe Zeile aktualisieren, erklärt merql das nicht sofort zum Konflikt. Stattdessen geht es zur Zusammenführung auf Spaltenebene über und vergleicht jede Spalte einzeln. Einzelheiten findest du unter Zusammenführung auf Spaltenebene.
Die Fassade verwenden
Die Merql-Fassade arbeitet mit benannten Snapshots, die auf der Festplatte gespeichert wurden. Rufe Merql::init() mit der Datenbank auf, die den jeweiligen Zustand enthält, bevor du den Snapshot erstellst:
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');
Erstelle nicht alle drei Snapshots nacheinander aus derselben Datenbank. Die Gegenseite enthielte dann auch die Änderungen unserer Seite. Jede von unserer Seite eingefügte Zeile erschiene somit auf beiden Seiten als eingefügt und würde als insert_insert-Konflikt gemeldet.
Merql::merge(), Merql::patch() und Merql::plan() erstellen ein ThreeWayMerge ohne CellMergeConfig und führen daher nur auf Spaltenebene zusammen. Für die Zusammenführung auf Zellenebene lade die Snapshots und verwende die Klasse direkt, wie unten gezeigt.
Die Klassen direkt verwenden
Für mehr Kontrolle kannst du die Komponenten selbst instanziieren. Snapshotter::capture() speichert nichts dauerhaft. Behalte die Snapshots daher im Arbeitsspeicher oder speichere sie mit 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'),
);
Die Patch-Abkürzung
Wenn nur eine Seite Änderungen enthält (keine gleichzeitigen Änderungen), verwende patch(). Das ist eine Zwei-Wege-Zusammenführung, bei der unsere Seite der Basis entspricht. Konflikte sind daher ausgeschlossen:
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.
Über die Fassade:
$result = Merql::patch('base', 'changes');
patch() entspricht merge($base, $base, $changes). Da unsere Seite nie von der Basis abgewichen ist, wird jede Änderung der Gegenseite ohne Konflikt übernommen.
Schemaabweichungen erkennen
Wurden zwischen Snapshots Spalten zu einer Tabelle hinzugefügt oder daraus entfernt, wird die Zusammenführung trotzdem ausgeführt, aber die Abweichungen werden erfasst. Geänderte Spaltentypen werden nicht verglichen. Eine umbenannte Spalte erscheint als eine entfernte und eine hinzugefügte Spalte; siehe Schema-Validierung.
$result = $merge->merge($base, $ours, $theirs);
if ($result->hasSchemaMismatches()) {
foreach ($result->schemaMismatches() as $mismatch) {
echo $mismatch->getMessage() . "\n";
}
}
merql migriert Schemas nicht automatisch. Für Schemaänderungen ist der Aufrufer verantwortlich. Die Zusammenführung meldet sie, damit du über das weitere Vorgehen entscheiden kannst.
Das MergeResult
Das Ergebnisobjekt enthält alles, was du brauchst:
$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
Jede MergeOperation hat einen Typ (insert, update oder delete), einen Tabellennamen, einen Zeilenschlüssel, die Spaltenwerte und eine Quelle (ours, theirs oder merged). merged kennzeichnet eine Zeile, die beide Seiten aktualisiert (spaltenweise kombiniert) oder gelöscht haben.
Wo ein Zusammenführungsergebnis angewendet werden kann
Ein Zusammenführungsergebnis beschreibt, wie die Basis in den zusammengeführten Zustand überführt wird. Seine Operationen enthalten die Änderungen beider Seiten: Eine Einfügung unserer Seite und eine Einfügung der Gegenseite erscheinen beide als Einfügungen.
Merql::apply()(oderApplier) führt jede Operation für die verbundene Datenbank aus. Das funktioniert problemlos für eine Datenbank, die noch den Basiszustand enthält, etwa eine frische Kopie der Basis. Bei einer Datenbank, die bereits die Änderungen einer Seite enthält, schlägt die Einfügung einer bereits vorhandenen Zeile fehl und die Transaktion wird zurückgerollt.- Um unsere Änderungen in eine Datenbank zu übernehmen, die bereits den Stand der Gegenseite enthält, etwa um Sandbox-Änderungen live zu schalten, wende nur die Operationen an, die dieser Datenbank noch fehlen: diejenigen mit der Quelle
ourssowiemerged-Aktualisierungen. Verwende einen Zusammenführungsplan, um sie auszuwählen, und Guarded Apply, um sie mit Vorbedingungen für aktuelle Zeilen zu schreiben.