
# Beveiligd toepassen

`GuardedApplier` past een schoon `MergeResult` alleen toe als de doeldatabase nog overeenkomt met de verwachte livesnapshot. Dit is handig wanneer een samenvoegingsplan eerder is beoordeeld en live rijen vóór het toepassen kunnen zijn gewijzigd.

De gebruikelijke opzet zet bewerkingen uit een sandbox over naar een live database. De basis is live op het moment waarop de sandbox ervan is gekopieerd, ours is de bewerkte sandbox en theirs is live zoals die nu is. De live database is het doel, dus de verwachte livesnapshot is `theirs`. De te schrijven bewerkingen zijn de bewerkingen die live nog niet bevat (zie [Bewerkingen kiezen](#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()` schrijft naar de database die aan `Merql::init()` is doorgegeven. De verwachte livesnapshot moet diezelfde database beschrijven: haal de snapshot op uit het doel en zorg dat deze recent genoeg is om de voorwaarden zinvol te maken.

## Voorwaarden

Beveiligd SQL voegt voorwaarden voor live rijen toe aan gegenereerde instructies:

- bij updates moet de identiteit van de doelrij bestaan en moeten de waarden van de niet-identiteitskolommen overeenkomen met de verwachte live rij,
- bij deletes moet de identiteit van de doelrij bestaan en moeten de waarden van de niet-identiteitskolommen overeenkomen met de verwachte live rij,
- bij inserts mag de doelidentiteit nog niet bestaan.

Als een instructie nul rijen beïnvloedt, beschouwt merql het plan als verouderd. De transactie wordt teruggedraaid en het geretourneerde `ApplyResult` bevat de fout `Optimistic precondition failed for guarded merge statement.`

In MySQL beïnvloedt een update waarvan de waarden al overeenkomen met de live rij ook nul rijen, omdat MySQL gewijzigde rijen telt in plaats van overeenkomende rijen. Zo'n update mislukt als verouderd, ook al komt de rij overeen ([#2](https://github.com/inline0/merql/issues/2)). SQLite telt overeenkomende rijen en accepteert de update. Dit kan gebeuren bij een `merged`-update wanneer beide kanten dezelfde wijziging aan een rij hebben aangebracht.

## Bewerkingen kiezen

Een samenvoegingsresultaat bevat de bewerkingen van beide kanten, omdat het de basis omzet in de samengevoegde status. Toegepast op een doel dat theirs al bevat, beschrijven de bewerkingen van theirs wijzigingen die het doel al heeft. De bijbehorende voorwaarden mislukken:

| Bewerking | Status in een doel dat theirs bevat | Selecteren |
|---|---|---|
| `ours` insert, update of delete | Ontbreekt | Ja |
| `merged` update (beide kanten hebben de rij gewijzigd) | Bevat de kolommen van theirs, ours ontbreekt | Ja |
| `theirs` insert | Rij bestaat, dus de beveiligde insert beïnvloedt nul rijen | Nee |
| `merged` delete (beide kanten hebben de rij verwijderd) | Rij is weg, dus de beveiligde delete beïnvloedt nul rijen | Nee |
| `theirs` update of delete | Al toegepast | Nee |

Als u het hele resultaat van `Merql::merge()` aan `applyGuarded()` doorgeeft, mislukt dit zodra theirs een rij heeft ingevoegd of beide kanten een rij hebben verwijderd. Selecteer daarom eerst de bewerkingen, op basis van de bron zoals hierboven of met een `ChangeGroupSelection` op basis van een beoordeling UI. Laat rijen weg die in `$plan->conflicts` staan: een geselecteerd resultaat is altijd schoon en de bewerking voor een conflicterende rij bevat de waarde van ours voor de conflicterende kolom (zie [Samenvoegingsplannen](https://merql.inline0.dev/nl/docs/merge/plans#selecting-what-a-target-is-missing)).

## Geselecteerd toepassen

Beveiligd toepassen werkt met geselecteerde samenvoegingsresultaten uit een samenvoegingsplan, ook wanneer de selectie op basis van wijzigingsgroepen is gemaakt.

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

Zo kan een hostapplicatie een deelverzameling van beoordeelde groepen klaarzetten en toch bevestigen dat live rijen sinds het maken van het plan niet zijn gewijzigd.

## Preview van SQL

Gebruik `SqlGenerator::generateGuarded()` om de beveiligde instructies te bekijken zonder ze uit te voeren.

```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";
}
```

De gegenereerde instructies zijn nog steeds geparametriseerd. Voorwaarden worden uitgedrukt met extra `WHERE`-clausules voor updates/deletes en `NOT EXISTS`-controles voor inserts.

## Beveiligd of gewoon toepassen

`Merql::apply()` voert dezelfde bewerkingen uit zonder voorwaarden. Updates en deletes slagen dan ongeacht of de live rij sinds het opstellen van het plan is gewijzigd. Een insert van een bestaande rij mislukt met de duplicate key-fout van de database. Gebruik gewoon toepassen voor een doel waarvan u weet dat het nog de basis bevat, en beveiligd toepassen wanneer andere schrijvers het doel mogelijk hebben gewijzigd sinds de snapshot is gemaakt.
