
# マージ計画

`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()`は、名前付きの3つのスナップショットを読み込み、`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
- 行のidentity列とidentity値
- 各操作の基準、ours、theirs、結果の行の値
- 任意のホストアプリケーションのメタデータ
- 計画のハッシュ
- スキーマのバージョン

## 安定したID

同じ操作内容には、常に同じ操作IDが割り当てられます。IDはテーブル名、行キー、操作の種類、操作のソース、変更された列、結果の値から生成されます。

同じグループ内容には、常に同じ変更グループIDが割り当てられます。デフォルトのビルダーは操作ごとに1つの操作グループを作成します。ホストアプリケーションは操作リストから、より大きなドメイン固有のグループを作成し、同じ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()`に渡せます。

## ターゲットにない変更の選択

マージ結果は基準をマージ後の状態に変えるため、計画には両側の操作が含まれます。ターゲットにすでに片側の状態がある場合、まだ適用されていない操作だけを選択します。たとえば、サンドボックスのコピーでの編集をライブデータベースに反映するなど、ターゲットが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`で解決済みの結果から計画を作成してください。ライブ行の事前条件を使って選択内容を書き込む方法は、[Guarded Apply](https://merql.inline0.dev/ja/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',
]));
```

スナップショットに保存されるスキーマでは正規のテーブル名が使われますが、物理テーブルの列、キー、identity列、行キー、フィルターはそのまま保持されます。
