コンテンツへスキップ

3ウェイマージ

3ウェイマージは、共通の祖先を基準に2つの変更セットを比較し、それらを1つの結果にまとめます。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

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つのフィンガープリントが一致すれば、行に変更はなく、列レベルの比較は不要です。

PHP
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()を呼び出します:

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');

1つのデータベースから3つのスナップショットを順に取得しないでください。その場合、theirsにもoursの変更が含まれるため、oursが挿入したすべての行が両側で挿入されたとみなされ、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()を使います。これはoursがbaseと等しい2ウェイマージのため、競合は起こりません:

PHP
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.

ファサードを使う場合:

PHP
$result = Merql::patch('base', 'changes');

patch()はmerge($base, $base, $changes)と同等です。oursはbaseから分岐していないため、theirsの変更はすべて競合なく適用されます。

スキーマ不一致の検出

スナップショット間でテーブルに列が追加または削除されても、マージは続行され、不一致が記録されます。列の型変更は比較されません。列名の変更は、1つの列の削除と別の列の追加として扱われます。詳しくはスキーマ検証を参照してください。

PHP
$result = $merge->merge($base, $ours, $theirs);

if ($result->hasSchemaMismatches()) {
    foreach ($result->schemaMismatches() as $mismatch) {
        echo $mismatch->getMessage() . "\n";
    }
}

merqlはスキーマを自動移行しません。スキーマ変更は呼び出し側の責任です。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は、両側が更新した行(列ごとに統合)または両側が削除した行を示します。

マージ結果を適用する場所

マージ結果は、基準をマージ後の状態に変える方法を示します。操作には両側の変更が含まれるため、oursが行った挿入もtheirsが行った挿入も、挿入として表示されます。

  • Merql::apply()(またはApplier)は、接続先データベースに対してすべての操作を実行します。基準状態の新しいコピーなど、基準を保持しているデータベースにはそのまま適用できます。片側の変更がすでに含まれるデータベースに適用すると、存在する行の挿入に失敗し、トランザクションがロールバックされます。
  • oursの変更を、theirsをすでに保持しているデータベース(サンドボックスでの編集をライブ環境に反映する場合など)に取り込むには、そのデータベースにない操作だけを適用します。つまり、ソースがoursの操作とmerged更新です。マージ計画でそれらを選択し、Guarded Applyでライブ行の事前条件を付けて書き込みます。