
# Планы слияния

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

План содержит:

- имена снимков, использованных для его создания,
- сводку операций, конфликтов, несовпадений схем, таблиц и групп,
- стабильные идентификаторы операций,
- стабильные идентификаторы групп изменений,
- столбцы идентификаторов строк и значения идентификаторов,
- значения строк из базы, нашей стороны, их стороны и результата для каждой операции,
- необязательные метаданные приложения-хоста,
- хеш плана,
- версию схемы.

## Стабильные идентификаторы

Идентификаторы операций детерминированы для одинакового содержимого операций. Они вычисляются на основе имени таблицы, ключа строки, типа операции, источника операции, измененных столбцов и значений результата.

Идентификаторы групп изменений детерминированы для одинакового содержимого групп. По умолчанию построитель создает одну группу операций для каждой операции. Приложения-хосты могут создавать более крупные группы, специфичные для предметной области, на основе списка операций и сериализовать их с той же структурой 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()`.

## Выбор отсутствующих в целевой базе данных изменений

План содержит операции обеих сторон, поскольку результат слияния преобразует базу в объединенное состояние. Если в целевой базе уже есть состояние одной из сторон, выберите только те операции, которых в ней еще нет. Для целевой базы с состоянием их стороны, например рабочей базы данных, в которую передаются изменения из тестовой копии, это все операции с источником `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` (строки, удаленные обеими сторонами) уже присутствуют в целевой базе.

План, построенный на основе слияния с конфликтами, по-прежнему содержит операцию для каждой конфликтующей строки, обновленной обеими сторонами. В результате для конфликтующего столбца сохраняется наше значение, а `SelectedMergeResultFactory` возвращает чистый результат для любого набора выбранных операций. И у операции, и у группы изменений значение `conflictState` даже в этом случае равно `clean` ([#9](https://github.com/inline0/merql/issues/9)). Поэтому самостоятельно проверьте `$plan->conflicts`, как описано выше, или разрешите конфликты с помощью `ConflictResolver` и создайте план на основе разрешенного результата с помощью `MergePlanBuilder`. См. раздел [Защищенное применение](https://merql.inline0.dev/ru/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',
]));
```

Схема, сохраненная в снимке, использует каноническое имя таблицы, сохраняя при этом столбцы, ключи, столбцы идентификаторов, ключи строк и фильтры физической таблицы.
