
# 三方合并

三方合并会比较相对于共同祖先的两组更改，并将它们合并为一个结果。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
```

## 三种状态

每次合并都必须包含恰好三个快照：

- **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)
```

没有共同的 base，就无法区分“新增”和“未更改”。双向比较只能看到 A 侧有一个值而 B 侧没有，却无法判断是 A 添加了它，还是 B 删除了它。base 可以回答这个问题。

## 如何计算变更集

`Differ` 会将每个快照与 base 比较，生成两个变更集。每个变更集包含三类操作：

| Base | 当前状态 | 操作 |
|---|---|---|
| 行存在 | 行存在，指纹相同 | 无更改 |
| 行存在 | 行存在，指纹不同 | **UPDATE**（逐列差异） |
| 行存在 | 行缺失 | **DELETE** |
| 行缺失 | 行存在 | **INSERT** |

指纹（SHA-256，即规范化行内容的哈希值）提供了快速处理路径。如果两个指纹相同，则该行未更改，无需进行逐列比较。

```php
use Merql\Diff\Differ;

$differ = new Differ();
$oursChangeset = $differ->diff($base, $ours);
$theirsChangeset = $differ->diff($base, $theirs);
```

## 合并规则

随后会逐个操作合并这两个变更集。此表定义了每种组合对应的结果：

| Ours | Theirs | 结果 |
|---|---|---|
| 无更改 | 无更改 | 保留 base |
| 无更改 | 已更新 | 接受 theirs |
| 已更新 | 无更改 | 接受 ours |
| 已更新（值相同） | 已更新（值相同） | 接受（双方一致） |
| 已更新（值不同） | 已更新（值不同） | **冲突** |
| 无更改 | 已删除 | 接受删除 |
| 已删除 | 无更改 | 接受删除 |
| 已删除 | 已删除 | 接受删除（双方一致） |
| 已更新 | 已删除 | **冲突** |
| 已删除 | 已更新 | **冲突** |
| 已插入（仅 ours） | -- | 接受插入 |
| -- | 已插入（仅 theirs） | 接受插入 |
| 已插入（PK 相同） | 已插入（PK 相同） | **冲突** |

双方都更新同一行时，merql 不会立即判定为冲突，而是继续进行逐列合并，分别比较每一列。详情请参阅 [列级合并](https://merql.inline0.dev/zh/docs/merge/column-level)。

## 使用 facade

`Merql` facade 会对已保存到磁盘的命名快照进行操作。创建快照前，请使用持有各状态的数据库调用 `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');
```

不要依次从同一个数据库获取全部三个快照。这样一来，theirs 也会包含 ours 的更改，因此 ours 插入的每一行都会显示为双方都已插入，并被报告为 `insert_insert` 冲突。

`Merql::merge()`、`Merql::patch()` 和 `Merql::plan()` 会创建不带 `CellMergeConfig` 的 `ThreeWayMerge`，因此它们只会在列级别进行合并。若要进行单元格级合并，请按下文所示加载快照并直接使用该类。

## 直接使用类

如需更多控制，请自行实例化各个组件。`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 的双向合并，因此不会发生冲突：

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

通过 facade：

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

`patch()` 等同于 `merge($base, $base, $changes)`。由于 ours 从未偏离 base，因此 theirs 的所有更改都会被接受，不会发生冲突。

## 架构不匹配检测

如果快照之间的表新增或删除了列，合并仍会继续，但会记录不匹配情况。不会比较已更改的列类型；重命名的列会显示为一个已删除列和一个新增列，详见 [架构验证](https://merql.inline0.dev/zh/docs/advanced/schema)。

```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` 表示双方都更新过该行（逐列合并），或双方都已删除该行。

## 合并结果适用的场景

合并结果描述了如何将 base 转换为合并后的状态。其操作包含双方的更改：ours 插入的行和 theirs 插入的行都会显示为插入操作。

- `Merql::apply()`（或 `Applier`）会对已连接的数据库执行每项操作。它适用于仍持有 base 的数据库，例如 base 状态的新副本。如果数据库已包含一方的更改，则插入已存在的行会失败，并回滚事务。
- 若要将 ours 应用到已持有 theirs 的数据库（例如将沙盒编辑内容推送到线上），只应用该数据库缺少的操作：来源为 `ours` 的操作，以及 `merged` 更新。使用 [合并计划](https://merql.inline0.dev/zh/docs/merge/plans) 选择这些操作，并使用 [受保护的应用](https://merql.inline0.dev/zh/docs/apply/guarded)，在写入时采用实时行前置条件。
