跳转到正文

三方合并

三方合并会比较相对于共同祖先的两组更改,并将它们合并为一个结果。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 不会立即判定为冲突,而是继续进行逐列合并,分别比较每一列。详情请参阅 列级合并。

使用 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 的所有更改都会被接受,不会发生冲突。

架构不匹配检测

如果快照之间的表新增或删除了列,合并仍会继续,但会记录不匹配情况。不会比较已更改的列类型;重命名的列会显示为一个已删除列和一个新增列,详见 架构验证。

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 更新。使用 合并计划 选择这些操作,并使用 受保护的应用,在写入时采用实时行前置条件。