
# 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:

```
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:

```
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:

```
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.

```
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](https://merql.inline0.dev/de/docs/merge/cell-level) 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.
