
# Sorazonosság

A sorazonosság határozza meg, hogyan párosítja a merql a sorokat a pillanatfelvételek között. Amikor a base és az ours állapotot hasonlítja össze, tudnia kell, hogy a X sor a base-ben logikailag megegyezik az ours Y sorával akkor is, ha egyes oszlopértékek megváltoztak. Az azonosítási szabály határozza meg, mely oszlopok alkotják a sor kulcsát.

```php
use Merql\Identity\PrimaryKeyIdentity;

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

## A RowIdentity interfész

Minden azonosítási stratégia két metódust valósít meg:

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

A `key()` egyedi szöveges azonosítót állít elő a sor adataiból. A `columns()` visszaadja az azonosításhoz használt oszlopneveket.

## PrimaryKeyIdentity

Az alapértelmezett stratégia. Az elsődleges kulcs oszlopai alapján párosítja a sorokat.

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

Ezt használja alapértelmezés szerint a merql, ha a táblához tartozik elsődleges kulcs a sémában.

## NaturalKeyIdentity

Az elsődleges kulcs automatikus növelése nélküli tábláknál az egyedi megszorítás oszlopai alapján párosítja a sorokat. Hasznos, ha a PK UUID, vagy ha a tábla összetett egyedi megszorítást használ logikai azonosítóként.

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

Visszaesési megoldás olyan táblákhoz, amelyeknek nincs elsődleges kulcsuk vagy egyedi megszorításuk. Kiszámítja az összes oszlopérték SHA-256 hashét. Két sor akkor azonos, ha minden oszlopuk egyezik.

```php
use Merql\Identity\ContentHashIdentity;

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

A tartalomhash-alapú azonosításnak van egy korlátja: ha megváltozik egy sor tartalma, a hash is megváltozik, ezért a sor a régi hash törléseként és az új hash beszúrásaként jelenik meg, nem frissítésként. Ez elkerülhetetlen kompromisszum, ha nincsenek stabil azonosítóoszlopok.

## IdentityRule

A `IdentityRule` a pillanatfelvétel készítése és a merge-tervek által használt szerializálható azonosítási konfiguráció.

```php
use Merql\Identity\IdentityRule;

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

Szabálytípusok:

| Típus | Jelentés |
|---|---|
| `primary` | Adatbázis elsődleges kulcs oszlopai |
| `natural` | Stabil, egyedi üzleti oszlopok |
| `composite` | Összetett logikai azonosító oszlopai |
| `content` | Tartalomhash-alapú visszaesési megoldás |

A `IdentityRule::forSchema($schema)` feloldja az alapértelmezett szabályt:

1. Ha a táblának van elsődleges kulcsa, az elsődleges kulcs oszlopait használja.
2. Ha vannak egyedi kulcsok, az elsőként meghatározott egyedi kulcs oszlopait használja.
3. Ha egyik sem áll rendelkezésre, minden oszlopot felhasznál (tartalomhash-alapú visszaesés).

## IdentityRuleSet

Akkor használja a `IdentityRuleSet` megoldást, ha a hostalkalmazás a séma alapértelmezettjénél jobb azonosítási szabályt ismer.

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

A `Snapshotter` minden rögzített táblához felold egy `IdentityRule` értéket. A kiválasztott oszlopok a `TableSnapshot::identityColumns` értékben tárolódnak, és minden további diff-, merge-, terv-, SQL- és visszaállítási művelet során felhasználásra kerülnek.

A statikus facade használatakor a `Merql::init()` ugyanazt a szabálykészletet kaphatja meg:

```php
use Merql\Merql;

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

A szabálykészletek tömbként stabilan szerializálhatók:

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

## Kétértelmű azonosítás

Ha két rögzített sorhoz ugyanaz az azonosítókulcs tartozik, a rögzítés sikertelen lesz, ahelyett hogy az egyik sor felülírná a másikat.

```php
use Merql\Exceptions\SnapshotException;

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

A hostalkalmazások a nyers sorokat a `IdentityRuleSet::conflictsFor()` segítségével előzetesen ellenőrizhetik, ha már a rögzítés előtt jelezni szeretnék a kétértelműséget:

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

A WordPress meta tábláknál általában hasznos a `post_id + meta_key` csoportosításhoz és megjelenítéshez, de sorkulcsként nem biztonságos, mert az ismétlődő meta kulcsok megengedettek. A sorazonossághoz használja a tábla elsődleges kulcsát, kivéve, ha a hostalkalmazás igazolni tudja, hogy a természetes kulcs egyedi.

## Sorkulcs kódolása

A sorkulcsok kódolt szöveges értékek, amelyek több oszlop értékét választókarakterrel (`\x1F`) kapcsolják össze. Az ütközések elkerülése érdekében a választókaraktert vagy százalékjelet tartalmazó értékeket escape-eljük:

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

Escape-szabályok:

| Eredeti | Escape-elt |
|---|---|
| `%` | `%25` |
| `\x1F` (választókarakter) | `%1F` |

Ez biztosítja, hogy a `\x1F` értéket tartalmazó érték ne essen szét több kulcsrészre. A kódolás determinisztikus és visszafordítható.

## Automatikus növelésű azonosítók eltérése

Ha mindkét oldal új sorokat szúr be, az automatikusan növekvő azonosítók eltérnek az egyes ágakban. Az ours #100 sora lehet, hogy teljesen más rekord, mint a theirs #100 sora.

A merql ezt az elsődleges kulcs alapján kezeli. Ha mindkét oldal azonos, automatikusan növekvő azonosítóval szúrt be egy sort, akkor az `insert_insert` ütközésként jelenik meg. Ennek elkerüléséhez használjon természetes kulcsokat (az ágak között stabil egyedi oszlopokat) azoknál a tábláknál, amelyekbe várhatóan egyszerre történik beszúrás.

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

## Azonosítók a pillanatfelvételekben

A rögzítéskor kiválasztott azonosítóoszlopok minden egyes `TableSnapshot` mellett tárolódnak:

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

A `SqlGenerator` ezeket az oszlopokat használja az UPDATE és DELETE utasítások WHERE záradékainak összeállításához. A merge-tervek emellett dekódolt azonosítóértékeket tesznek elérhetővé áttekintéshez, kiválasztáshoz, ellenőrzött alkalmazáshoz és visszaállításhoz.
