Přejít k obsahu

Třícestné sloučení

Třícestné sloučení porovnává dvě sady změn vůči společnému předkovi a kombinuje je do jednoho výsledku. merql používá tento algoritmus na databázové řádky místo řádků souborů.

PHP
use Merql\Merge\ThreeWayMerge;

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

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

Tři stavy

Každé sloučení vyžaduje přesně tři snímky:

  • Základ -- stav databáze v okamžiku, kdy se obě větve rozdělily.
  • Naše -- jedna větev po provedení změn, například upravená sandboxová kopie.
  • Jejich -- druhá větev po provedení změn, například živá databáze, která dál fungovala.

Stavy naší a jejich strany musí být zaznamenány z databáze, která danou větev obsahuje. Pravidla sloučení zacházejí s oběma stranami stejně; názvy určují source každé operace, názvy typů konfliktů (update_delete znamená, že naše strana řádek aktualizovala a jejich jej odstranila) a to, kterou stranu upřednostňují ConflictPolicy::OursWins a TheirsWins.

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

Bez společného základu nelze rozlišit „přidáno“ od „beze změny“. Dvoucestné porovnání ukáže, že strana A má hodnotu a strana B ji nemá, ale nedokáže určit, zda ji A přidala, nebo ji B odstranila. Odpověď poskytne základ.

Jak se počítají sady změn

Differ porovná každý snímek se základem a vytvoří dvě sady změn. Každá sada obsahuje tři druhy operací:

Základ Aktuální stav Operace
Řádek existuje Řádek existuje, stejný otisk Beze změny
Řádek existuje Řádek existuje, jiný otisk UPDATE (rozdíl po sloupcích)
Řádek existuje Řádek chybí DELETE
Řádek chybí Řádek existuje INSERT

Otisky (hash hodnot SHA-256 normalizovaného obsahu řádku) představují rychlou cestu. Pokud se dva otisky shodují, řádek se nezměnil a není třeba porovnávat jednotlivé sloupce.

PHP
use Merql\Diff\Differ;

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

Pravidla sloučení

Obě sady změn se poté sloučí operaci po operaci. Tato tabulka uvádí výsledek každé kombinace:

Naše Jejich Výsledek
Beze změny Beze změny Ponechat základ
Beze změny Aktualizováno Přijmout jejich
Aktualizováno Beze změny Přijmout naši verzi
Aktualizováno (stejná hodnota) Aktualizováno (stejná hodnota) Přijmout (obě strany souhlasí)
Aktualizováno (jiná hodnota) Aktualizováno (jiná hodnota) Konflikt
Beze změny Odstraněno Přijmout odstranění
Odstraněno Beze změny Přijmout odstranění
Odstraněno Odstraněno Přijmout odstranění (obě strany souhlasí)
Aktualizováno Odstraněno Konflikt
Odstraněno Aktualizováno Konflikt
Vloženo (pouze naše strana) -- Přijmout vložení
-- Vloženo (pouze jejich strana) Přijmout vložení
Vloženo (stejný PK) Vloženo (stejný PK) Konflikt

Když obě strany aktualizují stejný řádek, merql ihned nevyhlásí konflikt. Přejde ke sloučení na úrovni sloupců a porovná každý sloupec samostatně. Podrobnosti najdete v části Sloučení na úrovni sloupců.

Použití fasády

Fasáda Merql pracuje s pojmenovanými snímky uloženými na disk. Před pořízením snímku zavolejte Merql::init() nad databází, která obsahuje příslušný stav:

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

Nezaznamenávejte všechny tři snímky postupně z jedné databáze. Jejich snímek by pak obsahoval také změny naší strany, takže každý námi vložený řádek by vypadal jako vložený na obou stranách a byl by nahlášen jako konflikt insert_insert.

Merql::merge(), Merql::patch() a Merql::plan() vytvoří ThreeWayMerge bez CellMergeConfig, takže sloučí pouze na úrovni sloupců. Chcete-li sloučit na úrovni buněk, načtěte snímky a použijte přímo třídu, jak je uvedeno níže.

Přímé použití tříd

Pro větší kontrolu vytvořte instance komponent sami. Snapshotter::capture() nic neukládá, proto uchovávejte snímky v paměti nebo je ukládejte pomocí 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'),
);

Zkratka pro záplatu

Pokud změny provedla jen jedna strana (nedošlo k souběžným úpravám), použijte patch(). Jde o dvoucestné sloučení, při kterém se naše strana rovná základu, takže ke konfliktům nemůže dojít:

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

Přes fasádu:

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

patch() odpovídá merge($base, $base, $changes). Protože se naše strana od základu nikdy neodchýlila, všechny změny z jejich strany se přijmou bez konfliktu.

Detekce nesouladu schématu

Pokud byly mezi snímky do tabulky přidány nebo z ní odebrány sloupce, sloučení přesto pokračuje a zaznamená nesoulady. Změněné typy sloupců se neporovnávají a přejmenovaný sloupec se projeví jako jeden odebraný a jeden přidaný; viz Ověření schématu.

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

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

merql schémata automaticky nemigruje. Za změny schématu odpovídá volající. Sloučení je ohlásí, abyste se mohli rozhodnout, jak postupovat.

MergeResult

Výsledný objekt obsahuje vše potřebné:

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

Každý MergeOperation má typ (insert, update nebo delete), název tabulky, klíč řádku, hodnoty sloupců a zdroj (ours, theirs nebo merged). merged označuje řádek, který obě strany aktualizovaly (sloupec po sloupci sloučený), nebo který obě strany odstranily.

K čemu se výsledek sloučení hodí

Výsledek sloučení popisuje, jak převést základní stav na sloučený. Jeho operace zahrnují změny obou stran: vložení provedené naší stranou i vložení provedené jejich stranou se projeví jako vložení.

  • Merql::apply() (nebo Applier) provede každou operaci nad připojenou databází. Lze jej bez problémů použít v databázi, která stále obsahuje základní stav, například v čerstvé kopii tohoto stavu. Pokud databáze už obsahuje změny jedné strany, vložení existujícího řádku selže a transakce se vrátí zpět.
  • Chcete-li přenést naše změny do databáze, která už obsahuje jejich změny, například při nasazování úprav ze sandboxu do živého prostředí, použijte jen operace, které v databázi chybí: ty se zdrojem ours a aktualizace merged. Vyberte je pomocí plánu sloučení a zapište pomocí chráněného použití s podmínkami pro živé řádky.