Aller au contenu

Gestion des conflits

Un conflit survient lorsque les deux côtés d'une fusion modifient les mêmes données de manière incompatible. merql détecte quatre types de conflits et propose des stratégies de résolution automatiques et manuelles.

PHP
$result = $merge->merge($base, $ours, $theirs);

if (!$result->isClean()) {
    foreach ($result->conflicts() as $conflict) {
        echo "{$conflict->table()}.{$conflict->column()}: "
           . "{$conflict->oursValue()} vs {$conflict->theirsValue()}\n";
    }
}

Types de conflit

update_update

Les deux côtés ont modifié la même colonne de la même ligne avec des valeurs différentes. C'est le type de conflit le plus courant.

Code
Base:    posts #42  title = "Hello"
Ours:    posts #42  title = "Welcome"
Theirs:  posts #42  title = "Greetings"

Le conflit indique : la table posts, la clé de ligne 42, la colonne title, notre valeur Welcome, leur valeur Greetings et la valeur de base Hello.

update_delete

Un côté a mis à jour une ligne et l'autre l'a supprimée. merql ne peut pas déterminer si la mise à jour ou la suppression doit l'emporter.

Code
Ours:    posts #42  title = "Updated Title"    (updated)
Theirs:  posts #42  (deleted)

Le conflit indique : la table posts, la clé de ligne 42 et le type update_delete. Notre valeur contient la ligne entière après mise à jour. Leur valeur est null.

delete_update

Le cas inverse de update_delete : ils ont mis à jour la ligne et nous l'avons supprimée.

Code
Ours:    posts #42  (deleted)
Theirs:  posts #42  title = "Updated Title"    (updated)

Le conflit indique : type delete_update. Notre valeur est null ; leur valeur contient la ligne entière après mise à jour.

insert_insert

Les deux côtés ont inséré une ligne avec la même clé primaire. Comme les deux lignes sont nouvelles (absentes de la base), merql ne peut pas les fusionner.

Code
Base:    (no row #99)
Ours:    posts #99  { title: "My Post" }
Theirs:  posts #99  { title: "Their Post" }

Le conflit indique : type insert_insert. Notre valeur contient les données de notre ligne ; leur valeur contient les données de leur ligne.

La classe Conflict

Chaque conflit est un objet en lecture seule avec les accesseurs suivants :

PHP
$conflict->table();      // string: table name
$conflict->rowKey();     // string: encoded row identity key
$conflict->type();       // string: "update_update"|"update_delete"|"delete_update"|"insert_insert"
$conflict->column();     // ?string: column name (null for structural conflicts)
$conflict->oursValue();  // mixed: our value (null for delete side, full row for structural)
$conflict->theirsValue(); // mixed: their value (null for delete side, full row for structural)
$conflict->baseValue();  // mixed: base value (null for inserts)

La méthode column() renvoie un nom de colonne pour les conflits update_update et null pour les conflits structurels (update_delete, delete_update, insert_insert).

Décoder la clé primaire

La clé de ligne est une chaîne encodée. Pour obtenir un tableau de clés primaires nommées, transmettez les colonnes d'identité :

PHP
$pk = $conflict->primaryKey(['id']);
// ['id' => '42']

$pk = $conflict->primaryKey(['tenant_id', 'user_id']);
// ['tenant_id' => '1', 'user_id' => '99']

Résolution automatique avec ConflictPolicy

ConflictResolver applique une politique pour résoudre tous les conflits en une seule fois :

PHP
use Merql\Merge\ConflictPolicy;
use Merql\Merge\ConflictResolver;

// One side wins everywhere.
$resolved = ConflictResolver::resolve($result, ConflictPolicy::OursWins);
$resolved = ConflictResolver::resolve($result, ConflictPolicy::TheirsWins);

// Manual: return the result unchanged (inspect conflicts yourself).
$resolved = ConflictResolver::resolve($result, ConflictPolicy::Manual);

Énumération ConflictPolicy

Valeur Comportement
OursWins Utiliser notre valeur pour update_update. Conserver la mise à jour pour update_delete. Conserver la suppression pour delete_update. Utiliser notre insertion pour insert_insert.
TheirsWins Utiliser leur valeur pour update_update. Supprimer pour update_delete. Conserver la mise à jour pour delete_update. Utiliser leur insertion pour insert_insert.
Manual Renvoyer le résultat sans le modifier. Tous les conflits restent non résolus.

Fonctionnement de la résolution

Pour les conflits update_update, le résolveur modifie l'opération de fusion existante pour cette ligne, en remplaçant la valeur de la colonne en conflit par celle du côté gagnant.

Pour les conflits structurels (update_delete, delete_update, insert_insert), le résolveur ajoute une nouvelle opération au résultat :

  • update_delete avec OursWins ajoute une opération UPDATE.
  • update_delete avec TheirsWins ajoute une opération DELETE.
  • delete_update avec OursWins ajoute une opération DELETE.
  • delete_update avec TheirsWins ajoute une opération UPDATE.
  • insert_insert utilise les valeurs de ligne du côté gagnant.

Le MergeResult résolu comporte une liste de conflits vide et peut être appliqué directement.

Résolution manuelle

Pour un contrôle précis, examinez chaque conflit et construisez votre propre résolution :

PHP
foreach ($result->conflicts() as $conflict) {
    echo "Table:  {$conflict->table()}\n";
    echo "Row:    {$conflict->rowKey()}\n";
    echo "Type:   {$conflict->type()}\n";

    if ($conflict->column() !== null) {
        echo "Column: {$conflict->column()}\n";
        echo "Base:   " . var_export($conflict->baseValue(), true) . "\n";
        echo "Ours:   " . var_export($conflict->oursValue(), true) . "\n";
        echo "Theirs: " . var_export($conflict->theirsValue(), true) . "\n";
    }
}

Application avec des conflits non résolus

Applier refuse d'appliquer un résultat de fusion qui contient des conflits non résolus. Il lance une exception ConflictException :

PHP
use Merql\Exceptions\ConflictException;

try {
    $applier->apply($result);
} catch (ConflictException $e) {
    echo $e->getMessage();
    // "Cannot apply merge with 3 unresolved conflicts"
}

Résolvez toujours les conflits avant d'appliquer le résultat, avec ConflictResolver::resolve() ou en construisant manuellement un MergeResult sans conflit.