
# 병합 계획

`MergeResult`은 병합의 실행 가능한 출력입니다. `MergePlan`은 해당 출력을 검토할 수 있는 형태입니다. 애플리케이션이 대상 데이터베이스에 쓰기 전에 데이터베이스 변경 사항을 표시하거나, 준비하거나, 직렬화하거나, 선택적으로 적용해야 할 때 사용하세요.

```php
use Merql\Merql;

$plan = Merql::plan(
    id: 'plan-2026-06-06',
    base: 'base',
    ours: 'ours',
    theirs: 'theirs',
    metadata: ['sandbox_id' => 'ow_123'],
);
```

`Merql::plan()`은 이름이 지정된 스냅샷 세 개를 불러와 `Merql::merge()`처럼 열 단위로 병합합니다. TEXT 및 JSON 열에 셀 단위 병합을 사용하는 계획을 세우려면 직접 결과를 구성해 `MergePlanBuilder`에 전달하세요.

```php
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Plan\MergePlanBuilder;
use Merql\Snapshot\SnapshotStore;

$base = SnapshotStore::load('base');
$ours = SnapshotStore::load('ours');
$theirs = SnapshotStore::load('theirs');

$result = (new ThreeWayMerge(CellMergeConfig::auto()))->merge($base, $ours, $theirs);
$plan = (new MergePlanBuilder())->build('plan-2026-06-06', $base, $ours, $theirs, $result);
```

계획에는 다음이 포함됩니다.

- 계획 구성에 사용된 스냅샷 이름,
- 작업, 충돌, 스키마 불일치, 테이블 및 그룹의 요약,
- 안정적인 작업 ID,
- 안정적인 변경 그룹 ID,
- 행 식별 열 및 식별 값,
- 각 작업의 base, ours, theirs 및 결과 행 값,
- 선택적 호스트 애플리케이션 메타데이터,
- 계획 해시,
- 스키마 버전.

## 안정적인 ID

작업 ID는 작업 내용이 같으면 결정적으로 생성됩니다. 테이블 이름, 행 키, 작업 유형, 작업 출처, 변경된 열 및 결과 값에서 파생됩니다.

변경 그룹 ID는 그룹 내용이 같으면 결정적으로 생성됩니다. 기본 빌더는 작업마다 작업 그룹 하나를 생성합니다. 호스트 애플리케이션은 작업 목록에서 더 큰 도메인별 그룹을 구성한 후에도 같은 DTO 형태로 직렬화할 수 있습니다.

```php
foreach ($plan->changeGroups as $group) {
    echo $group->id . ': ' . implode(', ', $group->operationIds);
}
```

## 작업 형태

각 `MergePlanOperation`에는 검토와 롤백에 필요한 맥락이 충분히 담겨 있습니다.

```php
$operation = $plan->operations[0];

$operation->id;              // stable operation ID
$operation->type;            // insert, update, delete
$operation->table;           // canonical table name
$operation->rowKey;          // encoded identity key
$operation->identityColumns; // columns used for identity
$operation->identityValues;  // decoded identity values
$operation->source;          // ours, theirs, or merged
$operation->baseRow;         // row in base snapshot, or null
$operation->oursRow;         // row in ours snapshot, or null
$operation->theirsRow;       // row in theirs snapshot, or null
$operation->resultValues;    // values the merge would write
$operation->changedColumns;  // changed result columns
```

## 선택 항목

UI에서 그룹을 준비할 때는 `ChangeGroupSelection`을 사용하세요. 적용하거나 롤백 아티팩트를 생성할 때는 이를 `OperationSelection`로 변환하세요.

```php
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

$selection = ChangeGroupSelection::fromIds([
    $plan->changeGroups[0]->id,
]);

$operationSelection = $selection->toOperationSelection($plan);

$selectedResult = (new SelectedMergeResultFactory())->fromOperationSelection(
    $plan,
    $operationSelection,
    SnapshotStore::load($plan->baseSnapshot),
);
```

선택한 결과는 선택된 작업만 포함하는 충돌 없는 `MergeResult`입니다. `Merql::apply()` 또는 `Merql::applyGuarded()`에 전달할 수 있습니다.

## 대상에 없는 항목 선택하기

병합 결과는 base를 병합된 상태로 바꾸므로 계획에는 양쪽의 작업이 포함됩니다. 대상에 이미 한쪽의 상태가 반영되어 있으면 대상에 아직 없는 작업만 선택하세요. 라이브 데이터베이스가 샌드박스 복사본의 편집 내용을 받는 경우처럼 대상에 theirs 상태가 저장되어 있다면 출처가 `ours`인 모든 작업과 양쪽의 열 변경 사항을 결합하는 `merged` 업데이트를 선택하면 됩니다.

```php
use Merql\Plan\OperationSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

// Rows with an unresolved conflict stay out of the selection.
$conflicted = [];
foreach ($plan->conflicts as $conflict) {
    $conflicted[$conflict->table . "\0" . $conflict->rowKey] = true;
}

$ids = [];
foreach ($plan->operations as $operation) {
    $missing = $operation->source === 'ours'
        || ($operation->source === 'merged' && $operation->type === 'update');

    if ($missing && !isset($conflicted[$operation->table . "\0" . $operation->rowKey])) {
        $ids[] = $operation->id;
    }
}

$selected = (new SelectedMergeResultFactory())->fromOperationSelection(
    $plan,
    OperationSelection::fromIds($ids),
    SnapshotStore::load($plan->baseSnapshot),
);
```

출처가 `theirs`인 작업과 `merged` 삭제(양쪽에서 삭제한 행)는 이미 대상에 반영되어 있습니다.

충돌이 있는 병합으로 만든 계획에도 양쪽에서 업데이트한 각 충돌 행의 작업이 포함됩니다. 결과 값에는 충돌한 열의 ours 값이 저장되며, `SelectedMergeResultFactory`은 어떤 항목을 선택하든 충돌 없는 결과를 반환합니다. 그 경우에도 작업과 변경 그룹의 `conflictState`은 현재 `clean`로 표시됩니다([#9](https://github.com/inline0/merql/issues/9)). 따라서 위와 같이 직접 `$plan->conflicts`을 확인하거나, `ConflictResolver`으로 충돌을 해결한 다음 해결된 결과를 사용해 `MergePlanBuilder`으로 계획을 구성하세요. 라이브 행 사전 조건을 적용해 선택 항목을 쓰는 방법은 [가드 적용](https://merql.inline0.dev/ko/docs/apply/guarded)을 참조하세요.

## 직렬화

병합 계획 및 선택 항목은 안정적인 JSON 직렬화를 지원합니다. 이후 적용 단계에서 검토한 작업 세트를 그대로 사용해야 한다면 직렬화된 계획을 저장하세요.

```php
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\MergePlanSerializer;

$json = MergePlanSerializer::toJson($plan);
$restored = MergePlanSerializer::fromJson($json);

$selectionJson = json_encode($selection->toArray(), JSON_THROW_ON_ERROR);
$selection = ChangeGroupSelection::fromArray(
    json_decode($selectionJson, true, flags: JSON_THROW_ON_ERROR),
);
```

## 별칭이 지정된 스냅샷

병합 계획은 정규 테이블 이름을 사용합니다. 환경 간 물리적 테이블 이름이 다르면 계획을 세우기 전에 같은 정규 이름으로 캡처하세요.

```php
use Merql\Snapshot\SnapshotStore;
use Merql\Snapshot\Snapshotter;

$snapshotter = new Snapshotter($connection);

SnapshotStore::save($snapshotter->captureAliased('live', [
    'wp_posts' => 'wp_posts',
]));

SnapshotStore::save($snapshotter->captureAliased('sandbox', [
    'wp_onumia_abc_posts' => 'wp_posts',
]));
```

스냅샷에 저장된 스키마는 정규 테이블 이름을 사용하면서 물리적 테이블의 열, 키, 식별 열, 행 키 및 필터를 보존합니다.
