
# Планове за сливане

`MergeResult` е изпълнимият резултат от сливане. `MergePlan` е
форма на този резултат, предназначена за проверка. Използвайте я, когато дадено приложение трябва да показва,
подготвя, сериализира или селективно прилага промени в базата данни, преди да ги запише в
целевата база данни.

```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()` зарежда трите посочени моментни снимки и ги слива на ниво колона,
както прави `Merql::merge()`. За да планирате сливане, което използва сливане на ниво клетка за
колони TEXT и JSON, създайте резултата сами и го подайте на
`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);
```

Планът съдържа:

- имената на моментните снимки, използвани за създаването му,
- обобщение на операциите, конфликтите, несъответствията в схемата, таблиците и групите,
- стабилни ID на операциите,
- стабилни ID на групите промени,
- колони за идентичност на редовете и стойности за идентичност,
- стойностите на реда в base, ours, theirs и result за всяка операция,
- незадължителни метаданни на хост приложението,
- хеш на плана,
- версия на схемата.

## Стабилни ID

ID на операциите са детерминистични при еднакво съдържание на операцията. Те се
извеждат от името на таблицата, ключа на реда, типа операция, източника на операцията, променените
колони и стойностите в резултата.

ID на групите промени са детерминистични при еднакво съдържание на групата. Стандартният
създател формира по една група операции за всяка операция. Хост приложенията могат да създават
по-големи специфични за домейна групи от списъка с операции и пак да ги сериализират
със същата форма на DTO.

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

## Структура на операцията

Всяка `MergePlanOperation` съхранява достатъчно контекст за преглед и връщане назад.

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

## Избори

Използвайте `ChangeGroupSelection`, когато UI подготвя групи. Преобразувайте го в
`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),
);
```

Избраният резултат е изчистен `MergeResult`, който съдържа само избраните
операции. Може да бъде подаден на `Merql::apply()` или `Merql::applyGuarded()`.

## Избор на липсващото в целта

Планът съдържа операциите и от двете страни, защото резултатът от сливането превръща base
в обединеното състояние. Когато целта вече съдържа едната страна, изберете само операциите,
които още липсват в нея. За цел, съдържаща theirs, например активна
база данни, която получава редакциите от копие в изолирана среда, това са всички операции с
източник `ours`, както и актуализациите `merged`, които обединяват промените в колоните от двете страни:

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

Операциите с източник `theirs` и изтриванията `merged` (редове, изтрити и от двете страни) вече
са налице в тази цел.

План, създаден от сливане с конфликти, пак съдържа операция за всеки
конфликтен ред, актуализиран и от двете страни. Стойностите в резултата му съдържат стойността на ours
в конфликтната колона, а `SelectedMergeResultFactory` връща изчистен резултат
за всеки избор. И `conflictState` на операцията, и на групата промени
засега е `clean` дори в този случай ([#9](https://github.com/inline0/merql/issues/9)), затова
проверете сами `$plan->conflicts`, както е описано по-горе, или разрешете конфликтите с
`ConflictResolver` и създайте плана от разрешения резултат чрез
`MergePlanBuilder`. Вижте
[Защитено прилагане](https://merql.inline0.dev/bg/docs/apply/guarded) за записване на избора с предварителни условия за активните редове.

## Сериализация

Плановете за сливане и изборите имат стабилна сериализация чрез JSON. Съхранете сериализирания
план, когато последващото прилагане трябва да използва точно прегледания набор от операции.

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

## Псевдоними на моментни снимки

Плановете за сливане използват канонични имена на таблици. Ако физическите имена на таблиците се различават между
средите, заснемете ги под едно и също канонично име преди планирането.

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

Схемата, съхранена в моментната снимка, използва каноничното име на таблицата, като запазва
колоните, ключовете, колоните за идентичност, ключовете на редовете и филтрите на физическата таблица.
