본문으로 건너뛰기

3방향 병합

3방향 병합은 공통 조상을 기준으로 두 변경 사항 집합을 비교한 뒤 하나의 결과로 결합합니다. merql은 이 알고리즘을 파일 줄 대신 데이터베이스 행에 적용합니다.

PHP
use Merql\Merge\ThreeWayMerge;

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

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

세 가지 상태

모든 병합에는 정확히 세 개의 스냅샷이 필요합니다.

  • Base -- 두 브랜치가 갈라진 시점의 데이터베이스 상태입니다.
  • Ours -- 변경 사항이 적용된 한쪽 브랜치입니다. 예를 들어 편집한 샌드박스 사본이 이에 해당합니다.
  • Theirs -- 변경 사항이 적용된 다른 쪽 브랜치입니다. 예를 들어 계속 실행 중인 라이브 데이터베이스가 이에 해당합니다.

Ours와 theirs는 각각 해당 브랜치의 상태를 보유한 데이터베이스에서 캡처해야 합니다. 병합 규칙은 두 쪽을 동일하게 취급합니다. 이름은 각 작업의 source, 충돌 유형의 명칭(update_delete은 ours가 업데이트되고 theirs가 삭제되었음을 뜻함), 그리고 어느 쪽을 ConflictPolicy::OursWins 및 TheirsWins 우선할지를 결정합니다.

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

공통 base가 없으면 "추가됨"과 "변경되지 않음"을 구분할 수 없습니다. 양방향 비교에서는 A 쪽에 값이 있고 B 쪽에는 없다는 사실만 알 수 있지만, A가 값을 추가했는지 B가 삭제했는지는 판단할 수 없습니다. 그 질문에 답하는 것이 base입니다.

변경 집합 계산 방식

Differ은 각 스냅샷을 base와 비교해 두 개의 변경 집합을 만듭니다. 각 변경 집합에는 다음 세 가지 유형의 작업이 포함됩니다.

Base 현재 상태 작업
행이 있음 행이 있음, 지문이 동일함 변경 없음
행이 있음 행이 있음, 지문이 다름 UPDATE (열별 차이)
행이 있음 행이 없음 DELETE
행이 없음 행이 있음 INSERT

지문(SHA-256은 정규화된 행 내용의 해시)은 빠른 경로를 제공합니다. 두 지문이 일치하면 행은 변경되지 않은 것이므로 열 단위 비교가 필요하지 않습니다.

PHP
use Merql\Diff\Differ;

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

병합 규칙

그런 다음 두 변경 집합을 작업별로 병합합니다. 이 표는 모든 조합의 결과를 정의합니다.

Ours Theirs 결과
변경 없음 변경 없음 base 유지
변경 없음 업데이트됨 theirs 수락
업데이트됨 변경 없음 ours 수락
업데이트됨 (같은 값) 업데이트됨 (같은 값) 수락 (양쪽 값이 일치함)
업데이트됨 (다른 값) 업데이트됨 (다른 값) 충돌
변경 없음 삭제됨 삭제 수락
삭제됨 변경 없음 삭제 수락
삭제됨 삭제됨 삭제 수락 (양쪽이 일치함)
업데이트됨 삭제됨 충돌
삭제됨 업데이트됨 충돌
삽입됨 (ours만) -- 삽입 수락
-- 삽입됨 (theirs만) 삽입 수락
삽입됨 (같은 PK) 삽입됨 (같은 PK) 충돌

양쪽에서 같은 행을 업데이트해도 merql은 즉시 충돌로 처리하지 않습니다. 대신 열 단위 병합으로 넘어가 각 열을 독립적으로 비교합니다. 자세한 내용은 열 단위 병합을 참조하세요.

파사드 사용하기

Merql 파사드는 디스크에 저장된 이름 있는 스냅샷을 처리합니다. 스냅샷을 만들기 전에 각 상태를 보유한 데이터베이스를 지정해 Merql::init()을 호출하세요.

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

세 스냅샷을 모두 하나의 데이터베이스에서 차례로 만들지 마세요. 그러면 theirs에 ours의 변경 사항도 포함되므로, ours가 삽입한 모든 행이 양쪽에서 삽입된 것으로 나타나 insert_insert 충돌로 보고됩니다.

Merql::merge(), Merql::patch(), Merql::plan()은 ThreeWayMerge을 CellMergeConfig 없이 만들기 때문에 열 단위로만 병합합니다. 셀 단위 병합을 하려면 아래와 같이 스냅샷을 불러온 뒤 클래스를 직접 사용하세요.

클래스를 직접 사용하기

더 세밀하게 제어하려면 구성 요소를 직접 인스턴스화하세요. Snapshotter::capture()은 아무것도 저장하지 않으므로 스냅샷을 메모리에 보관하거나 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'),
);

패치 단축 방법

한쪽에만 변경 사항이 있는 경우(동시 수정이 없는 경우) patch()을 사용하세요. ours가 base와 같으므로 충돌이 발생하지 않는 양방향 병합입니다.

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

파사드를 통한 사용:

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

patch()은 merge($base, $base, $changes)과 같습니다. ours가 base에서 달라진 적이 없으므로 theirs의 모든 변경 사항이 충돌 없이 수락됩니다.

스키마 불일치 감지

스냅샷 사이에 테이블의 열이 추가되거나 제거된 경우에도 병합은 진행되지만, 불일치는 기록됩니다. 변경된 열 유형은 비교하지 않으며, 이름이 바뀐 열은 하나가 제거되고 하나가 추가된 것으로 표시됩니다. 자세한 내용은 스키마 검증을 참조하세요.

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

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

merql은 스키마를 자동으로 마이그레이션하지 않습니다. 스키마 변경은 호출자가 처리해야 합니다. 병합 결과에 해당 변경 사항이 보고되므로 진행 방법을 결정할 수 있습니다.

MergeResult

결과 객체에는 필요한 모든 정보가 들어 있습니다.

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

각 MergeOperation에는 유형(insert, update 또는 delete), 테이블 이름, 행 키, 열 값, 출처(ours, theirs 또는 merged)가 포함됩니다. merged은 양쪽에서 업데이트된 행(열별로 병합됨) 또는 양쪽에서 삭제된 행을 표시합니다.

병합 결과를 적용할 위치

병합 결과는 base를 병합된 상태로 바꾸는 방법을 설명합니다. 작업에는 양쪽의 변경 사항이 포함됩니다. ours가 삽입한 항목과 theirs가 삽입한 항목은 모두 삽입으로 표시됩니다.

  • Merql::apply() (또는 Applier)은 연결된 데이터베이스에 모든 작업을 실행합니다. base 상태를 유지하는 데이터베이스(예: base 상태의 새 사본)에 적용하면 문제없이 실행됩니다. 이미 한쪽의 변경 사항이 포함된 데이터베이스에 적용하면 이미 있는 행을 삽입하려다 실패하고 트랜잭션이 롤백됩니다.
  • 이미 theirs 상태인 데이터베이스에 ours를 반영하려면(예: 샌드박스 수정 사항을 라이브 환경에 반영하는 경우) 해당 데이터베이스에 없는 작업만 적용하세요. 즉, 출처가 ours인 작업과 merged 업데이트를 적용합니다. 병합 계획으로 적용할 작업을 선택하고 Guarded Apply로 라이브 행 사전 조건을 확인해 기록하세요.