
# 가드 적용

`GuardedApplier`은 대상 데이터베이스가 예상 라이브 스냅샷과 일치할 때만 충돌 없는 `MergeResult`을 적용합니다. 병합 계획을 미리 검토했고 적용 전에 라이브 행이 변경되었을 수 있는 경우 유용합니다.

일반적인 설정에서는 샌드박스의 편집 내용을 라이브 데이터베이스로 승격합니다. 기준은 샌드박스를 복사한 시점의 라이브 상태이고, ours는 편집한 샌드박스이며, theirs는 현재 라이브 상태입니다. 대상은 라이브 데이터베이스이므로 예상 라이브 스냅샷은 `theirs`이고, 쓸 작업은 라이브에 아직 없는 작업입니다([작업 선택](#choosing-operations) 참조).

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

Merql::init(Connection::sqlite('/data/live.sqlite'));

$plan = Merql::plan('promote-1', 'base', 'ours', 'theirs');

// Skip rows with unresolved conflicts.
$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('base'),
);

$applied = Merql::applyGuarded($selected, SnapshotStore::load('theirs'));

if ($applied->hasErrors()) {
    foreach ($applied->errors() as $error) {
        echo $error . "\n";
    }
}
```

`Merql::applyGuarded()`은 `Merql::init()`에 전달된 데이터베이스에 씁니다. 예상 라이브 스냅샷은 같은 데이터베이스를 설명해야 합니다. 대상에서 가져오고, 사전 조건이 의미를 가질 만큼 최근에 캡처하세요.

## 사전 조건

가드 적용 SQL은 생성된 문에 라이브 행 사전 조건을 추가합니다:

- 업데이트는 대상 행 식별자가 존재하고 비식별 열 값이 예상 라이브 행과 일치해야 합니다.
- 삭제는 대상 행 식별자가 존재하고 비식별 열 값이 예상 라이브 행과 일치해야 합니다.
- 삽입은 대상 식별자가 아직 존재하지 않아야 합니다.

문 실행이 행에 영향을 주지 않으면 merql은 계획이 오래된 것으로 처리합니다. 트랜잭션이 롤백되고 반환된 `ApplyResult`에는 오류 `Optimistic precondition failed for guarded merge statement.`이 포함됩니다.

MySQL에서는 값이 이미 라이브 행과 같은 업데이트도 변경된 행이 아닌 일치한 행을 MySQL이 계산하므로 행에 영향을 주지 않습니다. 따라서 행이 일치하더라도 해당 업데이트는 오래된 것으로 실패합니다([#2](https://github.com/inline0/merql/issues/2)). SQLite는 일치한 행을 계산하므로 이를 허용합니다. 양쪽에서 같은 행을 똑같이 변경한 경우, `merged` 업데이트에서 이런 일이 발생할 수 있습니다.

## 작업 선택

병합 결과에는 양쪽의 작업이 모두 포함됩니다. 기준 상태를 병합된 상태로 바꾸기 때문입니다. 이미 theirs 상태인 대상에 적용하면 theirs에서 비롯된 작업은 대상에 이미 반영된 변경을 설명하므로 사전 조건이 실패합니다:

| 작업 | theirs 상태를 보유한 대상의 상태 | 선택 |
|---|---|---|
| `ours` 삽입, 업데이트 또는 삭제 | 없음 | 예 |
| `merged` 업데이트(양쪽에서 행을 변경함) | theirs의 열은 있고 ours의 열은 없음 | 예 |
| `theirs` 삽입 | 행이 존재하므로 가드 삽입은 영향을 주는 행이 0개입니다 | 아니요 |
| `merged` 삭제(양쪽에서 행을 삭제함) | 행이 없으므로 가드 삭제는 영향을 주는 행이 0개입니다 | 아니요 |
| `theirs` 업데이트 또는 삭제 | 이미 적용됨 | 아니요 |

따라서 `Merql::merge()`의 전체 결과를 `applyGuarded()`에 전달하면 theirs에서 행을 삽입했거나 양쪽에서 행을 삭제한 경우 즉시 실패합니다. 위와 같이 출처별로 작업을 선택하거나 검토 UI에서 만든 `ChangeGroupSelection`를 사용해 작업을 먼저 선택하세요. `$plan->conflicts`에 나열된 행은 제외하세요. 선택한 결과는 항상 충돌이 없으며, 충돌한 행의 작업은 충돌 열에 ours의 값을 포함합니다([병합 계획](https://merql.inline0.dev/ko/docs/merge/plans#selecting-what-a-target-is-missing) 참조).

## 선택 적용

가드 적용은 변경 그룹별로 선택한 항목을 포함해 병합 계획에서 생성한 선택된 병합 결과와 함께 작동합니다.

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

$selection = ChangeGroupSelection::fromIds([$plan->changeGroups[0]->id]);
$selected = (new SelectedMergeResultFactory())->fromChangeGroupSelection(
    $plan,
    $selection,
    SnapshotStore::load($plan->baseSnapshot),
);

$applied = Merql::applyGuarded($selected, SnapshotStore::load($plan->theirsSnapshot));
```

이를 통해 호스트 애플리케이션은 검토한 그룹의 일부를 준비하면서 계획 생성 후 라이브 행이 바뀌지 않았음을 확인할 수 있습니다.

## SQL 미리보기

`SqlGenerator::generateGuarded()`을 사용해 가드 문을 실행하지 않고 검사하세요.

```php
use Merql\Apply\SqlGenerator;
use Merql\Snapshot\SnapshotStore;

$statements = SqlGenerator::generateGuarded(
    $selected,
    SnapshotStore::load($plan->theirsSnapshot),
);

foreach ($statements as $statement) {
    echo $statement['sql'] . "\n";
}
```

생성된 문은 계속 매개변수화되어 있습니다. 사전 조건은 업데이트/삭제의 추가 `WHERE` 절과 삽입의 `NOT EXISTS` 검사로 표현됩니다.

## 가드 적용 또는 일반 적용

`Merql::apply()`은 사전 조건 없이 같은 작업을 실행합니다. 그러면 계획을 만든 뒤 라이브 행이 변경되었는지와 관계없이 업데이트와 삭제가 성공하며, 기존 행을 삽입하면 데이터베이스의 중복 키 오류가 발생합니다. 대상이 여전히 기준 상태라고 확신하는 경우에는 일반 적용을 사용하고, 스냅샷을 만든 뒤 다른 작성자가 대상을 수정했을 가능성이 있는 경우에는 가드 적용을 사용하세요.
