
# 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つずつ比較します。詳しくは[列レベルのマージ](https://merql.inline0.dev/ja/docs/merge/column-level)を参照してください。

## ファサードの使用

`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つの列の削除と別の列の追加として扱われます。詳しくは[スキーマ検証](https://merql.inline0.dev/ja/docs/advanced/schema)を参照してください。

```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`更新です。[マージ計画](https://merql.inline0.dev/ja/docs/merge/plans)でそれらを選択し、[Guarded Apply](https://merql.inline0.dev/ja/docs/apply/guarded)でライブ行の事前条件を付けて書き込みます。
