
# Откат

В планах отката описано, как отменить выбранные операции слияния после их применения. Они создаются до применения на основе точных снимков актуальных строк непосредственно перед записью.

Не создавайте откат только на основе базового снимка. Базовая версия — общий предок, который не обязательно соответствует актуальному состоянию, перезаписываемому при применении.

## Создание плана отката

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

План хранит:

- идентификатор плана слияния,
- идентификаторы выбранных операций,
- затронутые таблицы,
- обратные операции,
- строки до изменений,
- ожидаемые строки после изменений,
- метаданные,
- хеш отката,
- версию схемы.

## Проверка расхождений

Перед применением отката сравните текущие актуальные строки с ожидаемыми строками после изменений из плана отката. Если какая-либо строка изменилась после переноса, по умолчанию выполнение прерывается.

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

Приложениям следует хранить сериализованный план вместе с операцией, выполнившей запись в рабочую базу, и любыми специфичными для принимающего приложения данными аудита.
