Aller au contenu

Fusion à trois voies

Une fusion à trois voies compare deux ensembles de modifications par rapport à un ancêtre commun et les combine en un seul résultat. merql applique cet algorithme aux lignes de base de données plutôt qu’aux lignes de fichiers.

PHP
use Merql\Merge\ThreeWayMerge;

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

echo $result->operationCount(); // resolved operations
echo $result->conflictCount();  // unresolved conflicts

Les trois états

Chaque fusion nécessite exactement trois instantanés :

  • Base -- l’état de la base de données au moment où les deux branches ont divergé.
  • Notre côté -- une branche après ses modifications, par exemple une copie de test qui a été modifiée.
  • Leur côté -- l’autre branche après ses modifications, par exemple la base de données en production qui a continué à fonctionner.

Notre côté et leur côté doivent chacun être capturés depuis la base de données qui contient la branche correspondante. Les règles de fusion traitent les deux côtés de la même façon ; les noms déterminent le source de chaque opération, la désignation des types de conflit (update_delete signifie que notre côté a modifié la ligne et que leur côté l’a supprimée), ainsi que le côté privilégié par ConflictPolicy::OursWins et TheirsWins.

Code
         Base (snapshot at fork time)
        /                              \
   Ours (our changes)                   Theirs (their changes)
        \                              /
         --------- MERGE ----------
                     |
              Merged result
              + conflicts (if any)

Sans base commune, impossible de distinguer « ajouté » de « inchangé ». Une comparaison à deux voies constate que le côté A contient une valeur et que le côté B ne la contient pas, mais ne peut pas déterminer si A l’a ajoutée ou si B l’a supprimée. La base permet de trancher.

Calcul des ensembles de modifications

Le Differ compare chaque instantané à la base pour produire deux ensembles de modifications. Chaque ensemble contient trois types d’opérations :

Base Actuel Opération
La ligne existe La ligne existe, même empreinte Aucune modification
La ligne existe La ligne existe, empreinte différente UPDATE (différence par colonne)
La ligne existe Ligne absente DELETE
Ligne absente La ligne existe INSERT

Les empreintes (SHA-256 hachages du contenu normalisé des lignes) offrent un raccourci. Si deux empreintes correspondent, la ligne est inchangée et aucune comparaison au niveau des colonnes n’est nécessaire.

PHP
use Merql\Diff\Differ;

$differ = new Differ();
$oursChangeset = $differ->diff($base, $ours);
$theirsChangeset = $differ->diff($base, $theirs);

Règles de fusion

Les deux ensembles de modifications sont ensuite fusionnés opération par opération. Ce tableau définit le résultat pour chaque combinaison :

Notre côté Leur côté Résultat
Aucune modification Aucune modification Conserver la base
Aucune modification Modifié Accepter leur côté
Modifié Aucune modification Accepter notre côté
Modifié (même valeur) Modifié (même valeur) Accepter (accord des deux côtés)
Modifié (valeur différente) Modifié (valeur différente) Conflit
Aucune modification Supprimé Accepter la suppression
Supprimé Aucune modification Accepter la suppression
Supprimé Supprimé Accepter la suppression (accord des deux côtés)
Modifié Supprimé Conflit
Supprimé Modifié Conflit
Inséré (notre côté uniquement) -- Accepter l’insertion
-- Inséré (leur côté uniquement) Accepter l’insertion
Inséré (même clé primaire) Inséré (même clé primaire) Conflit

Lorsque les deux côtés modifient la même ligne, merql ne déclare pas immédiatement un conflit. Il passe à une fusion au niveau des colonnes et compare chaque colonne séparément. Consultez Fusion au niveau des colonnes pour en savoir plus.

Utilisation de la façade

La façade Merql fonctionne avec des instantanés nommés enregistrés sur le disque. Appelez Merql::init() avec la base de données qui contient chaque état avant de créer son instantané :

PHP
use Merql\Connection;
use Merql\Merql;

$live    = Connection::sqlite('/data/live.sqlite');
$sandbox = Connection::sqlite('/data/sandbox.sqlite');

Merql::init($live);
Merql::snapshot('base');      // before the sandbox is copied from live

// ... copy live.sqlite to sandbox.sqlite, then both databases change ...

Merql::init($sandbox);
Merql::snapshot('ours');
Merql::init($live);
Merql::snapshot('theirs');

$result = Merql::merge('base', 'ours', 'theirs');

Ne créez pas les trois instantanés depuis une seule base de données, l’un après l’autre. Leur côté contiendrait alors aussi les modifications de notre côté : chaque ligne insérée par notre côté semblerait avoir été insérée des deux côtés et serait signalée comme un conflit insert_insert.

Merql::merge(), Merql::patch() et Merql::plan() créent un ThreeWayMerge sans CellMergeConfig ; la fusion s’effectue donc uniquement au niveau des colonnes. Pour une fusion au niveau des cellules, chargez les instantanés et utilisez directement la classe, comme ci-dessous.

Utilisation directe des classes

Pour plus de contrôle, instanciez vous-même les composants. Snapshotter::capture() n’enregistre rien ; gardez donc les instantanés en mémoire ou enregistrez-les avec SnapshotStore::save() :

PHP
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Snapshot\Snapshotter;
use Merql\Snapshot\SnapshotStore;

$base   = (new Snapshotter($live))->capture('base');
// ... copy live to the sandbox, then both databases change ...
$ours   = (new Snapshotter($sandbox))->capture('ours');
$theirs = (new Snapshotter($live))->capture('theirs');

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

// Snapshots saved earlier, merged with cell-level merge for TEXT and JSON columns.
$merge = new ThreeWayMerge(CellMergeConfig::auto());
$result = $merge->merge(
    SnapshotStore::load('base'),
    SnapshotStore::load('ours'),
    SnapshotStore::load('theirs'),
);

Raccourci pour les correctifs

Lorsque les modifications proviennent d’un seul côté (sans modifications concurrentes), utilisez patch(). Il s’agit d’une fusion à deux voies où notre côté est identique à la base ; les conflits sont donc impossibles :

PHP
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.

Via la façade :

PHP
$result = Merql::patch('base', 'changes');

patch() est équivalent à merge($base, $base, $changes). Comme notre côté n’a jamais divergé de la base, toutes les modifications de leur côté sont acceptées sans conflit.

Détection des incompatibilités de schéma

Si des colonnes ont été ajoutées à une table ou supprimées de celle-ci entre les instantanés, la fusion se poursuit, mais signale les incompatibilités. Les types de colonnes modifiés ne sont pas comparés, et une colonne renommée apparaît comme une colonne supprimée et une colonne ajoutée ; consultez Validation du schéma.

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

if ($result->hasSchemaMismatches()) {
    foreach ($result->schemaMismatches() as $mismatch) {
        echo $mismatch->getMessage() . "\n";
    }
}

merql ne migre pas automatiquement les schémas. Les modifications de schéma relèvent de la responsabilité de l’appelant. La fusion les signale afin que vous puissiez décider de la marche à suivre.

L’objet MergeResult

L’objet résultat contient tout ce dont vous avez besoin :

PHP
$result->isClean();            // true if no conflicts
$result->operations();         // list of MergeOperation objects
$result->conflicts();          // list of Conflict objects
$result->operationCount();     // number of resolved operations
$result->conflictCount();      // number of unresolved conflicts
$result->hasSchemaMismatches(); // true if schemas differ
$result->baseSnapshot();       // the base snapshot used for the merge

Chaque MergeOperation possède un type (insert, update ou delete), un nom de table, une clé de ligne, les valeurs des colonnes et une source (ours, theirs ou merged). merged marque une ligne modifiée des deux côtés (combinée colonne par colonne) ou supprimée des deux côtés.

Où appliquer un résultat de fusion

Un résultat de fusion décrit comment transformer la base en état fusionné. Ses opérations regroupent les modifications des deux côtés : une insertion effectuée par notre côté et une insertion effectuée par leur côté apparaissent toutes deux comme des insertions.

  • Merql::apply() (ou Applier) exécute chaque opération sur la base de données connectée. L’application fonctionne sans problème sur une base qui contient encore la base d’origine, par exemple une copie fraîche de cet état. Sur une base qui contient déjà les modifications d’un côté, l’insertion d’une ligne déjà présente échoue et la transaction est annulée.
  • Pour intégrer les modifications de notre côté dans une base qui contient déjà celles de leur côté, par exemple pour promouvoir des modifications de test en production, appliquez uniquement les opérations manquantes à cette base : celles dont la source est ours, ainsi que les mises à jour merged. Utilisez un plan de fusion pour les sélectionner et l’application protégée pour les écrire avec des préconditions sur les lignes en temps réel.