
# Применение с проверкой

`GuardedApplier` применяет слияние без конфликтов `MergeResult`, только если целевая база данных по-прежнему соответствует ожидаемому актуальному снимку. Это полезно, когда план слияния был проверен ранее, а актуальные строки могли измениться до его применения.

Обычно изменения из изолированной среды переносят в рабочую базу данных. Базовая версия — это рабочая база на момент создания из нее копии изолированной среды, наша версия — измененная изолированная среда, а их версия — текущее состояние рабочей базы. Цель — рабочая база данных, поэтому ожидаемый актуальный снимок — `theirs`, а записать нужно операции, которых в рабочей базе еще нет (см. [Выбор операций](#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()` записывает данные в базу, переданную в `Merql::init()`. Ожидаемый актуальный снимок должен описывать ту же базу: получите его из целевой базы достаточно недавно, чтобы предварительные условия оставались актуальными.

## Предварительные условия

Защищенное SQL добавляет к сгенерированным инструкциям предварительные условия для актуальных строк:

- для обновления целевая строка должна существовать, а значения неидентификационных столбцов должны совпадать с ожидаемой актуальной строкой,
- для удаления целевая строка должна существовать, а значения неидентификационных столбцов должны совпадать с ожидаемой актуальной строкой,
- при вставке целевая запись с таким идентификатором не должна уже существовать.

Если какая-либо инструкция затрагивает ноль строк, merql считает план устаревшим. Транзакция откатывается, а в возвращенном `ApplyResult` содержится ошибка `Optimistic precondition failed for guarded merge statement.`

В MySQL обновление, значения которого уже совпадают с актуальной строкой, тоже затрагивает ноль строк, поскольку MySQL учитывает измененные строки, а не найденные. Такое обновление завершается ошибкой устаревшего плана, хотя строка совпадает ([#2](https://github.com/inline0/merql/issues/2)). SQLite учитывает найденные строки и принимает такое обновление. Это может произойти при `merged` обновлении, если обе стороны внесли одинаковое изменение в строку.

## Выбор операций

Результат слияния содержит операции обеих сторон, поскольку он преобразует базовую версию в объединенное состояние. Если применить его к цели, которая уже содержит их версию, операции, пришедшие от них, описывают изменения, уже имеющиеся в цели, поэтому их предварительные условия не выполняются:

| Операция | Состояние цели, содержащей их версию | Выбрать |
|---|---|---|
| `ours` вставка, обновление или удаление | Отсутствует | Да |
| `merged` обновление (обе стороны изменили строку) | Содержит их столбцы, не содержит наших | Да |
| `theirs` вставка | Строка существует, поэтому защищенная вставка затрагивает ноль строк | Нет |
| `merged` удаление (обе стороны удалили строку) | Строка удалена, поэтому защищенное удаление затрагивает ноль строк | Нет |
| `theirs` обновление или удаление | Уже применено | Нет |

Поэтому передача всего результата `Merql::merge()` в `applyGuarded()` приведет к ошибке, как только их сторона вставит строку или обе стороны удалят одну и ту же строку. Сначала выберите операции, указав источник, как выше, или используя `ChangeGroupSelection`, созданный на основе проверки UI. Исключите строки из `$plan->conflicts`: выбранный результат всегда не содержит конфликтов, а операция для конфликтной строки содержит наше значение в конфликтующем столбце (см. [Планы слияния](https://merql.inline0.dev/ru/docs/merge/plans#selecting-what-a-target-is-missing)).

## Выборочное применение

Защищенное применение работает с выбранными результатами слияния, созданными на основе плана слияния, включая выбор по группам изменений.

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

Это позволяет принимающему приложению подготовить часть проверенных групп и при этом убедиться, что актуальные строки не изменились после создания плана.

## Предварительный просмотр SQL

Используйте `SqlGenerator::generateGuarded()`, чтобы проверить защищенные инструкции, не выполняя их.

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

Сгенерированные инструкции по-прежнему параметризованы. Предварительные условия выражаются дополнительными условиями `WHERE` для обновлений и удалений и проверками `NOT EXISTS` для вставок.

## Защищенное или обычное применение

`Merql::apply()` выполняет те же операции без предварительных условий. Обновления и удаления в этом случае выполняются независимо от того, менялась ли актуальная строка после создания плана, а вставка уже существующей строки завершается ошибкой дублирования ключа базы данных. Используйте обычное применение для цели, которая, как вы уверены, по-прежнему содержит базовую версию, а защищенное — если другие процессы могли изменить цель после создания снимка.
