Merql
Apply

Rollback

Rollback plans describe how to undo selected merge operations after they have been applied. They are built before apply, using the exact live rows that exist immediately before the write.

Do not generate rollback from the base snapshot alone. Base is the common ancestor, not necessarily the live state being overwritten.

Build A Rollback Plan

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

The plan stores:

  • the merge plan ID,
  • selected operation IDs,
  • touched tables,
  • inverse operations,
  • before rows,
  • expected after rows,
  • metadata,
  • a rollback hash,
  • a schema version.

Drift Check

Before applying rollback, compare current live rows with the rollback plan's expected after rows. If any row has changed since promotion, abort by default.

use Merql\Rollback\RollbackDrift;

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

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

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

The drift check compares row fingerprints, so column order does not matter.

Apply Rollback

When there is no drift, apply the inverse operations.

use Merql\Rollback\RollbackPlanApplier;

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

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

Rollback applies a normal MergeResult built from inverse operations. It runs in the same transactional applier path as ordinary merge application.

Serialization

Persist rollback plans before applying the forward merge. That artifact is the durable basis for later rollback.

use Merql\Rollback\RollbackPlanSerializer;

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

Applications should store the serialized plan with the operation that performed the live write, together with any host-specific audit metadata.

On this page