合并计划
MergeResult 是合并的可执行输出。MergePlan 是该输出的可检查形式。当应用需要在写入目标数据库前展示、暂存、序列化或有选择地应用数据库变更时,可以使用它。
use Merql\Merql;
$plan = Merql::plan(
id: 'plan-2026-06-06',
base: 'base',
ours: 'ours',
theirs: 'theirs',
metadata: ['sandbox_id' => 'ow_123'],
);
Merql::plan() 会加载三个指定的快照,并像 Merql::merge() 一样在列级别执行合并。若要对 TEXT 和 JSON 列使用单元格级合并来制定计划,请自行构建结果并传入 MergePlanBuilder:
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Plan\MergePlanBuilder;
use Merql\Snapshot\SnapshotStore;
$base = SnapshotStore::load('base');
$ours = SnapshotStore::load('ours');
$theirs = SnapshotStore::load('theirs');
$result = (new ThreeWayMerge(CellMergeConfig::auto()))->merge($base, $ours, $theirs);
$plan = (new MergePlanBuilder())->build('plan-2026-06-06', $base, $ours, $theirs, $result);
计划包含:
- 用于构建计划的快照名称,
- 操作、冲突、架构不匹配、表和分组的摘要,
- 稳定操作 ID,
- 稳定变更组 ID,
- 行标识列和标识值,
- 每项操作对应的基线、我方、对方和结果行值,
- 可选的宿主应用元数据,
- 计划哈希,
- 架构版本。
稳定 ID
相同操作内容会生成相同的操作 ID。ID 根据表名、行键、操作类型、操作来源、变更列和结果值生成。
对于相同的分组内容,变更组 ID 是确定的。默认构建器会为每个操作创建一个操作组。宿主应用可以根据操作列表构建更大的特定领域分组,并仍使用相同的 DTO 形状对其进行序列化。
foreach ($plan->changeGroups as $group) {
echo $group->id . ': ' . implode(', ', $group->operationIds);
}
操作结构
每个 MergePlanOperation 都保留了足够的上下文,便于审查和回滚。
$operation = $plan->operations[0];
$operation->id; // stable operation ID
$operation->type; // insert, update, delete
$operation->table; // canonical table name
$operation->rowKey; // encoded identity key
$operation->identityColumns; // columns used for identity
$operation->identityValues; // decoded identity values
$operation->source; // ours, theirs, or merged
$operation->baseRow; // row in base snapshot, or null
$operation->oursRow; // row in ours snapshot, or null
$operation->theirsRow; // row in theirs snapshot, or null
$operation->resultValues; // values the merge would write
$operation->changedColumns; // changed result columns
选择
当 UI 对分组进行暂存时,使用 ChangeGroupSelection。应用或生成回滚产物时,将其转换为 OperationSelection。
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;
$selection = ChangeGroupSelection::fromIds([
$plan->changeGroups[0]->id,
]);
$operationSelection = $selection->toOperationSelection($plan);
$selectedResult = (new SelectedMergeResultFactory())->fromOperationSelection(
$plan,
$operationSelection,
SnapshotStore::load($plan->baseSnapshot),
);
所选结果是一个干净的 MergeResult,其中只包含选定的操作。可以将它传递给 Merql::apply() 或 Merql::applyGuarded()。
选择目标缺少的内容
计划包含双方的操作,因为合并结果会将 base 转换为合并后的状态。当目标已包含一方的状态时,只选择它尚未包含的操作。对于持有 theirs 的目标(例如接收沙盒副本编辑内容的线上数据库),这包括所有 source 为 ours 的操作,以及 merged 更新;后者会合并双方对列所做的更改:
use Merql\Plan\OperationSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;
// Rows with an unresolved conflict stay out of the selection.
$conflicted = [];
foreach ($plan->conflicts as $conflict) {
$conflicted[$conflict->table . "\0" . $conflict->rowKey] = true;
}
$ids = [];
foreach ($plan->operations as $operation) {
$missing = $operation->source === 'ours'
|| ($operation->source === 'merged' && $operation->type === 'update');
if ($missing && !isset($conflicted[$operation->table . "\0" . $operation->rowKey])) {
$ids[] = $operation->id;
}
}
$selected = (new SelectedMergeResultFactory())->fromOperationSelection(
$plan,
OperationSelection::fromIds($ids),
SnapshotStore::load($plan->baseSnapshot),
);
source 为 theirs 的操作以及 merged 删除操作(双方都已删除的行)已经存在于该目标中。
由存在冲突的合并生成的计划,仍会为双方都更新过的每个冲突行列出一个操作。其结果值在冲突列中保留 ours 的值,而 SelectedMergeResultFactory 无论选择什么都会返回干净的结果。即使如此,操作和变更组的 conflictState 目前仍显示为 clean(#9),因此请按上述方式自行检查 $plan->conflicts,或使用 ConflictResolver 解决冲突,再通过 MergePlanBuilder 从已解决的结果构建计划。有关使用实时行前置条件写入所选内容,请参阅 受保护的应用。
序列化
合并计划和选择采用稳定的 JSON 序列化格式。如果后续应用步骤必须使用经过审查的确切操作集,请存储序列化后的计划。
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\MergePlanSerializer;
$json = MergePlanSerializer::toJson($plan);
$restored = MergePlanSerializer::fromJson($json);
$selectionJson = json_encode($selection->toArray(), JSON_THROW_ON_ERROR);
$selection = ChangeGroupSelection::fromArray(
json_decode($selectionJson, true, flags: JSON_THROW_ON_ERROR),
);
别名快照
合并计划使用规范表名。如果不同环境中的物理表名不一致,请在规划前使用相同的规范名称捕获它们。
use Merql\Snapshot\SnapshotStore;
use Merql\Snapshot\Snapshotter;
$snapshotter = new Snapshotter($connection);
SnapshotStore::save($snapshotter->captureAliased('live', [
'wp_posts' => 'wp_posts',
]));
SnapshotStore::save($snapshotter->captureAliased('sandbox', [
'wp_onumia_abc_posts' => 'wp_posts',
]));
快照中存储的架构使用规范表名,同时保留物理表的列、键、标识列、行键和筛选条件。