
# Zusammenführungspläne

Ein `MergeResult` ist die ausführbare Ausgabe einer Zusammenführung. Ein `MergePlan` ist deren prüfbare Form. Verwende ihn, wenn eine Anwendung Datenbankänderungen vor dem Schreiben in die Zieldatenbank anzeigen, bereitstellen, serialisieren oder gezielt anwenden muss.

```php
use Merql\Merql;

$plan = Merql::plan(
    id: 'plan-2026-06-06',
    base: 'base',
    ours: 'ours',
    theirs: 'theirs',
    metadata: ['sandbox_id' => 'ow_123'],
);
```

`Merql::plan()` lädt die drei benannten Snapshots und führt sie auf Spaltenebene zusammen, wie `Merql::merge()`. Für einen Plan, der die Zusammenführung auf Zellenebene für TEXT- und JSON-Spalten nutzt, erstelle das Ergebnis selbst und übergib es an `MergePlanBuilder`:

```php
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Plan\MergePlanBuilder;
use Merql\Snapshot\SnapshotStore;

$base = SnapshotStore::load('base');
$ours = SnapshotStore::load('ours');
$theirs = SnapshotStore::load('theirs');

$result = (new ThreeWayMerge(CellMergeConfig::auto()))->merge($base, $ours, $theirs);
$plan = (new MergePlanBuilder())->build('plan-2026-06-06', $base, $ours, $theirs, $result);
```

Der Plan enthält:

- die zum Erstellen verwendeten Snapshot-Namen,
- eine Zusammenfassung der Operationen, Konflikte, Schemaabweichungen, Tabellen und Gruppen,
- stabile Operations-IDs,
- stabile Änderungsgruppen-IDs,
- Zeilenidentitätsspalten und Identitätswerte,
- Zeilenwerte für Basis, unsere Seite, Gegenseite und Ergebnis für jede Operation,
- optionale Metadaten der Host-Anwendung,
- einen Plan-Hash,
- eine Schema-Version.

## Stabile IDs

Operations-IDs sind bei identischem Operationsinhalt deterministisch. Sie werden aus dem Tabellennamen, Zeilenschlüssel, Operationstyp, der Operationsquelle, den geänderten Spalten und den Ergebniswerten abgeleitet.

Änderungsgruppen-IDs sind bei identischem Gruppeninhalt deterministisch. Der Standard-Builder erstellt eine Operationsgruppe pro Operation. Host-Anwendungen können aus der Operationsliste größere, domänenspezifische Gruppen erstellen und sie dennoch mit derselben DTO-Form serialisieren.

```php
foreach ($plan->changeGroups as $group) {
    echo $group->id . ': ' . implode(', ', $group->operationIds);
}
```

## Operationsstruktur

Jede `MergePlanOperation` enthält genügend Kontext für Prüfung und Wiederherstellung.

```php
$operation = $plan->operations[0];

$operation->id;              // stable operation ID
$operation->type;            // insert, update, delete
$operation->table;           // canonical table name
$operation->rowKey;          // encoded identity key
$operation->identityColumns; // columns used for identity
$operation->identityValues;  // decoded identity values
$operation->source;          // ours, theirs, or merged
$operation->baseRow;         // row in base snapshot, or null
$operation->oursRow;         // row in ours snapshot, or null
$operation->theirsRow;       // row in theirs snapshot, or null
$operation->resultValues;    // values the merge would write
$operation->changedColumns;  // changed result columns
```

## Selections

Verwende `ChangeGroupSelection`, wenn der UI Gruppen bereitstellt. Wandle ihn in ein `OperationSelection` um, wenn du Änderungen anwendest oder Artefakte für die Wiederherstellung erzeugst.

```php
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

$selection = ChangeGroupSelection::fromIds([
    $plan->changeGroups[0]->id,
]);

$operationSelection = $selection->toOperationSelection($plan);

$selectedResult = (new SelectedMergeResultFactory())->fromOperationSelection(
    $plan,
    $operationSelection,
    SnapshotStore::load($plan->baseSnapshot),
);
```

Das ausgewählte Ergebnis ist ein konfliktfreies `MergeResult`, das nur ausgewählte Operationen enthält. Es kann an `Merql::apply()` oder `Merql::applyGuarded()` übergeben werden.

## Auswählen, was einem Ziel noch fehlt

Ein Plan enthält die Operationen beider Seiten, da das Zusammenführungsergebnis die Basis in den zusammengeführten Zustand überführt. Wenn das Ziel bereits eine Seite enthält, wähle nur die Operationen aus, die dort noch fehlen. Für ein Ziel mit dem Stand der Gegenseite, etwa eine Live-Datenbank, die Änderungen aus einer Sandbox-Kopie erhält, sind das alle Operationen mit der Quelle `ours` sowie `merged`-Aktualisierungen, die die Spaltenänderungen beider Seiten kombinieren:

```php
use Merql\Plan\OperationSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

// Rows with an unresolved conflict stay out of the selection.
$conflicted = [];
foreach ($plan->conflicts as $conflict) {
    $conflicted[$conflict->table . "\0" . $conflict->rowKey] = true;
}

$ids = [];
foreach ($plan->operations as $operation) {
    $missing = $operation->source === 'ours'
        || ($operation->source === 'merged' && $operation->type === 'update');

    if ($missing && !isset($conflicted[$operation->table . "\0" . $operation->rowKey])) {
        $ids[] = $operation->id;
    }
}

$selected = (new SelectedMergeResultFactory())->fromOperationSelection(
    $plan,
    OperationSelection::fromIds($ids),
    SnapshotStore::load($plan->baseSnapshot),
);
```

Operationen mit der Quelle `theirs` sowie `merged`-Löschungen (Zeilen, die beide Seiten gelöscht haben) sind in diesem Ziel bereits enthalten.

Ein Plan aus einer Zusammenführung mit Konflikten führt weiterhin für jede konfliktbehaftete Zeile, die beide Seiten aktualisiert haben, eine Operation auf. Die Ergebniswerte enthalten für die Konfliktspalte den Wert unserer Seite, und `SelectedMergeResultFactory` gibt für jede Auswahl ein konfliktfreies Ergebnis zurück. Die `conflictState` der Operation und Änderungsgruppe lauten derzeit auch dann `clean` ([#9](https://github.com/inline0/merql/issues/9)). Prüfe daher wie oben selbst `$plan->conflicts` oder löse die Konflikte mit `ConflictResolver` und erstelle den Plan mit `MergePlanBuilder` aus dem gelösten Ergebnis. Unter [Guarded Apply](https://merql.inline0.dev/de/docs/apply/guarded) erfährst du, wie du die Auswahl mit Vorbedingungen für aktuelle Zeilen schreibst.

## Serialisierung

Zusammenführungspläne und Auswahlen lassen sich mit stabiler JSON-Serialisierung speichern. Speichere den serialisierten Plan, wenn ein späterer Anwendungsschritt genau den geprüften Operationssatz verwenden muss.

```php
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\MergePlanSerializer;

$json = MergePlanSerializer::toJson($plan);
$restored = MergePlanSerializer::fromJson($json);

$selectionJson = json_encode($selection->toArray(), JSON_THROW_ON_ERROR);
$selection = ChangeGroupSelection::fromArray(
    json_decode($selectionJson, true, flags: JSON_THROW_ON_ERROR),
);
```

## Alias-Snapshots

Zusammenführungspläne verwenden kanonische Tabellennamen. Wenn sich physische Tabellennamen zwischen Umgebungen unterscheiden, erfasse sie vor der Planung unter demselben kanonischen Namen.

```php
use Merql\Snapshot\SnapshotStore;
use Merql\Snapshot\Snapshotter;

$snapshotter = new Snapshotter($connection);

SnapshotStore::save($snapshotter->captureAliased('live', [
    'wp_posts' => 'wp_posts',
]));

SnapshotStore::save($snapshotter->captureAliased('sandbox', [
    'wp_onumia_abc_posts' => 'wp_posts',
]));
```

Das im Snapshot gespeicherte Schema verwendet den kanonischen Tabellennamen und bewahrt dabei die Spalten, Schlüssel, Identitätsspalten, Zeilenschlüssel und Filter der physischen Tabelle.
