
# Merge Tiga Arah

Merge tiga arah membandingkan dua kumpulan perubahan dengan leluhur yang sama, lalu menggabungkannya menjadi satu hasil. merql menerapkan algoritma ini pada baris database, bukan baris berkas.

```php
use Merql\Merge\ThreeWayMerge;

$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);

echo $result->operationCount(); // resolved operations
echo $result->conflictCount();  // unresolved conflicts
```

## Tiga keadaan

Setiap merge memerlukan tepat tiga snapshot:

- **Base** -- keadaan database saat kedua cabang mulai berbeda.
- **Ours** -- salah satu cabang setelah mengalami perubahan, misalnya salinan sandbox yang telah diedit.
- **Theirs** -- cabang lainnya setelah mengalami perubahan, misalnya database aktif yang terus berjalan.

Ours dan theirs masing-masing harus diambil dari database yang menyimpan cabang tersebut. Aturan merge memperlakukan kedua sisi dengan cara yang sama; namanya menentukan `source` setiap operasi, penamaan jenis konflik (`update_delete` berarti ours diperbarui dan theirs dihapus), serta sisi yang diutamakan oleh `ConflictPolicy::OursWins` dan `TheirsWins`.

```
         Base (snapshot at fork time)
        /                              \
   Ours (our changes)                   Theirs (their changes)
        \                              /
         --------- MERGE ----------
                     |
              Merged result
              + conflicts (if any)
```

Tanpa base bersama, Anda tidak dapat membedakan "ditambahkan" dari "tidak berubah". Perbandingan dua arah menunjukkan bahwa sisi A memiliki nilai dan sisi B tidak, tetapi tidak dapat menentukan apakah A menambahkannya atau B menghapusnya. Base menjawab pertanyaan itu.

## Cara changeset dihitung

`Differ` membandingkan setiap snapshot dengan base untuk menghasilkan dua changeset. Setiap changeset berisi tiga jenis operasi:

| Base | Saat ini | Operasi |
|---|---|---|
| Baris ada | Baris ada, fingerprint sama | Tidak ada perubahan |
| Baris ada | Baris ada, fingerprint berbeda | **UPDATE** (perbedaan per kolom) |
| Baris ada | Baris tidak ada | **DELETE** |
| Baris tidak ada | Baris ada | **INSERT** |

Fingerprint (hash SHA-256 dari konten baris yang dinormalisasi) menyediakan jalur cepat. Jika dua fingerprint cocok, baris tidak berubah dan perbandingan tingkat kolom tidak diperlukan.

```php
use Merql\Diff\Differ;

$differ = new Differ();
$oursChangeset = $differ->diff($base, $ours);
$theirsChangeset = $differ->diff($base, $theirs);
```

## Aturan merge

Kedua changeset kemudian digabungkan operasi demi operasi. Tabel ini menentukan hasil untuk setiap kombinasi:

| Ours | Theirs | Hasil |
|---|---|---|
| Tidak ada perubahan | Tidak ada perubahan | Pertahankan base |
| Tidak ada perubahan | Diperbarui | Terima theirs |
| Diperbarui | Tidak ada perubahan | Terima ours |
| Diperbarui (nilai sama) | Diperbarui (nilai sama) | Terima (keduanya setuju) |
| Diperbarui (nilai berbeda) | Diperbarui (nilai berbeda) | **Konflik** |
| Tidak ada perubahan | Dihapus | Terima penghapusan |
| Dihapus | Tidak ada perubahan | Terima penghapusan |
| Dihapus | Dihapus | Terima penghapusan (keduanya setuju) |
| Diperbarui | Dihapus | **Konflik** |
| Dihapus | Diperbarui | **Konflik** |
| Disisipkan (hanya ours) | -- | Terima penyisipan |
| -- | Disisipkan (hanya theirs) | Terima penyisipan |
| Disisipkan (PK sama) | Disisipkan (PK sama) | **Konflik** |

Saat kedua sisi memperbarui baris yang sama, merql tidak langsung menyatakan adanya konflik. Proses berlanjut ke penggabungan tingkat kolom dengan membandingkan setiap kolom secara terpisah. Lihat [Penggabungan Tingkat Kolom](https://merql.inline0.dev/id/docs/merge/column-level) untuk detailnya.

## Menggunakan facade

Facade `Merql` bekerja pada snapshot bernama yang telah disimpan ke disk. Panggil `Merql::init()` dengan database yang menyimpan setiap keadaan sebelum mengambil snapshot:

```php
use Merql\Connection;
use Merql\Merql;

$live    = Connection::sqlite('/data/live.sqlite');
$sandbox = Connection::sqlite('/data/sandbox.sqlite');

Merql::init($live);
Merql::snapshot('base');      // before the sandbox is copied from live

// ... copy live.sqlite to sandbox.sqlite, then both databases change ...

Merql::init($sandbox);
Merql::snapshot('ours');
Merql::init($live);
Merql::snapshot('theirs');

$result = Merql::merge('base', 'ours', 'theirs');
```

Jangan mengambil ketiga snapshot secara berurutan dari satu database. Dengan begitu, theirs juga akan berisi perubahan ours, sehingga setiap baris yang disisipkan ours akan tampak disisipkan oleh kedua sisi dan dilaporkan sebagai konflik `insert_insert`.

`Merql::merge()`, `Merql::patch()`, dan `Merql::plan()` membuat `ThreeWayMerge` tanpa `CellMergeConfig`, sehingga hanya menggabungkan pada tingkat kolom. Untuk penggabungan tingkat sel, muat snapshot dan gunakan kelas tersebut secara langsung, seperti di bawah ini.

## Menggunakan kelas secara langsung

Untuk kendali lebih lanjut, buat instans komponen sendiri. `Snapshotter::capture()` tidak menyimpan apa pun, jadi simpan snapshot di memori atau simpan dengan `SnapshotStore::save()`:

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

$base   = (new Snapshotter($live))->capture('base');
// ... copy live to the sandbox, then both databases change ...
$ours   = (new Snapshotter($sandbox))->capture('ours');
$theirs = (new Snapshotter($live))->capture('theirs');

$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);

// Snapshots saved earlier, merged with cell-level merge for TEXT and JSON columns.
$merge = new ThreeWayMerge(CellMergeConfig::auto());
$result = $merge->merge(
    SnapshotStore::load('base'),
    SnapshotStore::load('ours'),
    SnapshotStore::load('theirs'),
);
```

## Pintasan patch

Jika hanya satu sisi yang memiliki perubahan (tanpa modifikasi bersamaan), gunakan `patch()`. Ini adalah merge dua arah dengan ours sama dengan base, jadi konflik tidak mungkin terjadi:

```php
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.
```

Melalui facade:

```php
$result = Merql::patch('base', 'changes');
```

`patch()` setara dengan `merge($base, $base, $changes)`. Karena ours tidak pernah berbeda dari base, setiap perubahan dari theirs diterima tanpa konflik.

## Deteksi ketidakcocokan skema

Jika kolom ditambahkan ke atau dihapus dari tabel di antara snapshot, merge tetap berjalan, tetapi mencatat ketidakcocokan tersebut. Jenis kolom yang berubah tidak dibandingkan, dan kolom yang diganti namanya muncul sebagai satu kolom yang dihapus dan satu yang ditambahkan; lihat [Validasi Skema](https://merql.inline0.dev/id/docs/advanced/schema).

```php
$result = $merge->merge($base, $ours, $theirs);

if ($result->hasSchemaMismatches()) {
    foreach ($result->schemaMismatches() as $mismatch) {
        echo $mismatch->getMessage() . "\n";
    }
}
```

merql tidak memigrasikan skema secara otomatis. Perubahan skema menjadi tanggung jawab pemanggil. Merge melaporkannya agar Anda dapat menentukan langkah selanjutnya.

## MergeResult

Objek hasil membawa semua yang Anda perlukan:

```php
$result->isClean();            // true if no conflicts
$result->operations();         // list of MergeOperation objects
$result->conflicts();          // list of Conflict objects
$result->operationCount();     // number of resolved operations
$result->conflictCount();      // number of unresolved conflicts
$result->hasSchemaMismatches(); // true if schemas differ
$result->baseSnapshot();       // the base snapshot used for the merge
```

Setiap `MergeOperation` memiliki jenis (`insert`, `update`, atau `delete`), nama tabel, kunci baris, nilai kolom, dan sumber (`ours`, `theirs`, atau `merged`). `merged` menandai baris yang diperbarui oleh kedua sisi (digabungkan kolom demi kolom) atau dihapus oleh kedua sisi.

## Tempat hasil merge diterapkan

Hasil merge menjelaskan cara mengubah base menjadi keadaan gabungan. Operasinya membawa perubahan dari kedua sisi: penyisipan yang dibuat ours dan penyisipan yang dibuat theirs sama-sama muncul sebagai penyisipan.

- `Merql::apply()` (atau `Applier`) menjalankan setiap operasi pada database yang terhubung. Operasi ini diterapkan dengan bersih ke database yang masih menyimpan base, misalnya salinan baru dari keadaan base. Jika diterapkan pada database yang sudah berisi perubahan dari salah satu sisi, penyisipan baris yang sudah ada akan gagal dan transaksi dibatalkan.
- Untuk membawa perubahan ours ke database yang sudah menyimpan theirs, misalnya mempromosikan perubahan sandbox ke lingkungan aktif, terapkan hanya operasi yang belum dimiliki database tersebut: operasi dengan sumber `ours` dan pembaruan `merged`. Gunakan [rencana merge](https://merql.inline0.dev/id/docs/merge/plans) untuk memilihnya dan [Penerapan Terjaga](https://merql.inline0.dev/id/docs/apply/guarded) untuk menulisnya dengan prasyarat baris aktif.
