Ga naar de inhoud

Driewegsamenvoeging

Een driewegsamenvoeging vergelijkt twee sets wijzigingen met een gemeenschappelijke voorouder en combineert ze tot één resultaat. merql past dit algoritme toe op databaserijen in plaats van bestandsregels.

PHP
use Merql\Merge\ThreeWayMerge;

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

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

De drie toestanden

Elke samenvoeging vereist precies drie snapshots:

  • Basis -- de databasestatus op het moment dat beide branches uiteenliepen.
  • Onze kant -- één branch na de wijzigingen, bijvoorbeeld een bewerkte sandboxkopie.
  • Hun kant -- de andere branch na de wijzigingen, bijvoorbeeld de live database die bleef draaien.

Onze kant en hun kant moeten elk worden vastgelegd vanuit de database waarin die branch staat. De samenvoegregels behandelen beide kanten gelijk; de namen bepalen de source van elke bewerking, hoe conflicttypen worden benoemd (update_delete betekent dat onze kant heeft bijgewerkt en hun kant heeft verwijderd), en welke kant ConflictPolicy::OursWins en TheirsWins bevoordelen.

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

Zonder een gemeenschappelijke basis kun je geen onderscheid maken tussen ‘toegevoegd’ en ‘ongewijzigd’. Een vergelijking tussen twee kanten ziet dat kant A een waarde heeft en kant B niet, maar kan niet bepalen of A deze heeft toegevoegd of B deze heeft verwijderd. De basis geeft antwoord op die vraag.

Hoe wijzigingssets worden berekend

De Differ vergelijkt elke snapshot met de basis om twee wijzigingssets te maken. Elke wijzigingsset bevat drie soorten bewerkingen:

Basis Huidig Bewerking
Rij bestaat Rij bestaat, dezelfde fingerprint Geen wijziging
Rij bestaat Rij bestaat, andere fingerprint UPDATE (verschil per kolom)
Rij bestaat Rij ontbreekt DELETE
Rij ontbreekt Rij bestaat INSERT

Fingerprints (SHA-256 hashes van genormaliseerde rijinhoud) bieden een snelle route. Als twee fingerprints overeenkomen, is de rij ongewijzigd en is vergelijking op kolomniveau niet nodig.

PHP
use Merql\Diff\Differ;

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

Samenvoegregels

De twee wijzigingssets worden vervolgens bewerking voor bewerking samengevoegd. Deze tabel definieert de uitkomst voor elke combinatie:

Onze kant Hun kant Resultaat
Geen wijziging Geen wijziging Basis behouden
Geen wijziging Bijgewerkt Hun wijziging accepteren
Bijgewerkt Geen wijziging Onze wijziging accepteren
Bijgewerkt (zelfde waarde) Bijgewerkt (zelfde waarde) Accepteren (beide zijn het eens)
Bijgewerkt (andere waarde) Bijgewerkt (andere waarde) Conflict
Geen wijziging Verwijderd Verwijdering accepteren
Verwijderd Geen wijziging Verwijdering accepteren
Verwijderd Verwijderd Verwijdering accepteren (beide zijn het eens)
Bijgewerkt Verwijderd Conflict
Verwijderd Bijgewerkt Conflict
Alleen door ons ingevoegd -- Invoeging accepteren
-- Alleen door hen ingevoegd Invoeging accepteren
Ingevoegd (zelfde PK) Ingevoegd (zelfde PK) Conflict

Wanneer beide kanten dezelfde rij bijwerken, verklaart merql niet direct een conflict. Het gaat verder met samenvoegen op kolomniveau en vergelijkt elke kolom afzonderlijk. Zie Samenvoegen op kolomniveau voor meer informatie.

De facade gebruiken

De Merql-facade werkt met benoemde snapshots die op schijf zijn opgeslagen. Roep Merql::init() aan met de database waarin elke staat zich bevindt voordat je er een snapshot van maakt:

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

Maak niet achtereenvolgens alle drie de snapshots van één database. Hun snapshot zou dan ook onze wijzigingen bevatten, waardoor elke rij die wij hebben ingevoegd aan beide kanten als ingevoegd zou verschijnen en als een conflict van het type insert_insert zou worden gemeld.

Merql::merge(), Merql::patch() en Merql::plan() maken een ThreeWayMerge zonder CellMergeConfig, waardoor ze alleen op kolomniveau worden samengevoegd. Laad voor samenvoegen op celniveau de snapshots en gebruik rechtstreeks de klasse, zoals hieronder.

De klassen rechtstreeks gebruiken

Voor meer controle kun je de componenten zelf instantiëren. Snapshotter::capture() slaat niets op, dus bewaar de snapshots in het geheugen of sla ze op met 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'),
);

De patch-snelkoppeling

Wanneer slechts één kant wijzigingen heeft (geen gelijktijdige aanpassingen), gebruik je patch(). Dit is een tweezijdige samenvoeging waarbij onze kant gelijk is aan de basis, dus conflicten zijn onmogelijk:

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

Via de facade:

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

patch() is gelijkwaardig aan merge($base, $base, $changes). Omdat onze kant nooit van de basis is afgeweken, wordt elke wijziging van hun kant zonder conflict geaccepteerd.

Schemamismatches detecteren

Als er tussen snapshots kolommen aan een tabel zijn toegevoegd of eruit zijn verwijderd, gaat de samenvoeging door maar worden de mismatches vastgelegd. Gewijzigde kolomtypen worden niet vergeleken, en een hernoemde kolom verschijnt als één verwijderde en één toegevoegde kolom; zie Schemavalidatie.

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

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

merql migreert schema's niet automatisch. Schemawijzigingen zijn de verantwoordelijkheid van de aanroeper. De samenvoeging meldt ze, zodat je kunt bepalen hoe je verdergaat.

Het MergeResult

Het resultaatobject bevat alles wat je nodig hebt:

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

Elke MergeOperation heeft een type (insert, update of delete), een tabelnaam, een rijsleutel, kolomwaarden en een bron (ours, theirs of merged). merged markeert een rij die beide kanten hebben bijgewerkt (gecombineerd per kolom) of die beide kanten hebben verwijderd.

Waar een samenvoegresultaat kan worden toegepast

Een samenvoegresultaat beschrijft hoe je de basis omzet naar de samengevoegde staat. De bewerkingen bevatten wijzigingen van beide kanten: een invoeging van onze kant en een invoeging van hun kant verschijnen beide als invoegingen.

  • Merql::apply() (of Applier) voert elke bewerking uit op de verbonden database. Het kan zonder problemen worden toegepast op een database die nog de basis bevat, zoals een verse kopie van de basisstaat. Op een database die al de wijzigingen van één kant bevat, mislukt een invoeging van een bestaande rij en wordt de transactie teruggedraaid.
  • Om onze wijzigingen toe te passen op een database die hun wijzigingen al bevat, zoals wanneer sandboxwijzigingen naar live worden gepromoveerd, pas je alleen de bewerkingen toe die de database nog mist: die met bron ours en merged-updates. Gebruik een samenvoegplan om ze te selecteren en Beveiligd toepassen om ze te schrijven met voorwaarden voor live rijen.