
# Samenvoegplannen

Een `MergeResult` is de uitvoerbare uitvoer van een samenvoeging. Een `MergePlan` is de
inspecteerbare vorm van die uitvoer. Gebruik deze wanneer een applicatie databasewijzigingen moet tonen,
voorbereiden, serialiseren of selectief toepassen voordat ze naar de
doeldatabase worden geschreven.

```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()` laadt de drie benoemde snapshots en voegt ze samen op kolomniveau,
net als `Merql::merge()`. Bouw zelf het resultaat en geef het door aan
`MergePlanBuilder` om samenvoegen op celniveau toe te passen op TEXT- en JSON-kolommen:

```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);
```

Het plan bevat:

- de snapshotnamen die zijn gebruikt om het plan te bouwen,
- een overzicht van bewerkingen, conflicten, schemamismatches, tabellen en groepen,
- stabiele bewerkings-ID's,
- stabiele ID's voor wijzigingsgroepen,
- rij-identiteitskolommen en identiteitswaarden,
- basis-, onze, hun en resultaatrijwaarden voor elke bewerking,
- optionele metadata van de hostapplicatie,
- een planhash,
- een schemaversie.

## Stabiele ID's

Bewerkings-ID's zijn deterministisch bij dezelfde bewerkingsinhoud. Ze
worden afgeleid van de tabelnaam, rijsleutel, bewerkingstype, bewerkingsbron, gewijzigde
kolommen en resultaatwaarden.

ID's van wijzigingsgroepen zijn deterministisch bij dezelfde groepsinhoud. De standaard
builder maakt één bewerkingsgroep per bewerking. Hostapplicaties kunnen grotere domeinspecifieke groepen maken op basis van de bewerkingslijst en deze nog steeds serialiseren
met dezelfde DTO-vorm.

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

## Bewerkingsvorm

Elke `MergePlanOperation` bevat voldoende context voor beoordeling en terugdraaien.

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

## Selecties

Gebruik `ChangeGroupSelection` wanneer de UI groepen klaarzet. Zet deze om naar een
`OperationSelection` wanneer je wijzigingen toepast of artefacten voor terugdraaien genereert.

```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),
);
```

Het geselecteerde resultaat is een schoon `MergeResult` dat alleen geselecteerde
bewerkingen bevat. Het kan worden doorgegeven aan `Merql::apply()` of `Merql::applyGuarded()`.

## Selecteren wat een doel mist

Een plan bevat de bewerkingen van beide kanten, omdat het samenvoegresultaat de basis
omzet naar de samengevoegde staat. Wanneer het doel al één kant bevat, selecteer je alleen de
bewerkingen die het nog niet heeft. Voor een doel dat hun kant bevat, zoals een live
database die de wijzigingen van een sandboxkopie ontvangt, geldt dit voor elke bewerking met
bron `ours`, plus `merged`-updates, die de kolomwijzigingen van beide kanten combineren:

```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),
);
```

Bewerkingen met bron `theirs` en `merged`-verwijderingen (rijen die beide kanten
hebben verwijderd) zijn al aanwezig in dat doel.

Een plan dat is gebouwd op basis van een samenvoeging met conflicten, vermeldt nog steeds een bewerking voor elke
conflicterende rij die beide kanten hebben bijgewerkt. De resultaatwaarden bevatten onze waarde in
de conflicterende kolom, en `SelectedMergeResultFactory` retourneert een schoon resultaat
voor elke selectie. De `conflictState` van de bewerking en wijzigingsgroep
blijven ook dan `clean` lezen ([#9](https://github.com/inline0/merql/issues/9)), dus
controleer zelf `$plan->conflicts` zoals hierboven, of los conflicten op met
`ConflictResolver` en bouw het plan op basis van het opgeloste resultaat met
`MergePlanBuilder`. Zie
[Beveiligd toepassen](https://merql.inline0.dev/nl/docs/apply/guarded) voor het schrijven van de selectie met voorwaarden voor live rijen.

## Serialisatie

Samenvoegplannen en selecties worden stabiel geserialiseerd naar JSON. Sla het geserialiseerde
plan op wanneer een latere toepassingsstap exact dezelfde beoordeelde set bewerkingen moet gebruiken.

```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),
);
```

## Aliassnapshots

Samenvoegplannen gebruiken canonieke tabelnamen. Als fysieke tabelnamen tussen
omgevingen verschillen, leg ze dan vóór het plannen vast onder dezelfde canonieke naam.

```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',
]));
```

Het schema dat in de snapshot is opgeslagen, gebruikt de canonieke tabelnaam en behoudt tegelijkertijd
de kolommen, sleutels, identiteitskolommen, rijsleutels en filters van de fysieke tabel.
