
# Identità delle righe

L'identità delle righe determina come merql abbina le righe tra snapshot. Quando confronta base con ours, deve sapere che la riga X in base è la stessa riga logica della riga Y in ours, anche se alcuni valori delle colonne sono cambiati. La regola di identità stabilisce quali colonne compongono la chiave della riga.

```php
use Merql\Identity\PrimaryKeyIdentity;

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

## L'interfaccia RowIdentity

Tutte le strategie di identità implementano due metodi:

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

`key()` costruisce un identificatore univoco in forma di stringa a partire dai dati di una riga. `columns()` restituisce i nomi delle colonne usate per l'identità.

## PrimaryKeyIdentity

La strategia predefinita. Abbina le righe in base alle colonne della chiave primaria.

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

È la strategia usata da merql per impostazione predefinita quando lo schema della tabella definisce una chiave primaria.

## NaturalKeyIdentity

Per le tabelle senza una chiave primaria a incremento automatico, abbina le righe in base alle colonne dei vincoli univoci. Utile quando la PK è una UUID o quando la tabella usa un vincolo univoco composto come identità logica.

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

Alternativa per le tabelle senza chiave primaria né vincoli univoci. Calcola un hash SHA-256 di tutti i valori delle colonne. Due righe sono uguali se tutte le colonne coincidono.

```php
use Merql\Identity\ContentHashIdentity;

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

L'identità basata sull'hash dei contenuti ha un limite: se il contenuto di una riga cambia, cambia anche il suo hash, quindi la riga appare come eliminazione del vecchio hash e inserimento del nuovo, anziché come aggiornamento. È un compromesso intrinseco quando non ci sono colonne di identità stabili.

## IdentityRule

`IdentityRule` è la configurazione di identità serializzabile usata dall'acquisizione degli snapshot
e dai piani di fusione.

```php
use Merql\Identity\IdentityRule;

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

Tipi di regole:

| Tipo | Significato |
|---|---|
| `primary` | Colonne della chiave primaria del database |
| `natural` | Colonne aziendali univoche e stabili |
| `composite` | Colonne dell'identità logica composta |
| `content` | Alternativa basata sull'hash dei contenuti |

`IdentityRule::forSchema($schema)` determina la regola predefinita:

1. Se la tabella ha una chiave primaria, usa le colonne della chiave primaria.
2. Se la tabella ha chiavi univoche, usa le colonne della prima chiave univoca.
3. Se non è presente nessuna delle due, usa tutte le colonne (alternativa basata sull'hash dei contenuti).

## IdentityRuleSet

Usate `IdentityRuleSet` quando l'applicazione host conosce una regola di identità migliore
rispetto a quella predefinita dello schema.

```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` determina un `IdentityRule` per ogni tabella acquisita. Le
colonne selezionate vengono salvate in `TableSnapshot::identityColumns` e usate per tutte le
successive operazioni di differenza, fusione, piano, SQL e rollback.

`Merql::init()` può ricevere lo stesso insieme di regole quando si usa la facade statica:

```php
use Merql\Merql;

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

Gli insiemi di regole hanno una serializzazione stabile come array:

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

## Identità ambigua

Se due righe acquisite producono la stessa chiave di identità, l'acquisizione fallisce invece di
sovrascrivere una riga con l'altra.

```php
use Merql\Exceptions\SnapshotException;

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

Le applicazioni host possono verificare in anticipo le righe grezze con `IdentityRuleSet::conflictsFor()`
per segnalare eventuali ambiguità prima dell'acquisizione:

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

Per le tabelle meta di WordPress, `post_id + meta_key` è solitamente utile per raggruppare
e visualizzare i dati, ma non è sicuro come identità della riga perché le chiavi meta duplicate sono valide.
Usate la chiave primaria della tabella come identità della riga, a meno che l'applicazione host non possa
dimostrare che la chiave naturale è univoca.

## Codifica della chiave di riga

Le chiavi di riga sono stringhe codificate che combinano più valori di colonna usando un separatore di unità (`\x1F`). I valori che contengono il separatore o il segno di percentuale vengono sottoposti a escape per evitare collisioni:

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

Regole di escape:

| Originale | Con escape |
|---|---|
| `%` | `%25` |
| `\x1F` (separatore di unità) | `%1F` |

In questo modo un valore contenente `\x1F` non viene suddiviso in più parti della chiave. La codifica è deterministica e reversibile.

## Divergenza degli incrementi automatici

Quando entrambi i lati inseriscono nuove righe, gli ID a incremento automatico saranno diversi tra i rami. La riga #100 in ours potrebbe essere un record completamente diverso dalla riga #100 in theirs.

merql gestisce questo caso abbinando le righe in base alla chiave primaria. Se entrambi i lati hanno inserito una riga con lo stesso ID a incremento automatico, viene segnalato un conflitto `insert_insert`. Per evitarlo, usate chiavi naturali (colonne univoche stabili tra i rami) per le tabelle in cui sono previsti inserimenti simultanei.

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

## Identità negli snapshot

Le colonne di identità selezionate durante l'acquisizione vengono salvate con ogni `TableSnapshot`:

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

`SqlGenerator` usa queste colonne per creare le clausole WHERE per le istruzioni UPDATE e DELETE; i piani di fusione le usano per mostrare i valori di identità decodificati durante la revisione, la selezione, l'applicazione con controlli e il rollback.
