
# Zastosowanie z kontrolą

`GuardedApplier` stosuje wynik `MergeResult` tylko wtedy, gdy docelowa baza danych nadal odpowiada oczekiwanemu zrzutowi bieżącego stanu. Przydaje się to, gdy plan scalania został wcześniej sprawdzony, a bieżące wiersze mogły się zmienić przed zastosowaniem.

Typowa konfiguracja przenosi zmiany z piaskownicy do bazy produkcyjnej. Podstawa to stan produkcyjny z chwili skopiowania go do piaskownicy, „nasze” to zmodyfikowana piaskownica, a „ich” to bieżący stan produkcyjny. Celem jest baza produkcyjna, więc oczekiwanym zrzutem bieżącego stanu jest `theirs`, a operacje do zapisania to te, których brakuje w bazie produkcyjnej (zob. [Wybieranie operacji](#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()` zapisuje zmiany w bazie przekazanej do `Merql::init()`. Oczekiwany zrzut bieżącego stanu musi opisywać tę samą bazę: pobierz go z celu i zadbaj, by był wystarczająco aktualny, aby warunki wstępne miały znaczenie.

## Warunki wstępne

Zastosowanie z kontrolą SQL dodaje do generowanych instrukcji warunki wstępne dotyczące bieżących wierszy:

- aktualizacje wymagają, aby wiersz o wskazanej tożsamości istniał w bazie docelowej, a wartości kolumn niebędących kolumnami identyfikacyjnymi odpowiadały oczekiwanemu bieżącemu wierszowi,
- usunięcia wymagają, aby wiersz o wskazanej tożsamości istniał w bazie docelowej, a wartości kolumn niebędących kolumnami identyfikacyjnymi odpowiadały oczekiwanemu bieżącemu wierszowi,
- wstawienia wymagają, aby w bazie docelowej nie istniał jeszcze wiersz o danej tożsamości.

Jeśli dowolna instrukcja zmieni zero wierszy, merql uznaje plan za nieaktualny. Transakcja zostaje wycofana, a zwrócony `ApplyResult` zawiera błąd `Optimistic precondition failed for guarded merge statement.`

W MySQL aktualizacja, której wartości są już takie same jak w bieżącym wierszu, również zmienia zero wierszy, ponieważ MySQL zlicza zmienione wiersze, a nie wiersze spełniające warunek. Taka aktualizacja kończy się błędem z powodu nieaktualnego planu, mimo że wiersz jest zgodny ([#2](https://github.com/inline0/merql/issues/2)). SQLite zlicza wiersze spełniające warunek i akceptuje taką aktualizację. Może się to zdarzyć podczas aktualizacji `merged`, gdy obie strony wprowadziły tę samą zmianę w wierszu.

## Wybieranie operacji

Wynik scalania zawiera operacje obu stron, ponieważ przekształca stan podstawowy w stan scalony. Zastosowane do celu, który zawiera już stan „ich”, operacje pochodzące od nich opisują zmiany, które cel już zawiera, więc ich warunki wstępne nie zostaną spełnione:

| Operacja | Stan w celu zawierającym zmiany „ich” | Wybierz |
|---|---|---|
| `ours` wstawienie, aktualizacja lub usunięcie | Brak | Tak |
| `merged` aktualizacja (obie strony zmieniły wiersz) | Zawiera kolumny „ich”, brakuje kolumn „naszych” | Tak |
| `theirs` wstawienie | Wiersz istnieje, więc wstawienie z kontrolą zmienia zero wierszy | Nie |
| `merged` usunięcie (obie strony usunęły wiersz) | Wiersz nie istnieje, więc usunięcie z kontrolą zmienia zero wierszy | Nie |
| `theirs` aktualizacja lub usunięcie | Już zastosowano | Nie |

Przekazanie całego wyniku `Merql::merge()` do `applyGuarded()` zakończy się więc błędem, gdy tylko strona „ich” wstawi wiersz lub obie strony go usuną. Najpierw wybierz operacje według źródła, jak powyżej, albo za pomocą `ChangeGroupSelection` utworzonego na podstawie przeglądu UI. Pomiń wiersze wymienione w `$plan->conflicts`: wybrany wynik jest zawsze wolny od konfliktów, a operacja dotycząca wiersza w konflikcie zawiera wartość strony „naszej” w kolumnie, której dotyczy konflikt (zob. [Plany scalania](https://merql.inline0.dev/pl/docs/merge/plans#selecting-what-a-target-is-missing)).

## Zastosowanie wybranego wyniku

Zastosowanie z kontrolą działa z wybranymi wynikami scalania utworzonymi na podstawie planu scalania, w tym z wyborami dokonanymi według grup zmian.

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

Pozwala to aplikacji nadrzędnej przygotować podzbiór sprawdzonych grup i nadal zagwarantować, że bieżące wiersze nie zmieniły się od utworzenia planu.

## Podgląd SQL

Użyj `SqlGenerator::generateGuarded()`, aby sprawdzić instrukcje z kontrolą bez ich wykonywania.

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

Wygenerowane instrukcje nadal używają parametrów. Warunki wstępne są wyrażone za pomocą dodatkowych klauzul `WHERE` dla aktualizacji i usunięć oraz sprawdzeń `NOT EXISTS` dla wstawień.

## Zastosowanie z kontrolą lub zwykłe

`Merql::apply()` wykonuje te same operacje bez warunków wstępnych. Aktualizacje i usunięcia powiodą się niezależnie od tego, czy bieżący wiersz zmienił się od utworzenia planu, a wstawienie istniejącego wiersza zakończy się błędem duplikatu klucza bazy danych. Użyj zwykłego zastosowania dla celu, który na pewno nadal zawiera stan podstawowy, a zastosowania z kontrolą, gdy inni użytkownicy mogli zmienić cel od czasu utworzenia zrzutu.
