3ウェイマージ
3ウェイマージは、共通の祖先を基準に2つの変更セットを比較し、それらを1つの結果にまとめます。merqlはこのアルゴリズムをファイルの行ではなく、データベースの行に適用します。
use Merql\Merge\ThreeWayMerge;
$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);
echo $result->operationCount(); // resolved operations
echo $result->conflictCount(); // unresolved conflicts
3つの状態
マージには、正確に3つのスナップショットが必要です:
- Base -- 両ブランチが分岐した時点のデータベース状態。
- Ours -- 変更後の一方のブランチ。たとえば、編集済みのサンドボックスコピー。
- Theirs -- 変更後のもう一方のブランチ。たとえば、稼働を続けているライブデータベース。
Oursとtheirsは、それぞれのブランチを保持するデータベースから取得する必要があります。マージルールでは両側を同等に扱います。これらの名前によって、各操作のsource、競合の種類の名前(update_deleteはoursが更新し、theirsが削除したことを意味します)、およびConflictPolicy::OursWinsとTheirsWinsがどちら側を優先するかが決まります。
Base (snapshot at fork time)
/ \
Ours (our changes) Theirs (their changes)
\ /
--------- MERGE ----------
|
Merged result
+ conflicts (if any)
共通の基準がないと、「追加」と「変更なし」を区別できません。2ウェイ比較では、サイドAに値がありサイドBにないことはわかりますが、Aが追加したのか、Bが削除したのかは判断できません。その問いに答えるのが基準です。
変更セットの計算方法
Differは各スナップショットを基準と比較し、2つの変更セットを生成します。各変更セットには、次の3種類の操作が含まれます:
| 基準 | 現在 | 操作 |
|---|---|---|
| 行が存在する | 行が存在し、フィンガープリントが同じ | 変更なし |
| 行が存在する | 行が存在し、フィンガープリントが異なる | UPDATE(列ごとの差分) |
| 行が存在する | 行がない | DELETE |
| 行がない | 行が存在する | INSERT |
フィンガープリント(正規化した行内容のSHA-256ハッシュ)は高速化に役立ちます。2つのフィンガープリントが一致すれば、行に変更はなく、列レベルの比較は不要です。
use Merql\Diff\Differ;
$differ = new Differ();
$oursChangeset = $differ->diff($base, $ours);
$theirsChangeset = $differ->diff($base, $theirs);
マージルール
次に2つの変更セットを操作ごとにマージします。この表に、すべての組み合わせに対する結果を示します:
| Ours | Theirs | 結果 |
|---|---|---|
| 変更なし | 変更なし | 基準を維持 |
| 変更なし | 更新 | theirsを採用 |
| 更新 | 変更なし | oursを採用 |
| 更新(同じ値) | 更新(同じ値) | 採用(両者一致) |
| 更新(異なる値) | 更新(異なる値) | 競合 |
| 変更なし | 削除 | 削除を採用 |
| 削除 | 変更なし | 削除を採用 |
| 削除 | 削除 | 削除を採用(両者一致) |
| 更新 | 削除 | 競合 |
| 削除 | 更新 | 競合 |
| 挿入(oursのみ) | -- | 挿入を採用 |
| -- | 挿入(theirsのみ) | 挿入を採用 |
| 挿入(同じPK) | 挿入(同じPK) | 競合 |
両側が同じ行を更新した場合、merqlはすぐに競合と判定しません。列レベルのマージに進み、列を1つずつ比較します。詳しくは列レベルのマージを参照してください。
ファサードの使用
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');
1つのデータベースから3つのスナップショットを順に取得しないでください。その場合、theirsにもoursの変更が含まれるため、oursが挿入したすべての行が両側で挿入されたとみなされ、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()を使います。これはoursがbaseと等しい2ウェイマージのため、競合は起こりません:
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.
ファサードを使う場合:
$result = Merql::patch('base', 'changes');
patch()はmerge($base, $base, $changes)と同等です。oursはbaseから分岐していないため、theirsの変更はすべて競合なく適用されます。
スキーマ不一致の検出
スナップショット間でテーブルに列が追加または削除されても、マージは続行され、不一致が記録されます。列の型変更は比較されません。列名の変更は、1つの列の削除と別の列の追加として扱われます。詳しくはスキーマ検証を参照してください。
$result = $merge->merge($base, $ours, $theirs);
if ($result->hasSchemaMismatches()) {
foreach ($result->schemaMismatches() as $mismatch) {
echo $mismatch->getMessage() . "\n";
}
}
merqlはスキーマを自動移行しません。スキーマ変更は呼び出し側の責任です。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は、両側が更新した行(列ごとに統合)または両側が削除した行を示します。
マージ結果を適用する場所
マージ結果は、基準をマージ後の状態に変える方法を示します。操作には両側の変更が含まれるため、oursが行った挿入もtheirsが行った挿入も、挿入として表示されます。
Merql::apply()(またはApplier)は、接続先データベースに対してすべての操作を実行します。基準状態の新しいコピーなど、基準を保持しているデータベースにはそのまま適用できます。片側の変更がすでに含まれるデータベースに適用すると、存在する行の挿入に失敗し、トランザクションがロールバックされます。- oursの変更を、theirsをすでに保持しているデータベース(サンドボックスでの編集をライブ環境に反映する場合など)に取り込むには、そのデータベースにない操作だけを適用します。つまり、ソースが
oursの操作とmerged更新です。マージ計画でそれらを選択し、Guarded Applyでライブ行の事前条件を付けて書き込みます。