三方合并
三方合并会比较相对于共同祖先的两组更改,并将它们合并为一个结果。merql 将这一算法应用于数据库行,而非文件行。
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,即规范化行内容的哈希值)提供了快速处理路径。如果两个指纹相同,则该行未更改,无需进行逐列比较。
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 不会立即判定为冲突,而是继续进行逐列合并,分别比较每一列。详情请参阅 列级合并。
使用 facade
Merql facade 会对已保存到磁盘的命名快照进行操作。创建快照前,请使用持有各状态的数据库调用 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');
不要依次从同一个数据库获取全部三个快照。这样一来,theirs 也会包含 ours 的更改,因此 ours 插入的每一行都会显示为双方都已插入,并被报告为 insert_insert 冲突。
Merql::merge()、Merql::patch() 和 Merql::plan() 会创建不带 CellMergeConfig 的 ThreeWayMerge,因此它们只会在列级别进行合并。若要进行单元格级合并,请按下文所示加载快照并直接使用该类。
直接使用类
如需更多控制,请自行实例化各个组件。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 的双向合并,因此不会发生冲突:
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.
通过 facade:
$result = Merql::patch('base', 'changes');
patch() 等同于 merge($base, $base, $changes)。由于 ours 从未偏离 base,因此 theirs 的所有更改都会被接受,不会发生冲突。
架构不匹配检测
如果快照之间的表新增或删除了列,合并仍会继续,但会记录不匹配情况。不会比较已更改的列类型;重命名的列会显示为一个已删除列和一个新增列,详见 架构验证。
$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 表示双方都更新过该行(逐列合并),或双方都已删除该行。
合并结果适用的场景
合并结果描述了如何将 base 转换为合并后的状态。其操作包含双方的更改:ours 插入的行和 theirs 插入的行都会显示为插入操作。