Zum Inhalt springen

Guarded Apply

GuardedApplier wendet eine konfliktfreie MergeResult nur an, wenn die Zieldatenbank noch mit dem erwarteten Live-Snapshot übereinstimmt. Das ist nützlich, wenn ein Zusammenführungsplan früher geprüft wurde und sich Live-Zeilen vor der Anwendung geändert haben könnten.

Typischerweise werden Änderungen aus einer Sandbox in eine Live-Datenbank übernommen. Base ist der Live-Stand zum Zeitpunkt, an dem die Sandbox daraus erstellt wurde, ours ist die bearbeitete Sandbox und theirs ist der aktuelle Live-Stand. Das Ziel ist die Live-Datenbank, daher lautet der erwartete Live-Snapshot theirs. Geschrieben werden die Operationen, die live noch fehlen (siehe Operationen auswählen).

PHP
use Merql\Connection;
use Merql\Merql;
use Merql\Plan\OperationSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

Merql::init(Connection::sqlite('/data/live.sqlite'));

$plan = Merql::plan('promote-1', 'base', 'ours', 'theirs');

// Skip rows with unresolved conflicts.
$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('base'),
);

$applied = Merql::applyGuarded($selected, SnapshotStore::load('theirs'));

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

Merql::applyGuarded() schreibt in die an Merql::init() übergebene Datenbank. Der erwartete Live-Snapshot muss dieselbe Datenbank beschreiben: Erfasse ihn aus dem Ziel und so kurz vorher, dass die Vorbedingungen aussagekräftig sind.

Vorbedingungen

Guarded SQL ergänzt die generierten Anweisungen um Vorbedingungen für Live-Zeilen:

  • Bei Aktualisierungen muss die Zielzeilenidentität vorhanden sein und die Werte der Nicht-Identitätsspalten müssen der erwarteten Live-Zeile entsprechen.
  • Bei Löschungen muss die Zielzeilenidentität vorhanden sein und die Werte der Nicht-Identitätsspalten müssen der erwarteten Live-Zeile entsprechen.
  • Bei Einfügungen darf die Zielidentität noch nicht vorhanden sein.

Wenn eine Anweisung keine Zeilen betrifft, behandelt merql den Plan als veraltet. Die Transaktion wird zurückgerollt und das zurückgegebene ApplyResult enthält den Fehler Optimistic precondition failed for guarded merge statement.

Bei MySQL betrifft eine Aktualisierung, deren Werte bereits der Live-Zeile entsprechen, ebenfalls null Zeilen, da MySQL geänderte statt gefundene Zeilen zählt. Eine solche Aktualisierung gilt als veraltet, obwohl die Zeile übereinstimmt (#2). SQLite zählt gefundene Zeilen und akzeptiert sie. Das kann bei einer merged- Aktualisierung passieren, wenn beide Seiten dieselbe Änderung an einer Zeile vorgenommen haben.

Operationen auswählen

Ein Zusammenführungsergebnis enthält die Operationen beider Seiten, da es den Basiszustand in den zusammengeführten Zustand überführt. Wird es auf ein Ziel angewendet, das bereits theirs enthält, beschreiben die von theirs stammenden Operationen Änderungen, die das Ziel schon hat, und ihre Vorbedingungen schlagen fehl:

Operation Zustand in einem Ziel mit theirs Auswählen
ours Einfügen, Aktualisieren oder Löschen Fehlt Ja
merged Aktualisierung (beide Seiten haben die Zeile geändert) Enthält die Spalten von theirs, ours fehlt Ja
theirs Einfügen Zeile vorhanden, daher betrifft die abgesicherte Einfügung null Zeilen Nein
merged Löschen (beide Seiten haben die Zeile gelöscht) Zeile ist bereits entfernt, daher betrifft das abgesicherte Löschen null Zeilen Nein
theirs Aktualisierung oder Löschung Bereits angewendet Nein

Das gesamte Ergebnis von Merql::merge() an applyGuarded() zu übergeben, schlägt daher fehl, sobald theirs eine Zeile eingefügt oder beide Seiten eine Zeile gelöscht haben. Wähle zuerst die Operationen aus, nach Quelle wie oben oder mit einem ChangeGroupSelection aus einem Prüf-UI. Lass Zeilen aus $plan->conflicts weg: Ein ausgewähltes Ergebnis ist immer konfliktfrei, und die Operation für eine Konfliktzeile enthält den Wert von ours in der betreffenden Spalte (siehe Zusammenführungspläne).

Ausgewählte Anwendung

Guarded Apply funktioniert mit ausgewählten Zusammenführungsergebnissen aus einem Zusammenführungsplan, einschließlich Auswahlen nach Änderungsgruppe.

PHP
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

$selection = ChangeGroupSelection::fromIds([$plan->changeGroups[0]->id]);
$selected = (new SelectedMergeResultFactory())->fromChangeGroupSelection(
    $plan,
    $selection,
    SnapshotStore::load($plan->baseSnapshot),
);

$applied = Merql::applyGuarded($selected, SnapshotStore::load($plan->theirsSnapshot));

Damit kann eine Host-Anwendung eine Teilmenge geprüfter Gruppen bereitstellen und zugleich sicherstellen, dass Live-Zeilen seit der Planerstellung unverändert geblieben sind.

SQL-Vorschau

Verwende SqlGenerator::generateGuarded(), um die abgesicherten Anweisungen zu prüfen, ohne sie auszuführen.

PHP
use Merql\Apply\SqlGenerator;
use Merql\Snapshot\SnapshotStore;

$statements = SqlGenerator::generateGuarded(
    $selected,
    SnapshotStore::load($plan->theirsSnapshot),
);

foreach ($statements as $statement) {
    echo $statement['sql'] . "\n";
}

Die generierten Anweisungen sind weiterhin parametrisiert. Vorbedingungen werden bei Aktualisierungen und Löschungen als zusätzliche WHERE-Klauseln und bei Einfügungen als NOT EXISTS-Prüfungen ausgedrückt.

Abgesicherte oder einfache Anwendung

Merql::apply() führt dieselben Operationen ohne Vorbedingungen aus. Aktualisierungen und Löschungen sind dann erfolgreich, unabhängig davon, ob sich die Live-Zeile seit der Planerstellung geändert hat. Eine Einfügung einer vorhandenen Zeile schlägt mit dem Duplikat-Schlüsselfehler der Datenbank fehl. Verwende die einfache Anwendung für ein Ziel, von dem du weißt, dass es noch den Basiszustand enthält, und Guarded Apply, wenn andere Schreibvorgänge das Ziel seit dem Snapshot verändert haben könnten.