
# Трехстороннее слияние

Трехстороннее слияние сравнивает два набора изменений с общим предком и объединяет их в один результат. 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/ru/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/ru/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/ru/docs/merge/plans), чтобы выбрать их, и [Защищенное применение](https://merql.inline0.dev/ru/docs/apply/guarded), чтобы записать их с предварительными условиями для актуальных строк.
