
# Тристороннє злиття

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

```php
use Merql\Merge\ThreeWayMerge;

$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);

echo $result->operationCount(); // resolved operations
echo $result->conflictCount();  // unresolved conflicts
```

## Три стани

Для кожного злиття потрібні рівно три знімки:

- **База** -- стан бази даних у момент розгалуження обох гілок.
- **Наша сторона** -- одна гілка після внесення змін, наприклад змінена тестова копія.
- **Їхня сторона** -- інша гілка після внесення змін, наприклад робоча база, яка продовжувала працювати.

Знімки нашої та їхньої сторін потрібно робити з бази даних, у якій зберігається відповідна гілка. Правила злиття однаково обробляють обидві сторони; назви визначають `source` кожної операції, назви типів конфліктів (`update_delete` означає, що наша сторона оновила рядок, а їхня — видалила), а також те, якій стороні віддають перевагу `ConflictPolicy::OursWins` і `TheirsWins`.

```
         Base (snapshot at fork time)
        /                              \
   Ours (our changes)                   Theirs (their changes)
        \                              /
         --------- MERGE ----------
                     |
              Merged result
              + conflicts (if any)
```

Без спільної бази неможливо відрізнити «додано» від «без змін». Порівняння двох станів показує, що сторона A має значення, а сторона B — ні, але не дає змоги визначити, чи додала його сторона A, чи видалила сторона B. Це з’ясовується завдяки базі.

## Як обчислюються набори змін

`Differ` порівнює кожен знімок із базою та створює два набори змін. Кожен набір містить три типи операцій:

| База | Поточний стан | Операція |
|---|---|---|
| Рядок існує | Рядок існує, відбиток збігається | Без змін |
| Рядок існує | Рядок існує, відбиток відрізняється | **UPDATE** (порівняння за стовпцями) |
| Рядок існує | Рядка немає | **DELETE** |
| Рядка немає | Рядок існує | **INSERT** |

Відбитки (хеші SHA-256 нормалізованого вмісту рядка) забезпечують швидку перевірку. Якщо два відбитки збігаються, рядок не змінено, і порівнювати його стовпці не потрібно.

```php
use Merql\Diff\Differ;

$differ = new Differ();
$oursChangeset = $differ->diff($base, $ours);
$theirsChangeset = $differ->diff($base, $theirs);
```

## Правила злиття

Потім два набори змін об’єднуються операція за операцією. У цій таблиці визначено результат для кожної комбінації:

| Наша сторона | Їхня сторона | Результат |
|---|---|---|
| Без змін | Без змін | Залишити базу |
| Без змін | Оновлено | Прийняти їхні зміни |
| Оновлено | Без змін | Прийняти наші зміни |
| Оновлено (однакове значення) | Оновлено (однакове значення) | Прийняти (обидві сторони згодні) |
| Оновлено (різні значення) | Оновлено (різні значення) | **Конфлікт** |
| Без змін | Видалено | Прийняти видалення |
| Видалено | Без змін | Прийняти видалення |
| Видалено | Видалено | Прийняти видалення (обидві сторони згодні) |
| Оновлено | Видалено | **Конфлікт** |
| Видалено | Оновлено | **Конфлікт** |
| Вставлено (лише нашою стороною) | -- | Прийняти вставлення |
| -- | Вставлено (лише їхньою стороною) | Прийняти вставлення |
| Вставлено (однаковий PK) | Вставлено (однаковий PK) | **Конфлікт** |

Коли обидві сторони оновлюють той самий рядок, merql не оголошує конфлікт одразу. Він переходить до злиття на рівні стовпців і порівнює кожен стовпець окремо. Докладніше див. [Злиття на рівні стовпців](https://merql.inline0.dev/uk/docs/merge/column-level).

## Використання фасаду

Фасад `Merql` працює з іменованими знімками, збереженими на диску. Перед створенням знімків викличте `Merql::init()` для бази даних, у якій міститься кожен стан:

```php
use Merql\Connection;
use Merql\Merql;

$live    = Connection::sqlite('/data/live.sqlite');
$sandbox = Connection::sqlite('/data/sandbox.sqlite');

Merql::init($live);
Merql::snapshot('base');      // before the sandbox is copied from live

// ... copy live.sqlite to sandbox.sqlite, then both databases change ...

Merql::init($sandbox);
Merql::snapshot('ours');
Merql::init($live);
Merql::snapshot('theirs');

$result = Merql::merge('base', 'ours', 'theirs');
```

Не створюйте всі три знімки послідовно з однієї бази даних. Тоді їхній знімок міститиме також зміни нашої сторони, тож кожен вставлений нами рядок виглядатиме вставленим на обох сторонах і буде зареєстрований як конфлікт `insert_insert`.

`Merql::merge()`, `Merql::patch()` і `Merql::plan()` створюють `ThreeWayMerge` без `CellMergeConfig`, тому злиття відбувається лише на рівні стовпців. Для злиття на рівні комірок завантажте знімки й використовуйте клас безпосередньо, як показано нижче.

## Безпосереднє використання класів

Щоб мати більше контролю, створіть компоненти самостійно. `Snapshotter::capture()` нічого не зберігає, тому тримайте знімки в пам’яті або збережіть їх за допомогою `SnapshotStore::save()`:

```php
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Snapshot\Snapshotter;
use Merql\Snapshot\SnapshotStore;

$base   = (new Snapshotter($live))->capture('base');
// ... copy live to the sandbox, then both databases change ...
$ours   = (new Snapshotter($sandbox))->capture('ours');
$theirs = (new Snapshotter($live))->capture('theirs');

$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);

// Snapshots saved earlier, merged with cell-level merge for TEXT and JSON columns.
$merge = new ThreeWayMerge(CellMergeConfig::auto());
$result = $merge->merge(
    SnapshotStore::load('base'),
    SnapshotStore::load('ours'),
    SnapshotStore::load('theirs'),
);
```

## Скорочений спосіб із патчем

Коли зміни є лише з одного боку (одночасних змін немає), використовуйте `patch()`. Це двостороннє злиття, у якому наша сторона збігається з базою, тож конфлікти неможливі:

```php
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.
```

Через фасад:

```php
$result = Merql::patch('base', 'changes');
```

`patch()` еквівалентний `merge($base, $base, $changes)`. Оскільки наша сторона не відрізнялася від бази, усі зміни їхньої сторони приймаються без конфлікту.

## Виявлення невідповідностей схеми

Якщо між знімками до таблиці додали або з неї вилучили стовпці, злиття все одно виконується, але невідповідності фіксуються. Змінені типи стовпців не порівнюються, а перейменований стовпець визначається як один вилучений і один доданий; див. [Перевірка схеми](https://merql.inline0.dev/uk/docs/advanced/schema).

```php
$result = $merge->merge($base, $ours, $theirs);

if ($result->hasSchemaMismatches()) {
    foreach ($result->schemaMismatches() as $mismatch) {
        echo $mismatch->getMessage() . "\n";
    }
}
```

merql не виконує автоматичної міграції схем. За зміни схеми відповідає той, хто викликає інструмент. Злиття повідомляє про них, щоб ви могли вирішити, як діяти далі.

## MergeResult

Об’єкт результату містить усе необхідне:

```php
$result->isClean();            // true if no conflicts
$result->operations();         // list of MergeOperation objects
$result->conflicts();          // list of Conflict objects
$result->operationCount();     // number of resolved operations
$result->conflictCount();      // number of unresolved conflicts
$result->hasSchemaMismatches(); // true if schemas differ
$result->baseSnapshot();       // the base snapshot used for the merge
```

Кожен `MergeOperation` має тип (`insert`, `update` або `delete`), назву таблиці, ключ рядка, значення стовпців і джерело (`ours`, `theirs` або `merged`). `merged` позначає рядок, який обидві сторони оновили (об’єднавши зміни стовпець за стовпцем) або видалили.

## Де застосовується результат злиття

Результат злиття описує, як перетворити базу на об’єднаний стан. Його операції містять зміни обох сторін: вставлений нашою стороною рядок і вставлений їхньою стороною рядок представлені як вставлення.

- `Merql::apply()` (або `Applier`) виконує кожну операцію в підключеній базі даних. Він без проблем застосовується до бази, у якій досі зберігається стан бази знімка, наприклад до нової копії цього стану. Якщо база вже містить зміни однієї зі сторін, вставлення рядка, який уже існує, завершиться помилкою, а транзакцію буде скасовано.
- Щоб перенести зміни нашої сторони до бази, у якій уже є зміни їхньої сторони, наприклад під час перенесення змін тестового середовища в робочу базу, застосуйте лише відсутні в цій базі операції: з джерелом `ours` і оновлення `merged`. Скористайтеся [планом злиття](https://merql.inline0.dev/uk/docs/merge/plans), щоб вибрати їх, і [Захищеним застосуванням](https://merql.inline0.dev/uk/docs/apply/guarded), щоб записати їх із попередніми умовами для поточних рядків.
