
# 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은 즉시 충돌로 처리하지 않습니다. 대신 열 단위 병합으로 넘어가 각 열을 독립적으로 비교합니다. 자세한 내용은 [열 단위 병합](https://merql.inline0.dev/ko/docs/merge/column-level)을 참조하세요.

## 파사드 사용하기

`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의 모든 변경 사항이 충돌 없이 수락됩니다.

## 스키마 불일치 감지

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

```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` 업데이트를 적용합니다. [병합 계획](https://merql.inline0.dev/ko/docs/merge/plans)으로 적용할 작업을 선택하고 [Guarded Apply](https://merql.inline0.dev/ko/docs/apply/guarded)로 라이브 행 사전 조건을 확인해 기록하세요.
