
# Application protégée

`GuardedApplier` applique une `MergeResult` propre uniquement si la base de données cible correspond toujours à l'instantané en temps réel attendu. Cette méthode est utile lorsqu'un plan de fusion a été examiné plus tôt et que les lignes en temps réel ont pu changer avant son application.

La configuration habituelle consiste à transférer les modifications d'un environnement de test vers une base de données en production. La base correspond à la production au moment où l'environnement de test en a été copié, notre côté est l'environnement de test modifié, et leur côté est la production dans son état actuel. La cible étant la base de production, l'instantané en temps réel attendu est `theirs`, et les opérations à écrire sont celles qui ne figurent pas encore dans la production (voir [Choisir les opérations](#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()` écrit dans la base de données transmise à `Merql::init()`. L'instantané en temps réel attendu doit décrire cette même base de données : prenez-le depuis la cible et assez récemment pour que les préconditions restent pertinentes.

## Préconditions

L'SQL protégée ajoute des préconditions sur les lignes en temps réel aux instructions générées :

- les mises à jour exigent que l'identité de la ligne cible existe et que les valeurs des colonnes autres que celles d'identité correspondent à la ligne en temps réel attendue,
- les suppressions exigent que l'identité de la ligne cible existe et que les valeurs des colonnes autres que celles d'identité correspondent à la ligne en temps réel attendue,
- les insertions exigent que l'identité de la cible n'existe pas déjà.

Si une instruction ne concerne aucune ligne, merql considère le plan comme obsolète. La transaction est annulée et le `ApplyResult` renvoyé contient l'erreur `Optimistic precondition failed for guarded merge statement.`

Sur MySQL, une mise à jour dont les valeurs correspondent déjà à la ligne en temps réel ne concerne également aucune ligne, car MySQL compte les lignes modifiées plutôt que les lignes correspondantes. Cette mise à jour échoue comme obsolète même si la ligne correspond ([#2](https://github.com/inline0/merql/issues/2)). SQLite compte les lignes correspondantes et l'accepte. Cela peut se produire lors d'une mise à jour `merged`, lorsque les deux côtés ont apporté la même modification à une ligne.

## Choisir les opérations

Un résultat de fusion contient les opérations des deux côtés, puisqu'il transforme la base en état fusionné. Lorsqu'il est appliqué à une cible qui contient déjà leur côté, les opérations provenant de leur côté décrivent des modifications déjà présentes sur la cible et leurs préconditions échouent :

| Opération | État de la cible contenant leur côté | Sélectionner |
|---|---|---|
| `ours` insertion, mise à jour ou suppression | Absente | Oui |
| `merged` mise à jour (les deux côtés ont modifié la ligne) | Contient les colonnes de leur côté, mais pas celles du nôtre | Oui |
| `theirs` insertion | La ligne existe, donc l'insertion protégée ne concerne aucune ligne | Non |
| `merged` suppression (les deux côtés ont supprimé la ligne) | La ligne a disparu, donc la suppression protégée ne concerne aucune ligne | Non |
| `theirs` mise à jour ou suppression | Déjà appliquée | Non |

Transmettre l'intégralité du résultat de `Merql::merge()` à `applyGuarded()` entraîne donc un échec dès que leur côté a inséré une ligne ou que les deux côtés en ont supprimé une. Sélectionnez d'abord les opérations, par source comme ci-dessus ou avec un `ChangeGroupSelection` créé à partir d'une révision UI. Excluez les lignes répertoriées dans `$plan->conflicts` : un résultat sélectionné est toujours propre, et l'opération d'une ligne en conflit porte la valeur de notre côté dans la colonne en conflit (voir [Plans de fusion](https://merql.inline0.dev/fr/docs/merge/plans#selecting-what-a-target-is-missing)).

## Application sélectionnée

L'application protégée fonctionne avec les résultats de fusion sélectionnés issus d'un plan de fusion, y compris les sélections effectuées par groupe de modifications.

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

Une application hôte peut ainsi préparer un sous-ensemble de groupes examinés tout en vérifiant que les lignes en temps réel n'ont pas dérivé depuis la création du plan.

## Aperçu SQL

Utilisez `SqlGenerator::generateGuarded()` pour examiner les instructions protégées sans les exécuter.

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

Les instructions générées restent paramétrées. Les préconditions sont exprimées par des clauses `WHERE` supplémentaires pour les mises à jour et suppressions, et des vérifications `NOT EXISTS` pour les insertions.

## Application protégée ou simple

`Merql::apply()` exécute les mêmes opérations sans préconditions. Les mises à jour et suppressions réussissent alors que la ligne en temps réel ait changé ou non depuis la création du plan, et l'insertion d'une ligne existante échoue avec l'erreur de clé dupliquée de la base de données. Utilisez l'application simple pour une cible dont vous savez qu'elle contient toujours la base, et l'application protégée lorsque d'autres intervenants ont pu modifier la cible depuis la création de son instantané.
