
# Плани злиття

`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 груп змін,
- стовпці ідентифікації рядків і значення ідентифікаторів,
- значення рядків для бази, нашої сторони, їхньої сторони та результату для кожної операції,
- необов’язкові метадані застосунку-хоста,
- хеш плану,
- версію схеми.

## Стабільні 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()`.

## Вибір відсутнього в цільовій базі

План містить операції обох сторін, оскільки результат злиття перетворює базу на об’єднаний стан. Якщо в цільовій базі вже є стан однієї сторони, виберіть лише ті операції, яких там ще немає. Для цільової бази зі станом їхньої сторони, наприклад робочої бази, до якої переносять зміни тестової копії, це всі операції з джерелом `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/uk/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',
]));
```

Схема, збережена у знімку, використовує канонічну назву таблиці та зберігає стовпці, ключі, стовпці ідентифікації, ключі рядків і фільтри фізичної таблиці.
