
# 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](#choosing-operations)).

```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](https://github.com/inline0/merql/issues/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](https://merql.inline0.dev/de/docs/merge/plans#selecting-what-a-target-is-missing)).

## 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.
