
# 回滚

回滚计划描述如何撤销已应用的选定合并操作。它们在应用之前构建，使用写入发生前一刻存在的确切实时行。

不要仅根据基准快照生成回滚计划。基准是共同祖先，不一定是即将被覆盖的实时状态。

## 构建回滚计划

```php
use Merql\Plan\ChangeGroupSelection;
use Merql\Rollback\RollbackPlanBuilder;
use Merql\Snapshot\SnapshotStore;

$selection = ChangeGroupSelection::fromIds([$plan->changeGroups[0]->id]);
$operationSelection = $selection->toOperationSelection($plan);
$expectedLive = SnapshotStore::load($plan->theirsSnapshot);

$liveBeforeRows = [];
foreach ($plan->operations as $operation) {
    if (!$operationSelection->contains($operation->id)) {
        continue;
    }

    $liveBeforeRows[$operation->id] = $expectedLive
        ->getTable($operation->table)
        ?->getRow($operation->rowKey);
}

$rollback = (new RollbackPlanBuilder())->build(
    'rollback-1',
    $plan,
    $operationSelection,
    $liveBeforeRows,
    metadata: ['promotion_id' => 'promotion-1'],
);
```

计划会存储：

- 合并计划 ID，
- 选定的操作 ID，
- 涉及的表，
- 逆向操作，
- 操作前的行，
- 预期的操作后行，
- 元数据，
- 回滚哈希，
- 架构版本。

## 漂移检查

应用回滚之前，将当前实时行与回滚计划中的预期操作后行进行比较。如果任何行自提升以来发生了变化，默认情况下会中止。

```php
use Merql\Rollback\RollbackDrift;

$currentRows = [
    $operationId => $currentLiveRow,
];

$drifted = (new RollbackDrift())->driftedOperationIds($rollback, $currentRows);

if ($drifted !== []) {
    throw new RuntimeException('Rollback drifted: ' . implode(', ', $drifted));
}
```

漂移检查会比较行指纹，因此列顺序无关紧要。

## 应用回滚

没有漂移时，应用逆向操作。

```php
use Merql\Rollback\RollbackPlanApplier;

$applied = (new RollbackPlanApplier($connection))->apply($rollback);

if ($applied->hasErrors()) {
    foreach ($applied->errors() as $error) {
        echo $error . "\n";
    }
}
```

回滚会根据逆向操作生成一个普通的 `MergeResult`。它通过与常规合并应用相同的事务执行器路径运行。

## 序列化

在应用正向合并之前持久化回滚计划。该产物是之后执行回滚的可靠依据。

```php
use Merql\Rollback\RollbackPlanSerializer;

$json = RollbackPlanSerializer::toJson($rollback);
$restored = RollbackPlanSerializer::fromJson($json);
```

应用应将序列化后的计划与执行实时写入的操作一同存储，并附上任何主机特定的审计元数据。
