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