
# 合并计划

`MergeResult` 是合并的可执行输出。`MergePlan` 是该输出的可检查形式。当应用需要在写入目标数据库前展示、暂存、序列化或有选择地应用数据库变更时，可以使用它。

```php
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`：

```php
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 形状对其进行序列化。

```php
foreach ($plan->changeGroups as $group) {
    echo $group->id . ': ' . implode(', ', $group->operationIds);
}
```

## 操作结构

每个 `MergePlanOperation` 都保留了足够的上下文，便于审查和回滚。

```php
$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`。

```php
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` 更新；后者会合并双方对列所做的更改：

```php
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](https://github.com/inline0/merql/issues/9)），因此请按上述方式自行检查 `$plan->conflicts`，或使用 `ConflictResolver` 解决冲突，再通过 `MergePlanBuilder` 从已解决的结果构建计划。有关使用实时行前置条件写入所选内容，请参阅 [受保护的应用](https://merql.inline0.dev/zh/docs/apply/guarded)。

## 序列化

合并计划和选择采用稳定的 JSON 序列化格式。如果后续应用步骤必须使用经过审查的确切操作集，请存储序列化后的计划。

```php
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),
);
```

## 别名快照

合并计划使用规范表名。如果不同环境中的物理表名不一致，请在规划前使用相同的规范名称捕获它们。

```php
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',
]));
```

快照中存储的架构使用规范表名，同时保留物理表的列、键、标识列、行键和筛选条件。
