Трехстороннее слияние
Трехстороннее слияние сравнивает два набора изменений с общим предком и объединяет их в один результат. merql применяет этот алгоритм к строкам базы данных, а не к строкам файлов.
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 нормализованного содержимого строки) ускоряют проверку. Если два отпечатка совпадают, строка не изменилась, и сравнивать отдельные столбцы не требуется.
use Merql\Diff\Differ;
$differ = new Differ();
$oursChangeset = $differ->diff($base, $ours);
$theirsChangeset = $differ->diff($base, $theirs);
Правила слияния
Затем два набора изменений объединяются по одной операции за раз. В этой таблице указаны результаты для всех сочетаний:
| Наши | Их | Результат |
|---|---|---|
| Без изменений | Без изменений | Оставить базовую версию |
| Без изменений | Обновлено | Принять их изменения |
| Обновлено | Без изменений | Принять наши изменения |
| Обновлено (одно и то же значение) | Обновлено (одно и то же значение) | Принять (совпадают) |
| Обновлено (разные значения) | Обновлено (разные значения) | Конфликт |
| Без изменений | Удалено | Принять удаление |
| Удалено | Без изменений | Принять удаление |
| Удалено | Удалено | Принять удаление (совпадает) |
| Обновлено | Удалено | Конфликт |
| Удалено | Обновлено | Конфликт |
| Добавлено (только у нас) | -- | Принять добавление |
| -- | Добавлено (только у них) | Принять добавление |
| Добавлено (тот же PK) | Добавлено (тот же PK) | Конфликт |
Если обе стороны обновили одну и ту же строку, merql не сразу объявляет конфликт. Он переходит к слиянию на уровне столбцов, сравнивая каждый столбец отдельно. Подробнее см. в разделе Слияние на уровне столбцов.
Использование фасада
Фасад Merql работает с именованными снимками, сохраненными на диске. Перед созданием снимка вызовите Merql::init(), указав базу данных, в которой хранится каждое состояние:
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():
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(). Это слияние двух сторон, при котором наша версия совпадает с базовой, поэтому конфликтов быть не может:
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.
Через фасад:
$result = Merql::patch('base', 'changes');
patch() эквивалентен merge($base, $base, $changes). Поскольку наша версия не отличается от базовой, все изменения с их стороны принимаются без конфликтов.
Обнаружение несоответствий схемы
Если между снимками в таблицу добавили столбцы или удалили их, слияние все равно выполняется, но такие несоответствия фиксируются. Типы измененных столбцов не сравниваются, а переименованный столбец отображается как один удаленный и один добавленный; см. раздел Проверка схемы.
$result = $merge->merge($base, $ours, $theirs);
if ($result->hasSchemaMismatches()) {
foreach ($result->schemaMismatches() as $mismatch) {
echo $mismatch->getMessage() . "\n";
}
}
merql не выполняет автоматическую миграцию схем. За изменения схемы отвечает вызывающий код. Слияние сообщает о них, чтобы вы могли решить, что делать дальше.
MergeResult
Объект результата содержит все необходимое:
$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. Используйте план слияния, чтобы выбрать их, и Защищенное применение, чтобы записать их с предварительными условиями для актуальных строк.