
# 受保护应用

仅当目标数据库仍与预期的实时快照相符时，`GuardedApplier` 才会应用无冲突的 `MergeResult`。如果合并计划较早前已审核，而实时行可能已发生变化，这项功能就很有用。

典型用法是将沙盒中的编辑内容提升到实时数据库。base 是复制沙盒时的实时数据库，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` 更新。

## 选择操作

合并结果包含两侧的操作，因为合并会将 base 转换为合并后的状态。将其应用于已经包含 theirs 的目标时，来自 theirs 的操作表示目标已有的变更，因此这些操作的前置条件会失败：

| 操作 | 包含 theirs 的目标中的状态 | 选择 |
|---|---|---|
| `ours` 插入、更新或删除 | 缺失 | 是 |
| `merged` 更新（两侧都更改了该行） | 已有 theirs 的列，缺少 ours 的列 | 是 |
| `theirs` 插入 | 行已存在，因此受保护的插入会影响零行 | 否 |
| `merged` 删除（两侧都删除了该行） | 行已删除，因此受保护的删除会影响零行 | 否 |
| `theirs` 更新或删除 | 已应用 | 否 |

因此，将 `Merql::merge()` 的完整结果传给 `applyGuarded()`，只要 theirs 插入了某行或两侧都删除了某行，就会失败。先按上述来源选择操作，或使用基于审查`ChangeGroupSelection`构建的选择UI。排除 `$plan->conflicts` 中列出的行：所选结果始终没有冲突，且冲突行对应的操作会在冲突列中携带 ours 的值（参见[合并计划](https://merql.inline0.dev/zh/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()` 在没有前置条件的情况下执行相同操作。此时，无论计划生成后实时行是否发生变化，更新和删除都会成功；如果插入的行已存在，则会触发数据库的重复键错误。对于你确定仍处于基准状态的目标，使用普通应用；如果自创建快照以来其他写入方可能修改过目标，则使用带保护条件的应用。
