
# Skyddad tillämpning

`GuardedApplier` tillämpar en konfliktfri `MergeResult` endast om måldatabasen fortfarande motsvarar den förväntade aktuella ögonblicksbilden. Det är användbart när en sammanslagningsplan har granskats tidigare och aktuella rader kan ha ändrats före tillämpningen.

En vanlig konfiguration flyttar ändringarna i en sandlåda till en produktionsdatabas. Basen är produktionen när sandlådan kopierades från den, vår sida är den redigerade sandlådan och deras sida är produktionen som den ser ut nu. Målet är produktionsdatabasen, så den förväntade aktuella ögonblicksbilden är `theirs`, och åtgärderna som ska skrivas är de som produktionen ännu inte har (se [Välja åtgärder](#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()` skriver till databasen som skickats till `Merql::init()`. Den förväntade aktuella ögonblicksbilden måste beskriva samma databas: hämta den från målet och se till att den är tillräckligt färsk för att förvillkoren ska vara meningsfulla.

## Förvillkor

Skyddad SQL lägger till förvillkor för aktuella rader i genererade satser:

- uppdateringar kräver att målradsidentiteten finns och att värdena för kolumner som inte är identitetskolumner motsvarar den förväntade aktuella raden,
- borttagningar kräver att målradsidentiteten finns och att värdena för kolumner som inte är identitetskolumner motsvarar den förväntade aktuella raden,
- infogningar kräver att målets identitet inte redan finns.

Om någon sats påverkar noll rader behandlar merql planen som inaktuell. Transaktionen återställs och det returnerade `ApplyResult` innehåller felet `Optimistic precondition failed for guarded merge statement.`.

På MySQL påverkar en uppdatering vars värden redan motsvarar den aktuella raden också noll rader, eftersom MySQL räknar ändrade rader i stället för matchade rader. Uppdateringen misslyckas då som inaktuell trots att raden matchar ([#2](https://github.com/inline0/merql/issues/2)). SQLite räknar matchade rader och godtar den. Detta kan inträffa vid en `merged`-uppdatering när båda sidorna har gjort samma ändring av en rad.

## Välja åtgärder

Ett sammanslagningsresultat innehåller åtgärder från båda sidorna eftersom det omvandlar basen till det sammanslagna tillståndet. När det tillämpas på ett mål som redan innehåller deras sida beskriver åtgärderna från deras sida ändringar som målet redan har, så deras förvillkor misslyckas:

| Åtgärd | Tillstånd i ett mål som innehåller deras sida | Välj |
|---|---|---|
| `ours` infogning, uppdatering eller borttagning | Saknas | Ja |
| `merged` uppdatering (båda sidor ändrade raden) | Innehåller deras kolumner, saknar våra | Ja |
| `theirs` infogning | Raden finns, så den skyddade infogningen påverkar noll rader | Nej |
| `merged` borttagning (båda sidor tog bort raden) | Raden är borta, så den skyddade borttagningen påverkar noll rader | Nej |
| `theirs` uppdatering eller borttagning | Redan tillämpad | Nej |

Att skicka hela resultatet från `Merql::merge()` till `applyGuarded()` misslyckas därför så snart deras sida har infogat en rad eller båda sidorna har tagit bort en rad. Välj först åtgärderna, efter källa som ovan eller med en `ChangeGroupSelection` byggd från en granskningsUI. Utelämna raderna i `$plan->conflicts`: ett valt resultat är alltid konfliktfritt, och åtgärden för en rad med konflikt innehåller vår sidas värde i konfliktkolumnen (se [Sammanslagningsplaner](https://merql.inline0.dev/sv/docs/merge/plans#selecting-what-a-target-is-missing)).

## Vald tillämpning

Skyddad tillämpning fungerar med valda sammanslagningsresultat som skapats från en sammanslagningsplan, inklusive urval som gjorts efter ändringsgrupp.

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

Detta gör att ett värdprogram kan förbereda en delmängd av granskade grupper och ändå säkerställa att de aktuella raderna inte har ändrats sedan planen skapades.

## Förhandsgranskning av SQL

Använd `SqlGenerator::generateGuarded()` för att granska de skyddade satserna utan att köra dem.

```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 genererade satserna är fortfarande parameteriserade. Förvillkor uttrycks med ytterligare `WHERE`-villkor för uppdateringar/borttagningar och `NOT EXISTS`-kontroller för infogningar.

## Skyddad eller vanlig tillämpning

`Merql::apply()` kör samma åtgärder utan förvillkor. Uppdateringar och borttagningar lyckas då oavsett om den aktuella raden har ändrats sedan planen skapades, och en infogning av en befintlig rad misslyckas med databasens dubblettnyckelfel. Använd vanlig tillämpning för ett mål som du vet fortfarande innehåller basen, och skyddad tillämpning när andra skrivare kan ha ändrat målet sedan ögonblicksbilden togs.
