Zum Inhalt springen

Merge auf Spaltenebene

Das Merge auf Spaltenebene ist der wichtigste Vorteil von merql gegenüber dem naiven Vergleich auf Zeilenebene. Ändern beide Seiten dieselbe Zeile, vergleicht merql jede Spalte einzeln, statt die ganze Zeile als Konflikt zu markieren.

Das Problem mit Merges auf Zeilenebene

Betrachte eine Tabelle posts, in der zwei Personen dieselbe Zeile bearbeiten:

Code
Base:    post #42  { title: "Hello",     content: "Body",    status: "draft"   }
Ours:    post #42  { title: "Hello",     content: "Body v2", status: "draft"   }
Theirs:  post #42  { title: "New Title", content: "Body",    status: "publish" }

Ein Merge auf Zeilenebene erkennt, dass beide Seiten die Zeile geändert haben, und meldet einen Konflikt. Die Änderungen überschneiden sich jedoch nicht. Ours hat content bearbeitet, Theirs dagegen title und status.

So löst das Merge auf Spaltenebene das Problem

merql wendet dieselben Drei-Wege-Merge-Regeln einzeln auf jede Spalte an:

Code
title:   base="Hello", ours="Hello",  theirs="New Title"  -> accept theirs
content: base="Body",  ours="Body v2", theirs="Body"      -> accept ours
status:  base="draft", ours="draft",  theirs="publish"    -> accept theirs

Ergebnis: { title: "New Title", content: "Body v2", status: "publish" }. Konfliktfrei zusammengeführt.

So funktioniert es

Wenn ThreeWayMerge erkennt, dass beide Seiten dieselbe Zeile aktualisiert haben, übergibt es sie an ColumnMerge::merge():

PHP
use Merql\Merge\ColumnMerge;

$result = ColumnMerge::merge(
    table: 'posts',
    rowKey: '42',
    base: ['title' => 'Hello', 'content' => 'Body', 'status' => 'draft'],
    ours: ['title' => 'Hello', 'content' => 'Body v2', 'status' => 'draft'],
    theirs: ['title' => 'New Title', 'content' => 'Body', 'status' => 'publish'],
);

// $result['values']    -> merged column values
// $result['conflicts'] -> list of Conflict objects (empty if clean)

Für jede Spalte gelten dieselben Regeln:

Basis Unsere Seite Gegenseite Ergebnis
A A A Basis beibehalten (keine Änderung)
A A B Gegenseite übernehmen
A B A Unsere Seite übernehmen
A B B Übernehmen (beide stimmen überein)
A B C Konflikt (unterschiedliche Werte)

Wann trotzdem Konflikte auftreten

Ein Konflikt auf Spaltenebene tritt auf, wenn beide Seiten dieselbe Spalte auf unterschiedliche Werte geändert haben:

Code
Base:    post #42  { title: "Hello" }
Ours:    post #42  { title: "Welcome" }
Theirs:  post #42  { title: "Greetings" }

Beide haben title auf unterschiedliche Werte geändert. Das ist ein echter Konflikt, der gelöst werden muss.

PHP
$result = ColumnMerge::merge(
    'posts', '42',
    ['title' => 'Hello'],
    ['title' => 'Welcome'],
    ['title' => 'Greetings'],
);

// $result['conflicts'] contains one Conflict:
//   table: "posts", column: "title"
//   oursValue: "Welcome", theirsValue: "Greetings", baseValue: "Hello"

Teilkonflikte

Eine einzelne Zeile kann sowohl konfliktfrei zusammengeführt werden als auch Konflikte enthalten. Wird eine Spalte konfliktfrei zusammengeführt, während eine andere einen Konflikt aufweist, enthält MergeOperation für die Zeile die zusammengeführten Werte der konfliktfreien Spalten und standardmäßig den Wert unserer Seite für die Spalten mit Konflikten. Der Konflikt wird zusätzlich separat gemeldet.

Code
Base:    { title: "Hello", content: "Body",    status: "draft" }
Ours:    { title: "Welcome", content: "Body v2", status: "draft" }
Theirs:  { title: "Greetings", content: "Body",    status: "publish" }

Hier liegt bei title ein Konflikt vor (beide Seiten haben die Spalte unterschiedlich geändert), content wird konfliktfrei zusammengeführt (nur unsere Seite hat die Spalte geändert) und status wird konfliktfrei zusammengeführt (nur die Gegenseite hat die Spalte geändert). Die Operation enthält:

PHP
// values: { title: "Welcome", content: "Body v2", status: "publish" }
// One conflict on the "title" column.

Wertvergleich

Spaltenwerte werden nach dem Umwandeln als Zeichenfolgen verglichen. NULL ist ein eigener Wert:

  • NULL zu NULL = keine Änderung.
  • NULL zu "hello" = Aktualisierung.
  • "hello" zu NULL = Aktualisierung.
  • "1" zu 1 = keine Änderung (beide werden zur Zeichenfolge "1").

Das entspricht der Abrufsemantik des MerQL-Verbindungsadapters: Alle Datenbankwerte werden als Zeichenfolgen (oder NULL) zurückgegeben.

Zusammenführung auf Zellenebene

Wenn die Zusammenführung auf Spaltenebene weiterhin einen Konflikt ergibt (beide Seiten haben dieselbe Spalte auf unterschiedliche Werte geändert), kann merql mit der Zusammenführung auf Zellenebene noch eine Ebene tiefer gehen. Das gilt für TEXT-Spalten (zeilenweiser Diff) und JSON-Spalten (Schlüssel-für-Schlüssel-Diff) und löst Fälle, in denen beide Seiten unterschiedliche Teile desselben Spaltenwerts bearbeitet haben.