
# Identitas Baris

Identitas baris menentukan cara merql mencocokkan baris di antara snapshot. Saat membandingkan base dengan ours, sistem perlu mengetahui bahwa baris X di base adalah baris logis yang sama dengan baris Y di ours, meskipun beberapa nilai kolom berubah. Aturan identitas menentukan kolom yang membentuk kunci baris.

```php
use Merql\Identity\PrimaryKeyIdentity;

$identity = new PrimaryKeyIdentity(['id']);
$key = $identity->key(['id' => 42, 'title' => 'Hello']);
// "42"
```

## Antarmuka RowIdentity

Semua strategi identitas mengimplementasikan dua metode:

```php
interface RowIdentity
{
    public function key(array $row): string;
    public function columns(): array;
}
```

`key()` membangun pengenal string unik dari data baris. `columns()` mengembalikan nama kolom yang digunakan untuk identitas.

## PrimaryKeyIdentity

Strategi default. Mencocokkan baris berdasarkan kolom kunci utamanya.

```php
use Merql\Identity\PrimaryKeyIdentity;

// Single-column PK.
$identity = new PrimaryKeyIdentity(['id']);
$key = $identity->key(['id' => 42, 'title' => 'Hello']);
// "42"

// Composite PK.
$identity = new PrimaryKeyIdentity(['tenant_id', 'user_id']);
$key = $identity->key(['tenant_id' => 1, 'user_id' => 99, 'name' => 'Alice']);
// "1\x1F99" (unit separator between parts)
```

Inilah strategi yang digunakan merql secara default ketika tabel memiliki kunci utama yang ditetapkan dalam skemanya.

## NaturalKeyIdentity

Untuk tabel tanpa kunci utama auto-increment, mencocokkan baris berdasarkan kolom batasan unik. Berguna ketika PK berupa UUID atau ketika tabel menggunakan batasan unik gabungan sebagai identitas logisnya.

```php
use Merql\Identity\NaturalKeyIdentity;

$identity = new NaturalKeyIdentity(['email']);
$key = $identity->key(['id' => 1, 'email' => 'alice@example.com', 'name' => 'Alice']);
// "alice@example.com"

$identity = new NaturalKeyIdentity(['country', 'postal_code']);
$key = $identity->key(['country' => 'US', 'postal_code' => '90210', 'city' => 'Beverly Hills']);
// "US\x1F90210"
```

## ContentHashIdentity

Fallback untuk tabel tanpa kunci utama dan tanpa batasan unik. Menghitung hash SHA-256 dari semua nilai kolom. Dua baris dianggap sama jika setiap kolom cocok.

```php
use Merql\Identity\ContentHashIdentity;

$identity = new ContentHashIdentity(['name', 'value']);
$key = $identity->key(['name' => 'color', 'value' => 'blue']);
// "a3f2..." (SHA-256 hash)
```

Identitas hash konten memiliki keterbatasan: jika konten suatu baris berubah, hash-nya juga berubah. Akibatnya, baris tersebut tampak sebagai penghapusan hash lama dan penyisipan hash baru, bukan pembaruan. Ini adalah konsekuensi bawaan ketika tidak ada kolom identitas yang stabil.

## IdentityRule

`IdentityRule` adalah konfigurasi identitas yang dapat diserialisasi dan digunakan oleh pengambilan snapshot
serta rencana merge.

```php
use Merql\Identity\IdentityRule;

$rule = IdentityRule::natural(['option_name']);
$key = $rule->key(['option_name' => 'siteurl']);
// "siteurl"
```

Jenis aturan:

| Jenis | Makna |
|---|---|
| `primary` | Kolom kunci utama database |
| `natural` | Kolom bisnis unik yang stabil |
| `composite` | Kolom identitas logis gabungan |
| `content` | Fallback hash konten |

`IdentityRule::forSchema($schema)` menentukan aturan default:

1. Jika tabel memiliki kunci utama, gunakan kolom kunci utama.
2. Jika tabel memiliki kunci unik, gunakan kolom dari kunci unik pertama.
3. Jika keduanya tidak ada, gunakan semua kolom (fallback hash konten).

## IdentityRuleSet

Gunakan `IdentityRuleSet` ketika aplikasi host mengetahui aturan identitas yang lebih baik daripada default skema.

```php
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;
use Merql\Snapshot\Snapshotter;

$rules = new IdentityRuleSet([
    'wp_options' => IdentityRule::natural(['option_name']),
    'wp_term_relationships' => IdentityRule::composite(['object_id', 'term_taxonomy_id']),
]);

$snapshotter = new Snapshotter($connection, identityRules: $rules);
$snapshot = $snapshotter->capture('live', ['wp_options', 'wp_term_relationships']);
```

`Snapshotter` menentukan `IdentityRule` untuk setiap tabel yang diambil. Kolom yang dipilih disimpan di `TableSnapshot::identityColumns` dan digunakan untuk semua operasi diff, merge, plan, SQL, dan rollback berikutnya.

`Merql::init()` dapat menerima kumpulan aturan yang sama saat menggunakan facade statis:

```php
use Merql\Merql;

Merql::init($connection, identityRules: $rules);
Merql::snapshot('live', ['wp_options']);
```

Kumpulan aturan memiliki serialisasi array yang stabil:

```php
$payload = $rules->toArray();
$rules = IdentityRuleSet::fromArray($payload);
```

## Identitas Ambigu

Jika dua baris yang diambil menghasilkan kunci identitas yang sama, pengambilan gagal alih-alih menimpa satu baris dengan baris lainnya.

```php
use Merql\Exceptions\SnapshotException;

try {
    $snapshotter->capture('live', ['wp_postmeta']);
} catch (SnapshotException $e) {
    // The configured identity rule matched multiple rows.
}
```

Aplikasi host dapat melakukan pemeriksaan awal pada baris mentah dengan `IdentityRuleSet::conflictsFor()` saat ingin melaporkan ambiguitas sebelum pengambilan:

```php
$conflicts = $rules->conflictsFor(
    'wp_postmeta',
    IdentityRule::natural(['post_id', 'meta_key']),
    $rows,
);
```

Untuk tabel meta WordPress, `post_id + meta_key` biasanya berguna untuk pengelompokan dan tampilan, tetapi tidak aman sebagai identitas baris karena kunci meta duplikat valid. Gunakan kunci utama tabel sebagai identitas baris, kecuali aplikasi host dapat membuktikan bahwa kunci alami tersebut unik.

## Pengodean kunci baris

Kunci baris adalah string berkode yang menggabungkan beberapa nilai kolom dengan pemisah unit (`\x1F`). Nilai yang berisi pemisah atau tanda persen di-escape untuk mencegah tabrakan:

```php
use Merql\Snapshot\Snapshotter;

// Encode.
$key = Snapshotter::buildRowKey(
    ['tenant_id' => '1', 'user_id' => '99'],
    ['tenant_id', 'user_id'],
);
// "1\x1F99"

// Decode.
$parts = Snapshotter::decodeRowKey($key);
// ['1', '99']
```

Aturan escape:

| Asli | Di-escape |
|---|---|
| `%` | `%25` |
| `\x1F` (pemisah unit) | `%1F` |

Ini memastikan nilai yang berisi `\x1F` tidak terpecah menjadi beberapa bagian kunci. Pengodean ini deterministik dan dapat dibalik.

## Perbedaan auto-increment

Saat kedua sisi menyisipkan baris baru, ID auto-increment akan berbeda antar cabang. Baris #100 di sisi kita mungkin merupakan catatan yang sama sekali berbeda dari baris #100 di sisi mereka.

merql menangani hal ini dengan mencocokkan berdasarkan kunci utama. Jika kedua sisi menyisipkan baris dengan ID auto-increment yang sama, hal itu ditandai sebagai konflik `insert_insert`. Untuk menghindarinya, gunakan kunci alami (kolom unik yang stabil antar cabang) pada tabel yang diperkirakan menerima penyisipan secara bersamaan.

```php
// A natural key on (slug) avoids auto-increment collisions.
$identity = new NaturalKeyIdentity(['slug']);
```

## Identitas dalam snapshot

Kolom identitas yang dipilih saat pengambilan disimpan bersama setiap `TableSnapshot`:

```php
$tableSnapshot = $snapshot->getTable('posts');
$tableSnapshot->identityColumns;
// ['id']
```

Kolom-kolom ini digunakan oleh `SqlGenerator` untuk menyusun klausa WHERE bagi pernyataan UPDATE dan DELETE, serta oleh rencana merge untuk menampilkan nilai identitas yang telah didekode untuk ditinjau, dipilih, diterapkan dengan pengamanan, dan di-rollback.
