
# Identita řádku

Identita řádku určuje, jak merql páruje řádky napříč snímky. Při porovnávání base s ours potřebuje vědět, že řádek X v base je tentýž logický řádek jako řádek Y v ours, i když se hodnoty některých sloupců změnily. Pravidlo identity určuje, které sloupce tvoří klíč řádku.

```php
use Merql\Identity\PrimaryKeyIdentity;

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

## Rozhraní RowIdentity

Všechny strategie identity implementují dvě metody:

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

`key()` vytvoří jedinečný řetězcový identifikátor z dat řádku. `columns()` vrátí názvy sloupců použitých k určení identity.

## PrimaryKeyIdentity

Výchozí strategie. Páruje řádky podle sloupců primárního klíče.

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

Tuto strategii merql standardně používá, pokud má tabulka ve schématu definovaný primární klíč.

## NaturalKeyIdentity

U tabulek bez automaticky navyšovaného primárního klíče páruje řádky podle sloupců s jedinečným omezením. Hodí se, když je PK UUID nebo když tabulka používá složené jedinečné omezení jako svou logickou identitu.

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

Záložní strategie pro tabulky bez primárního klíče i jedinečných omezení. Vypočítá hash SHA-256 ze všech hodnot sloupců. Dva řádky jsou stejné, pokud se shodují ve všech sloupcích.

```php
use Merql\Identity\ContentHashIdentity;

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

Identita založená na otisku obsahu má omezení: pokud se obsah řádku změní, změní se i jeho hash, takže se řádek místo aktualizace zobrazí jako odstranění starého hashe a vložení nového. Jde o nevyhnutelný kompromis, pokud nejsou k dispozici stabilní identifikační sloupce.

## IdentityRule

`IdentityRule` je serializovatelná konfigurace identity používaná při pořizování snímku a přípravě plánů sloučení.

```php
use Merql\Identity\IdentityRule;

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

Typy pravidel:

| Typ | Význam |
|---|---|
| `primary` | Sloupce primárního klíče databáze |
| `natural` | Stabilní jedinečné obchodní sloupce |
| `composite` | Sloupce složené logické identity |
| `content` | Záložní otisk obsahu |

`IdentityRule::forSchema($schema)` vybere výchozí pravidlo:

1. Pokud má tabulka primární klíč, použijí se jeho sloupce.
2. Pokud má tabulka jedinečné klíče, použijí se sloupce prvního z nich.
3. Pokud nemá ani jedno, použijí se všechny sloupce (záložní otisk obsahu).

## IdentityRuleSet

Použijte `IdentityRuleSet`, když hostitelská aplikace zná lepší pravidlo identity než výchozí pravidlo schématu.

```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` vybere `IdentityRule` pro každou zaznamenanou tabulku. Vybrané sloupce se uloží do `TableSnapshot::identityColumns` a použijí se při všech následných operacích diff, merge, plan, SQL a rollback.

`Merql::init()` může při použití statického rozhraní dostat stejnou sadu pravidel:

```php
use Merql\Merql;

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

Sady pravidel mají stabilní serializaci polí:

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

## Nejednoznačná identita

Pokud dva zaznamenané řádky vytvoří stejný klíč identity, pořizování snímku selže, místo aby jeden řádek přepsal druhým.

```php
use Merql\Exceptions\SnapshotException;

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

Hostitelské aplikace mohou před pořizováním snímku ověřit nezpracované řádky pomocí `IdentityRuleSet::conflictsFor()` a včas oznámit nejednoznačnost:

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

U metatabulWordPress je `post_id + meta_key` obvykle užitečné pro seskupování a zobrazení, ale není bezpečné jako identita řádku, protože duplicitní meta klíče jsou povolené. Pro identitu řádku použijte primární klíč tabulky, pokud hostitelská aplikace nemůže zaručit jedinečnost přirozeného klíče.

## Kódování klíče řádku

Klíče řádků jsou zakódované řetězce, které kombinují hodnoty více sloupců pomocí oddělovače jednotek (`\x1F`). Hodnoty obsahující oddělovač nebo znak procenta se escapují, aby se předešlo kolizím:

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

Pravidla escapování:

| Původní | Escapované |
|---|---|
| `%` | `%25` |
| `\x1F` (oddělovač jednotek) | `%1F` |

Tím se zajistí, že se hodnota obsahující `\x1F` nerozdělí na více částí klíče. Kódování je deterministické a reverzibilní.

## Rozdíly v automaticky navyšovaných ID

Když obě strany vloží nové řádky, automaticky navyšovaná ID se mezi větvemi budou lišit. Řádek #100 v ours může být zcela jiný záznam než řádek #100 v theirs.

merql tento problém řeší párováním podle primárního klíče. Pokud obě strany vložily řádek se stejným automaticky navyšovaným ID, označí jej jako konflikt `insert_insert`. Tomu lze předejít použitím přirozených klíčů (jedinečných sloupců, které jsou stabilní napříč větvemi) u tabulek, kde se očekává souběžné vkládání.

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

## Identita ve snímcích

Sloupce identity vybrané při pořizování snímku se ukládají spolu s každým `TableSnapshot`:

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

`SqlGenerator` tyto sloupce používá k sestavení klauzulí WHERE pro příkazy UPDATE a DELETE. Plány sloučení je využívají k zobrazení dekódovaných hodnot identity pro kontrolu, výběr, řízené použití a vrácení změn.
