
# Plány sloučení

`MergeResult` je spustitelným výstupem sloučení. `MergePlan` je jeho
prohlížitelná podoba. Použijte jej, když aplikace potřebuje zobrazit,
připravit, serializovat nebo selektivně použít změny databáze před zápisem
do cílové databáze.

```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()` načte tři pojmenované snímky a sloučí je na úrovni sloupců,
podobně jako `Merql::merge()`. Chcete-li naplánovat sloučení využívající sloučení na úrovni buněk pro
sloupce TEXT a JSON, sestavte výsledek sami a předejte jej do
`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);
```

Plán obsahuje:

- názvy snímků použité k jeho vytvoření,
- souhrn operací, konfliktů, nesouladů schématu, tabulek a skupin,
- stabilní ID operací,
- stabilní ID skupin změn,
- sloupce identity řádků a hodnoty identity,
- hodnoty řádků před sloučením, naše, jejich a výsledné,
- volitelná metadata hostitelské aplikace,
- hash plánu,
- verzi schématu.

## Stabilní ID

ID operací jsou deterministická pro stejný obsah operace. Odvozují se
z názvu tabulky, klíče řádku, typu operace, zdroje operace, změněných
sloupců a výsledných hodnot.

ID skupin změn jsou deterministická pro stejný obsah skupiny. Výchozí
nástroj pro sestavení vytváří jednu skupinu operací na každou operaci. Hostitelské aplikace mohou
ze seznamu operací sestavit větší skupiny specifické pro danou doménu a přesto je serializovat
se stejným tvarem DTO.

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

## Struktura operace

Každé `MergePlanOperation` uchovává dostatek kontextu pro kontrolu a vrácení změn.

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

## Výběry

Použijte `ChangeGroupSelection`, když UI připravuje skupiny. Při použití změn nebo generování artefaktů pro vrácení změn jej převeďte na `OperationSelection`.

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

Vybraný výsledek je čisté `MergeResult` obsahující pouze vybrané operace. Lze jej předat do `Merql::apply()` nebo `Merql::applyGuarded()`.

## Výběr chybějících změn v cíli

Plán obsahuje operace obou stran, protože výsledek sloučení převede základ do sloučeného stavu. Pokud cíl již obsahuje stav jedné strany, vyberte pouze operace, které v něm ještě nejsou. U cíle se stavem jejich strany, například živé databáze přijímající úpravy ze sandboxové kopie, jde o každou operaci se zdrojem `ours` a také aktualizace `merged`, které kombinují změny sloupců obou stran:

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

Operace se zdrojem `theirs` a odstranění `merged` (řádky odstraněné oběma stranami) jsou v tomto cíli již obsaženy.

Plán vytvořený ze sloučení s konflikty stále uvádí operaci pro každý konfliktní řádek, který aktualizovaly obě strany. Jeho výsledné hodnoty obsahují hodnotu naší strany v konfliktním sloupci a `SelectedMergeResultFactory` vrací čistý výsledek pro jakýkoli výběr. Atributy `conflictState` operace i skupiny změn však i v takovém případě aktuálně mají hodnotu `clean` ([#9](https://github.com/inline0/merql/issues/9)). Proto si sami ověřte `$plan->conflicts` podle výše uvedeného postupu, nebo vyřešte konflikty pomocí `ConflictResolver` a sestavte plán z vyřešeného výsledku pomocí `MergePlanBuilder`. Pokyny k zápisu výběru s předběžnými podmínkami pro živé řádky najdete v části [Guarded Apply](https://merql.inline0.dev/cs/docs/apply/guarded).

## Serializace

Plány sloučení a výběry mají stabilní serializaci JSON. Uložte serializovaný plán, pokud má pozdější krok použití změn využít přesně tu sadu operací, která byla zkontrolována.

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

## Aliasy snímků

Plány sloučení používají kanonické názvy tabulek. Pokud se fyzické názvy tabulek v různých prostředích liší, před plánováním je zaznamenejte pod stejným kanonickým názvem.

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

Schéma uložené ve snímku používá kanonický název tabulky a zároveň zachovává sloupce, klíče, sloupce identity, klíče řádků a filtry fyzické tabulky.
