
# Plans de fusion

Un `MergeResult` est le résultat exécutable d'une fusion. Un `MergePlan` en est la forme consultable. Utilisez-le lorsqu'une application doit afficher, préparer, sérialiser ou appliquer de manière sélective des modifications de base de données avant de les écrire dans la base de données cible.

```php
use Merql\Merql;

$plan = Merql::plan(
    id: 'plan-2026-06-06',
    base: 'base',
    ours: 'ours',
    theirs: 'theirs',
    metadata: ['sandbox_id' => 'ow_123'],
);
```

`Merql::plan()` charge les trois instantanés nommés et les fusionne au niveau des colonnes, comme `Merql::merge()`. Pour planifier une fusion qui utilise la fusion au niveau des cellules pour les colonnes TEXT et JSON, construisez vous-même le résultat et transmettez-le à `MergePlanBuilder` :

```php
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Plan\MergePlanBuilder;
use Merql\Snapshot\SnapshotStore;

$base = SnapshotStore::load('base');
$ours = SnapshotStore::load('ours');
$theirs = SnapshotStore::load('theirs');

$result = (new ThreeWayMerge(CellMergeConfig::auto()))->merge($base, $ours, $theirs);
$plan = (new MergePlanBuilder())->build('plan-2026-06-06', $base, $ours, $theirs, $result);
```

Le plan contient :

- les noms des instantanés utilisés pour le construire,
- un résumé des opérations, conflits, incompatibilités de schéma, tables et groupes,
- des identifiants d'opération stables,
- des identifiants de groupe de modifications stables,
- les colonnes d'identité des lignes et leurs valeurs,
- les valeurs des lignes dans la base, de notre côté, de leur côté et dans le résultat pour chaque opération,
- des métadonnées facultatives de l'application hôte,
- un hachage du plan,
- une version du schéma.

## Identifiants stables

Les identifiants d'opération sont déterministes pour un même contenu d'opération. Ils sont dérivés du nom de la table, de la clé de ligne, du type d'opération, de sa source, des colonnes modifiées et des valeurs du résultat.

Les identifiants de groupe de modifications sont déterministes pour un même contenu de groupe. Le générateur par défaut crée un groupe d'opérations par opération. Les applications hôtes peuvent créer des groupes métier plus vastes à partir de la liste d'opérations et les sérialiser avec la même structure DTO.

```php
foreach ($plan->changeGroups as $group) {
    echo $group->id . ': ' . implode(', ', $group->operationIds);
}
```

## Structure d'une opération

Chaque `MergePlanOperation` conserve suffisamment de contexte pour la révision et la restauration.

```php
$operation = $plan->operations[0];

$operation->id;              // stable operation ID
$operation->type;            // insert, update, delete
$operation->table;           // canonical table name
$operation->rowKey;          // encoded identity key
$operation->identityColumns; // columns used for identity
$operation->identityValues;  // decoded identity values
$operation->source;          // ours, theirs, or merged
$operation->baseRow;         // row in base snapshot, or null
$operation->oursRow;         // row in ours snapshot, or null
$operation->theirsRow;       // row in theirs snapshot, or null
$operation->resultValues;    // values the merge would write
$operation->changedColumns;  // changed result columns
```

## Sélections

Utilisez `ChangeGroupSelection` lorsque le UI prépare les groupes. Convertissez-le en `OperationSelection` lors de l'application ou de la génération d'artefacts de restauration.

```php
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

$selection = ChangeGroupSelection::fromIds([
    $plan->changeGroups[0]->id,
]);

$operationSelection = $selection->toOperationSelection($plan);

$selectedResult = (new SelectedMergeResultFactory())->fromOperationSelection(
    $plan,
    $operationSelection,
    SnapshotStore::load($plan->baseSnapshot),
);
```

Le résultat sélectionné est un `MergeResult` sans conflit qui ne contient que les opérations sélectionnées. Il peut être transmis à `Merql::apply()` ou à `Merql::applyGuarded()`.

## Sélection des éléments manquants dans la cible

Un plan contient les opérations des deux côtés, car le résultat de la fusion transforme la base en état fusionné. Lorsque la cible contient déjà un côté, sélectionnez uniquement les opérations qui lui manquent. Pour une cible qui contient leur côté, comme une base de données live recevant les modifications d'une copie bac à sable, il s'agit de toutes les opérations dont la source est `ours`, ainsi que des mises à jour `merged`, qui combinent les modifications de colonnes des deux côtés :

```php
use Merql\Plan\OperationSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

// Rows with an unresolved conflict stay out of the selection.
$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($plan->baseSnapshot),
);
```

Les opérations dont la source est `theirs`, ainsi que les suppressions `merged` (lignes supprimées des deux côtés), sont déjà présentes dans cette cible.

Un plan créé à partir d’une fusion avec des conflits contient toujours une opération pour chaque ligne en conflit modifiée par les deux côtés. Les valeurs du résultat correspondent à celles de notre côté dans la colonne en conflit, et `SelectedMergeResultFactory` renvoie un résultat sans conflit pour toute sélection. Les `conflictState` de l’opération et du groupe de modifications indiquent actuellement `clean` même dans ce cas ([#9](https://github.com/inline0/merql/issues/9)). Vérifiez donc vous-même `$plan->conflicts` comme indiqué ci-dessus, ou résolvez les conflits avec `ConflictResolver` et créez le plan à partir du résultat résolu avec `MergePlanBuilder`. Consultez [Application protégée](https://merql.inline0.dev/fr/docs/apply/guarded) pour écrire la sélection avec des préconditions sur les lignes en temps réel.

## Sérialisation

Les plans de fusion et les sélections ont une sérialisation JSON stable. Enregistrez le plan sérialisé si une étape d’application ultérieure doit utiliser exactement l’ensemble d’opérations examiné.

```php
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\MergePlanSerializer;

$json = MergePlanSerializer::toJson($plan);
$restored = MergePlanSerializer::fromJson($json);

$selectionJson = json_encode($selection->toArray(), JSON_THROW_ON_ERROR);
$selection = ChangeGroupSelection::fromArray(
    json_decode($selectionJson, true, flags: JSON_THROW_ON_ERROR),
);
```

## Instantanés avec alias

Les plans de fusion utilisent des noms de table canoniques. Si les noms physiques des tables diffèrent selon les environnements, capturez-les sous le même nom canonique avant de planifier.

```php
use Merql\Snapshot\SnapshotStore;
use Merql\Snapshot\Snapshotter;

$snapshotter = new Snapshotter($connection);

SnapshotStore::save($snapshotter->captureAliased('live', [
    'wp_posts' => 'wp_posts',
]));

SnapshotStore::save($snapshotter->captureAliased('sandbox', [
    'wp_onumia_abc_posts' => 'wp_posts',
]));
```

Le schéma stocké dans l’instantané utilise le nom canonique de la table tout en préservant les colonnes, clés, colonnes d’identité, clés de ligne et filtres de la table physique.
