Scalanie trójstronne
Scalanie trójstronne porównuje dwa zestawy zmian ze wspólnym przodkiem i łączy je w jeden wynik. merql stosuje ten algorytm do wierszy bazy danych zamiast wierszy plików.
use Merql\Merge\ThreeWayMerge;
$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);
echo $result->operationCount(); // resolved operations
echo $result->conflictCount(); // unresolved conflicts
Trzy stany
Każde scalanie wymaga dokładnie trzech zrzutów:
- Baza -- stan bazy danych w chwili rozgałęzienia obu gałęzi.
- Nasza strona -- jedna z gałęzi po wprowadzeniu zmian, na przykład edytowana kopia piaskownicy.
- Druga strona -- druga gałąź po wprowadzeniu zmian, na przykład działająca baza danych, która nadal była używana.
Naszą stronę i drugą stronę należy zarejestrować każdą z bazy danych zawierającej daną gałąź. Reguły scalania traktują obie strony jednakowo; nazwy określają źródło każdej operacji source, sposób nazywania typów konfliktów (update_delete oznacza, że nasza strona zaktualizowała, a druga usunęła wiersz) oraz to, której stronie sprzyjają ConflictPolicy::OursWins i TheirsWins.
Base (snapshot at fork time)
/ \
Ours (our changes) Theirs (their changes)
\ /
--------- MERGE ----------
|
Merged result
+ conflicts (if any)
Bez wspólnej bazy nie da się odróżnić elementu „dodano” od „bez zmian”. Porównanie dwóch stron pokazuje, że strona A ma wartość, a strona B jej nie ma, ale nie pozwala ustalić, czy strona A ją dodała, czy strona B usunęła. Odpowiedź daje baza.
Jak obliczane są zestawy zmian
Differ porównuje każdy zrzut z bazą, aby utworzyć dwa zestawy zmian. Każdy zestaw obejmuje trzy rodzaje operacji:
| Baza | Bieżący stan | Operacja |
|---|---|---|
| Wiersz istnieje | Wiersz istnieje, ten sam odcisk | Bez zmian |
| Wiersz istnieje | Wiersz istnieje, inny odcisk | UPDATE (różnice w poszczególnych kolumnach) |
| Wiersz istnieje | Brak wiersza | DELETE |
| Brak wiersza | Wiersz istnieje | INSERT |
Odciski (SHA-256 skróty znormalizowanej zawartości wiersza) zapewniają szybką ścieżkę. Jeśli dwa odciski są identyczne, wiersz pozostaje bez zmian i nie trzeba porównywać poszczególnych kolumn.
use Merql\Diff\Differ;
$differ = new Differ();
$oursChangeset = $differ->diff($base, $ours);
$theirsChangeset = $differ->diff($base, $theirs);
Reguły scalania
Następnie oba zestawy zmian są scalane operacja po operacji. Ta tabela określa wynik dla każdej kombinacji:
| Nasze | Ich | Wynik |
|---|---|---|
| Bez zmian | Bez zmian | Zachowaj bazę |
| Bez zmian | Zaktualizowano | Przyjmij ich zmiany |
| Zaktualizowano | Bez zmian | Przyjmij nasze zmiany |
| Zaktualizowano (ta sama wartość) | Zaktualizowano (ta sama wartość) | Przyjmij (obie strony są zgodne) |
| Zaktualizowano (inna wartość) | Zaktualizowano (inna wartość) | Konflikt |
| Bez zmian | Usunięto | Przyjmij usunięcie |
| Usunięto | Bez zmian | Przyjmij usunięcie |
| Usunięto | Usunięto | Przyjmij usunięcie (obie strony są zgodne) |
| Zaktualizowano | Usunięto | Konflikt |
| Usunięto | Zaktualizowano | Konflikt |
| Wstawiono (tylko po naszej stronie) | -- | Przyjmij wstawienie |
| -- | Wstawiono (tylko po ich stronie) | Przyjmij wstawienie |
| Wstawiono (ten sam PK) | Wstawiono (ten sam PK) | Konflikt |
Gdy obie strony aktualizują ten sam wiersz, merql nie zgłasza od razu konfliktu. Przechodzi do scalania na poziomie kolumn i porównuje każdą kolumnę osobno. Szczegóły znajdziesz w sekcji Scalanie na poziomie kolumn.
Korzystanie z fasady
Fasada Merql działa na nazwanych migawkach zapisanych na dysku. Przed utworzeniem migawki wywołaj Merql::init(), przekazując bazę danych zawierającą dany stan:
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');
Nie twórz po kolei wszystkich trzech migawek z jednej bazy danych. Migawka ich zmian zawierałaby wtedy również nasze zmiany, więc każdy wiersz dodany przez nas pojawiłby się jako dodany po obu stronach i zostałby zgłoszony jako insert_insert konflikt.
Merql::merge(), Merql::patch() i Merql::plan() tworzą ThreeWayMerge bez CellMergeConfig, więc scalają wyłącznie na poziomie kolumn. Aby scalać na poziomie komórek, wczytaj migawki i użyj bezpośrednio klasy, jak poniżej.
Bezpośrednie użycie klas
Aby uzyskać większą kontrolę, utwórz instancje komponentów samodzielnie. Snapshotter::capture() niczego nie zapisuje, więc przechowuj migawki w pamięci lub zapisz je za pomocą SnapshotStore::save():
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'),
);
Skrót patch
Gdy zmiany ma tylko jedna strona (brak równoczesnych modyfikacji), użyj patch(). To scalanie dwukierunkowe, w którym nasze zmiany są takie same jak baza, więc konflikty nie są możliwe:
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.
Za pośrednictwem fasady:
$result = Merql::patch('base', 'changes');
patch() jest równoważne merge($base, $base, $changes). Ponieważ nasze zmiany nie odbiegły od bazy, wszystkie zmiany po ich stronie są przyjmowane bez konfliktu.
Wykrywanie niezgodności schematu
Jeśli między migawkami do tabeli dodano lub z niej usunięto kolumny, scalanie nadal przebiega, ale odnotowuje niezgodności. Zmienione typy kolumn nie są porównywane, a zmieniona nazwa kolumny jest wykazywana jako jedna usunięta i jedna dodana; zobacz sekcję Walidacja schematu.
$result = $merge->merge($base, $ours, $theirs);
if ($result->hasSchemaMismatches()) {
foreach ($result->schemaMismatches() as $mismatch) {
echo $mismatch->getMessage() . "\n";
}
}
merql nie przeprowadza automatycznej migracji schematów. Za zmiany schematu odpowiada wywołujący. Wynik scalania je zgłasza, aby można było zdecydować, co dalej.
Obiekt MergeResult
Obiekt wyniku zawiera wszystko, czego potrzebujesz:
$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żdy MergeOperation ma typ (insert, update lub delete), nazwę tabeli, klucz wiersza, wartości kolumn i źródło (ours, theirs lub merged). merged oznacza wiersz zaktualizowany po obu stronach (połączony kolumna po kolumnie) lub usunięty po obu stronach.
Zastosowanie wyniku scalania
Wynik scalania opisuje, jak przekształcić bazę w stan po scaleniu. Jego operacje obejmują zmiany z obu stron: wstawienie wykonane przez nas i wstawienie wykonane przez nich są uwzględniane jako wstawienia.
Merql::apply()(lubApplier) wykonuje każdą operację na połączonej bazie danych. Działa poprawnie na bazie, która nadal zawiera stan bazowy, na przykład na świeżej kopii bazy. W przypadku bazy zawierającej już zmiany jednej ze stron wstawienie istniejącego wiersza kończy się niepowodzeniem i powoduje wycofanie transakcji.- Aby przenieść nasze zmiany do bazy zawierającej już ich zmiany, na przykład promując zmiany z piaskownicy do środowiska produkcyjnego, zastosuj tylko operacje, których brakuje w tej bazie: te ze źródłem
oursoraz aktualizacjemerged. Użyj planu scalania, aby je wybrać, i Zastosowania z ochroną, aby zapisać je z warunkami wstępnymi dotyczącymi bieżących wierszy.