
# Zeilenidentität

Die Zeilenidentität legt fest, wie merql Zeilen über Snapshots hinweg zuordnet. Beim Vergleich von Basis und ours muss merql wissen, dass Zeile X in der Basis dieselbe logische Zeile ist wie Zeile Y in ours, auch wenn sich einige Spaltenwerte geändert haben. Die Identitätsregel bestimmt, welche Spalten den Zeilenschlüssel bilden.

```php
use Merql\Identity\PrimaryKeyIdentity;

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

## Die RowIdentity-Schnittstelle

Alle Identitätsstrategien implementieren zwei Methoden:

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

`key()` erstellt aus den Daten einer Zeile einen eindeutigen Zeichenfolgenbezeichner. `columns()` gibt die für die Identität verwendeten Spaltennamen zurück.

## PrimaryKeyIdentity

Die Standardstrategie. Ordnet Zeilen anhand ihrer Primärschlüsselspalten zu.

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

Das verwendet merql standardmäßig, wenn im Tabellenschema ein Primärschlüssel definiert ist.

## NaturalKeyIdentity

Für Tabellen ohne automatisch inkrementierten Primärschlüssel: Ordnet Zeilen anhand von Spalten mit eindeutigen Einschränkungen zu. Nützlich, wenn der PK ein UUID ist oder die Tabelle eine zusammengesetzte Unique-Constraint als logische Identität verwendet.

```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 für Tabellen ohne Primärschlüssel und ohne Unique-Constraints. Berechnet einen SHA-256-Hash aus allen Spaltenwerten. Zwei Zeilen sind identisch, wenn alle Spalten übereinstimmen.

```php
use Merql\Identity\ContentHashIdentity;

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

Die Identität per Inhaltshash hat einen Nachteil: Ändert sich der Inhalt einer Zeile, ändert sich auch ihr Hash. Die Zeile erscheint dann als Löschung des alten Hashes und Einfügung des neuen Hashes statt als Aktualisierung. Das ist ein inhärenter Kompromiss, wenn es keine stabilen Identitätsspalten gibt.

## IdentityRule

`IdentityRule` ist die serialisierbare Identitätskonfiguration, die bei der Snapshot-Erfassung und in Merge-Plänen verwendet wird.

```php
use Merql\Identity\IdentityRule;

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

Regeltypen:

| Typ | Bedeutung |
|---|---|
| `primary` | Primärschlüsselspalten der Datenbank |
| `natural` | Stabile eindeutige Geschäftsspalten |
| `composite` | Zusammengesetzte logische Identitätsspalten |
| `content` | Fallback per Inhaltshash |

`IdentityRule::forSchema($schema)` ermittelt die Standardregel:

1. Hat die Tabelle einen Primärschlüssel, werden die Primärschlüsselspalten verwendet.
2. Hat die Tabelle eindeutige Schlüssel, werden die Spalten des ersten eindeutigen Schlüssels verwendet.
3. Gibt es keines von beiden, werden alle Spalten verwendet (Fallback per Inhaltshash).

## IdentityRuleSet

Verwende `IdentityRuleSet`, wenn eine Hostanwendung eine bessere Identitätsregel kennt als das Schema vorgibt.

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

Der `Snapshotter` löst für jede erfasste Tabelle einen `IdentityRule` auf. Die ausgewählten Spalten werden in `TableSnapshot::identityColumns` gespeichert und für alle nachfolgenden Diff-, Merge-, Plan-, SQL- und Rollback-Vorgänge verwendet.

`Merql::init()` kann bei Verwendung der statischen Fassade denselben Regelsatz erhalten:

```php
use Merql\Merql;

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

Regelsätze werden stabil als Arrays serialisiert:

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

## Mehrdeutige Identität

Wenn zwei erfasste Zeilen denselben Identitätsschlüssel ergeben, schlägt die Erfassung fehl, statt eine Zeile durch eine andere zu überschreiben.

```php
use Merql\Exceptions\SnapshotException;

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

Host-Anwendungen können rohe Zeilen mit `IdentityRuleSet::conflictsFor()` vorab prüfen, wenn sie Mehrdeutigkeiten vor der Erfassung melden möchten:

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

Bei WordPress-Metatabellen eignet sich `post_id + meta_key` meist zum Gruppieren und Anzeigen, aber nicht sicher als Zeilenidentität, da doppelte Meta-Schlüssel zulässig sind. Verwende den Primärschlüssel der Tabelle als Zeilenidentität, es sei denn, die Host-Anwendung kann nachweisen, dass der natürliche Schlüssel eindeutig ist.

## Kodierung von Zeilenschlüsseln

Zeilenschlüssel sind kodierte Zeichenfolgen, die mehrere Spaltenwerte mit einem Einheitentrenner (`\x1F`) verbinden. Werte, die den Trenner oder das Prozentzeichen enthalten, werden maskiert, um Kollisionen zu verhindern:

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

Maskierungsregeln:

| Original | Maskiert |
|---|---|
| `%` | `%25` |
| `\x1F` (Einheitentrenner) | `%1F` |

So wird verhindert, dass ein Wert mit `\x1F` in mehrere Schlüsselbestandteile aufgeteilt wird. Die Kodierung ist deterministisch und umkehrbar.

## Abweichungen bei Auto-Inkrementen

Wenn beide Seiten neue Zeilen einfügen, unterscheiden sich die Auto-Inkrement-IDs zwischen den Branches. Zeile #100 bei uns könnte ein völlig anderer Datensatz sein als Zeile #100 bei ihnen.

merql löst dies durch Abgleich anhand des Primärschlüssels. Wenn beide Seiten eine Zeile mit derselben Auto-Inkrement-ID eingefügt haben, wird dies als `insert_insert`-Konflikt markiert. Verwende für Tabellen, bei denen gleichzeitige Einfügungen zu erwarten sind, natürliche Schlüssel (eindeutige Spalten, die über Branches hinweg stabil sind), um dies zu vermeiden.

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

## Identität in Snapshots

Die bei der Erfassung ausgewählten Identitätsspalten werden mit jedem `TableSnapshot` gespeichert:

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

`SqlGenerator` verwendet diese Spalten, um WHERE-Klauseln für UPDATE- und DELETE-Anweisungen zu erstellen. Merge-Pläne zeigen damit außerdem dekodierte Identitätswerte zur Überprüfung, Auswahl, abgesicherten Anwendung und zum Rollback an.
