
# Rencana Merge

`MergeResult` adalah keluaran merge yang dapat dieksekusi. `MergePlan` adalah bentuk keluaran tersebut yang dapat diperiksa. Gunakan saat aplikasi perlu menampilkan, menyiapkan, menserialisasi, atau menerapkan perubahan database secara selektif sebelum menulisnya ke database target.

```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()` memuat tiga snapshot bernama dan menggabungkannya pada tingkat kolom, seperti `Merql::merge()`. Untuk merencanakan merge yang menggunakan penggabungan tingkat sel pada kolom TEXT dan JSON, buat sendiri hasilnya dan berikan ke `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);
```

Rencana ini berisi:

- nama snapshot yang digunakan untuk membuatnya,
- ringkasan operasi, konflik, ketidakcocokan skema, tabel, dan grup,
- ID operasi stabil,
- ID grup perubahan stabil,
- kolom identitas baris dan nilai identitas,
- nilai baris base, ours, theirs, dan hasil untuk setiap operasi,
- metadata aplikasi host opsional,
- hash rencana,
- versi skema.

## ID Stabil

ID operasi bersifat deterministik untuk konten operasi yang sama. ID ini diturunkan dari nama tabel, kunci baris, jenis operasi, sumber operasi, kolom yang diubah, dan nilai hasil.

ID grup perubahan bersifat deterministik untuk konten grup yang sama. Builder default membuat satu grup operasi untuk setiap operasi. Aplikasi host dapat membuat grup khusus domain yang lebih besar dari daftar operasi dan tetap menserialisasikannya dengan bentuk DTO yang sama.

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

## Bentuk Operasi

Setiap `MergePlanOperation` menyimpan konteks yang cukup untuk ditinjau dan dikembalikan.

```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
```

## Selections

Gunakan `ChangeGroupSelection` saat UI menyiapkan grup. Ubah menjadi `OperationSelection` saat menerapkan atau membuat artefak rollback.

```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),
);
```

Hasil yang dipilih adalah `MergeResult` bersih yang hanya berisi operasi terpilih. Hasil ini dapat diberikan ke `Merql::apply()` atau `Merql::applyGuarded()`.

## Memilih yang Belum Dimiliki Target

Rencana berisi operasi dari kedua sisi karena hasil merge mengubah base menjadi keadaan gabungan. Jika target sudah memiliki salah satu sisi, pilih hanya operasi yang belum dimilikinya. Untuk target yang memiliki theirs, misalnya database aktif yang menerima perubahan dari salinan sandbox, pilih semua operasi dengan sumber `ours`, ditambah pembaruan `merged` yang menggabungkan perubahan kolom dari kedua sisi:

```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),
);
```

Operasi dengan sumber `theirs` dan penghapusan `merged` (baris yang dihapus oleh kedua sisi) sudah ada di target tersebut.

Rencana yang dibuat dari merge dengan konflik tetap mencantumkan operasi untuk setiap baris berkonflik yang diperbarui oleh kedua sisi. Nilai hasilnya menyimpan nilai ours pada kolom yang berkonflik, dan `SelectedMergeResultFactory` menghasilkan hasil bersih untuk pilihan apa pun. Bahkan dalam kondisi itu, `conflictState` operasi dan grup perubahan saat ini bernilai `clean` ([#9](https://github.com/inline0/merql/issues/9)), jadi periksa sendiri `$plan->conflicts` seperti di atas, atau selesaikan konflik dengan `ConflictResolver` lalu buat rencana dari hasil yang telah diselesaikan menggunakan `MergePlanBuilder`. Lihat [Penerapan Terjaga](https://merql.inline0.dev/id/docs/apply/guarded) untuk menulis pilihan dengan prasyarat baris aktif.

## Serialisasi

Rencana merge dan pilihan memiliki serialisasi JSON yang stabil. Simpan rencana yang telah diserialisasi jika langkah penerapan berikutnya harus menggunakan kumpulan operasi yang persis sama dengan yang ditinjau.

```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),
);
```

## Snapshot dengan Alias

Rencana merge menggunakan nama tabel kanonis. Jika nama tabel fisik berbeda antarlingkungan, ambil snapshot dengan nama kanonis yang sama sebelum merencanakan merge.

```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',
]));
```

Skema yang disimpan dalam snapshot menggunakan nama tabel kanonis sambil mempertahankan kolom, kunci, kolom identitas, kunci baris, dan filter tabel fisik.
