Перейти к содержимому

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

Трехстороннее слияние сравнивает два набора изменений с общим предком и объединяет их в один результат. 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 не сразу объявляет конфликт. Он переходит к слиянию на уровне столбцов, сравнивая каждый столбец отдельно. Подробнее см. в разделе Слияние на уровне столбцов.

Использование фасада

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

Обнаружение несоответствий схемы

Если между снимками в таблицу добавили столбцы или удалили их, слияние все равно выполняется, но такие несоответствия фиксируются. Типы измененных столбцов не сравниваются, а переименованный столбец отображается как один удаленный и один добавленный; см. раздел Проверка схемы.

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. Используйте план слияния, чтобы выбрать их, и Защищенное применение, чтобы записать их с предварительными условиями для актуальных строк.