
# Identidad de fila

La identidad de fila determina cómo merql empareja filas entre instantáneas. Al comparar la base con la nuestra, necesita saber que la fila X de la base es la misma fila lógica que la fila Y de la nuestra, aunque hayan cambiado algunos valores de columna. La regla de identidad determina qué columnas forman la clave de fila.

```php
use Merql\Identity\PrimaryKeyIdentity;

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

## La interfaz RowIdentity

Todas las estrategias de identidad implementan dos métodos:

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

`key()` crea un identificador de texto único a partir de los datos de una fila. `columns()` devuelve los nombres de las columnas utilizadas para la identidad.

## PrimaryKeyIdentity

La estrategia predeterminada. Empareja las filas mediante sus columnas de clave 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)
```

Es lo que merql usa de forma predeterminada cuando el esquema de la tabla define una clave primaria.

## NaturalKeyIdentity

En tablas sin una clave primaria autoincremental, empareja las filas mediante las columnas con restricciones de unicidad. Es útil cuando la PK es un UUID o cuando la tabla usa una restricción de unicidad compuesta como identidad lógica.

```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 para tablas sin clave primaria ni restricciones de unicidad. Calcula un hash SHA-256 de los valores de todas las columnas. Dos filas son iguales si coinciden todas las columnas.

```php
use Merql\Identity\ContentHashIdentity;

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

La identidad basada en el hash del contenido tiene una limitación: si cambia el contenido de una fila, también cambia su hash. Por tanto, la fila aparece como una eliminación del hash anterior y una inserción del nuevo, en vez de como una actualización. Es una contrapartida inevitable cuando no hay columnas de identidad estables.

## IdentityRule

`IdentityRule` es la configuración de identidad serializable que usan la captura de instantáneas y los planes de combinación.

```php
use Merql\Identity\IdentityRule;

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

Tipos de reglas:

| Tipo | Significado |
|---|---|
| `primary` | Columnas de clave primaria de la base de datos |
| `natural` | Columnas de negocio únicas y estables |
| `composite` | Columnas de identidad lógica compuesta |
| `content` | Alternativa basada en el hash del contenido |

`IdentityRule::forSchema($schema)` resuelve la regla predeterminada:

1. Si la tabla tiene una clave primaria, usa sus columnas.
2. Si la tabla tiene claves únicas, usa las columnas de la primera.
3. Si no tiene ninguna, usa todas las columnas (alternativa basada en el hash del contenido).

## IdentityRuleSet

Use `IdentityRuleSet` cuando la aplicación anfitriona conozca una regla de identidad mejor que la predeterminada del esquema.

```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` resuelve un `IdentityRule` para cada tabla capturada. Las columnas elegidas se guardan en `TableSnapshot::identityColumns` y se usan en todas las operaciones posteriores de comparación, combinación, planificación, SQL y reversión.

`Merql::init()` puede recibir el mismo conjunto de reglas al usar la fachada estática:

```php
use Merql\Merql;

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

Los conjuntos de reglas tienen una serialización de matriz estable:

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

## Identidad ambigua

Si dos filas capturadas generan la misma clave de identidad, la captura falla en lugar de sobrescribir una fila con la otra.

```php
use Merql\Exceptions\SnapshotException;

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

Las aplicaciones anfitrionas pueden comprobar previamente las filas sin procesar con `IdentityRuleSet::conflictsFor()` para informar de una ambigüedad antes de la captura:

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

Para las tablas meta de WordPress, `post_id + meta_key` suele ser útil para agrupar y mostrar datos, pero no es segura como identidad de fila porque se permiten claves meta duplicadas. Use la clave primaria de la tabla como identidad de fila, salvo que la aplicación anfitriona pueda demostrar que la clave natural es única.

## Codificación de la clave de fila

Las claves de fila son cadenas codificadas que combinan varios valores de columna mediante un separador de unidad (`\x1F`). Para evitar colisiones, se escapan los valores que contienen el separador o el signo de porcentaje:

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

Reglas de escape:

| Original | Escapado |
|---|---|
| `%` | `%25` |
| `\x1F` (separador de unidad) | `%1F` |

Esto garantiza que un valor que contiene `\x1F` no se divida en varias partes de la clave. La codificación es determinista y reversible.

## Divergencia de autoincrementos

Cuando ambas partes insertan filas nuevas, los ID autoincrementales serán distintos entre ramas. La fila #100 de la nuestra podría ser un registro completamente distinto de la fila #100 de la suya.

merql resuelve esto emparejando por clave primaria. Si ambas partes insertan una fila con el mismo ID autoincremental, se marca como conflicto `insert_insert`. Para evitarlo, use claves naturales (columnas únicas que se mantienen estables entre ramas) en las tablas donde se prevean inserciones simultáneas.

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

## Identidad en las instantáneas

Las columnas de identidad elegidas durante la captura se guardan con cada `TableSnapshot`:

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

`SqlGenerator` usa estas columnas para crear cláusulas WHERE en las instrucciones UPDATE y DELETE, y los planes de combinación las usan para mostrar los valores de identidad decodificados al revisar, seleccionar, aplicar con protecciones y revertir.
