
# Rivi-identiteetti

Rivi-identiteetti määrittää, miten merql yhdistää rivit tilannevedosten välillä. Kun base-tilannetta verrataan ours-tilanteeseen, sen on tiedettävä, että basen rivi X vastaa samaa loogista riviä kuin rivi Y ours-tilanteessa, vaikka joidenkin sarakkeiden arvot olisivat muuttuneet. Identiteettisääntö määrittää, mitkä sarakkeet muodostavat rivitunnisteen.

```php
use Merql\Identity\PrimaryKeyIdentity;

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

## RowIdentity-rajapinta

Kaikki identiteettistrategiat toteuttavat kaksi metodia:

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

`key()` muodostaa rivin tiedoista yksilöllisen merkkijonotunnisteen. `columns()` palauttaa identiteetissä käytettävien sarakkeiden nimet.

## PrimaryKeyIdentity

Oletusstrategia. Yhdistää rivit niiden pääavainsarakkeiden perusteella.

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

Tätä merql käyttää oletusarvoisesti, kun taulun skeemassa on määritetty pääavain.

## NaturalKeyIdentity

Tauluille, joilla ei ole automaattisesti kasvavaa pääavainta, yhdistää rivit yksilöllisen rajoitteen sarakkeiden perusteella. Hyödyllinen, kun PK on UUID tai kun taulu käyttää yhdistelmällistä yksilöllistä rajoitetta loogisena identiteettinään.

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

Vararatkaisu tauluille, joilla ei ole pääavainta eikä yksilöllisiä rajoitteita. Laskee SHA-256-tiivisteen kaikkien sarakkeiden arvoista. Kaksi riviä ovat samoja, jos kaikki sarakkeet täsmäävät.

```php
use Merql\Identity\ContentHashIdentity;

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

Sisältötiivisteeseen perustuvalla identiteetillä on rajoitus: jos rivin sisältö muuttuu, myös sen tiiviste muuttuu. Siksi rivi näkyy vanhan tiivisteen poistona ja uuden tiivisteen lisäyksenä päivityksen sijaan. Tämä on väistämätön kompromissi, kun vakaita identiteettisarakkeita ei ole.

## IdentityRule

`IdentityRule` on sarjoitettava identiteettimääritys, jota tilannevedosten tallennus ja yhdistämissuunnitelmat käyttävät.

```php
use Merql\Identity\IdentityRule;

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

Sääntötyypit:

| Tyyppi | Merkitys |
|---|---|
| `primary` | Tietokannan pääavainsarakkeet |
| `natural` | Vakaat yksilölliset liiketoimintasarakkeet |
| `composite` | Yhdistelmälliset loogisen identiteetin sarakkeet |
| `content` | Sisältötiivisteeseen perustuva vararatkaisu |

`IdentityRule::forSchema($schema)` ratkaisee oletussäännön:

1. Jos taululla on pääavain, käytetään sen sarakkeita.
2. Jos taululla on yksilöllisiä avaimia, käytetään ensimmäisen yksilöllisen avaimen sarakkeita.
3. Jos kumpaakaan ei ole, käytetään kaikkia sarakkeita (sisältötiivisteeseen perustuva vararatkaisu).

## IdentityRuleSet

Käytä metodia `IdentityRuleSet`, kun isäntäsovellus tuntee skeeman oletusta paremman identiteettisäännön.

```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` ratkaisee jokaiselle tallennetulle taululle `IdentityRule`-arvon. Valitut sarakkeet tallennetaan kohteeseen `TableSnapshot::identityColumns`, ja niitä käytetään kaikissa myöhemmissä erovertailuissa, yhdistämisissä, suunnitelmissa, SQL-toiminnoissa ja palautuksissa.

`Merql::init()` voi saada saman sääntöjoukon, kun käytetään staattista julkisivua:

```php
use Merql\Merql;

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

Sääntöjoukot sarjoitetaan vakaasti taulukkomuodossa:

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

## Moniselitteinen identiteetti

Jos kahdella tallennetulla rivillä on sama identiteettiavain, tallennus epäonnistuu sen sijaan, että toinen rivi korvaisi toisen.

```php
use Merql\Exceptions\SnapshotException;

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

Isäntäsovellukset voivat tarkistaa raakarivien moniselitteisyyden etukäteen metodilla `IdentityRuleSet::conflictsFor()`, jotta siitä voidaan ilmoittaa ennen tallennusta:

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

WordPress-metatauluissa ryhmittelyyn ja näyttämiseen on yleensä hyödyllistä käyttää arvoa `post_id + meta_key`, mutta se ei sovi rivin identiteetiksi, koska päällekkäiset meta-avaimet ovat sallittuja. Käytä rivin identiteettinä taulun pääavainta, ellei isäntäsovellus pysty varmistamaan, että luonnollinen avain on yksilöllinen.

## Rivitunnisteen koodaus

Rivitunnisteet ovat koodattuja merkkijonoja, joissa useita sarakearvoja yhdistää yksikköerotin (`\x1F`). Erottimen tai prosenttimerkin sisältävät arvot suojataan yhteentörmäysten estämiseksi:

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

Suojaussäännöt:

| Alkuperäinen | Suojattu |
|---|---|
| `%` | `%25` |
| `\x1F` (yksikköerotin) | `%1F` |

Näin varmistetaan, ettei arvon `\x1F` sisältävä arvo jakaudu useaksi avaimen osaksi. Koodaus on deterministinen ja palautettavissa alkuperäiseen muotoon.

## Automaattisesti kasvavien tunnisteiden eriytyminen

Kun molemmat puolet lisäävät uusia rivejä, automaattisesti kasvavat tunnisteet eroavat haarojen välillä. Ours-puolen rivi #100 voi olla täysin eri tietue kuin theirs-puolen rivi #100.

merql käsittelee tämän yhdistämällä rivit pääavaimen perusteella. Jos molemmat puolet lisäävät rivin samalla automaattisesti kasvavalla tunnisteella, se merkitään `insert_insert`-ristiriidaksi. Käytä tämän välttämiseksi luonnollisia avaimia (yksilöllisiä sarakkeita, jotka pysyvät samoina haarojen välillä) tauluissa, joihin odotetaan samanaikaisia lisäyksiä.

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

## Identiteetti tilannevedoksissa

Tallennuksen aikana valitut identiteettisarakkeet tallennetaan kuhunkin `TableSnapshot`-kohteeseen:

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

`SqlGenerator` käyttää näitä sarakkeita UPDATE- ja DELETE-lauseiden WHERE-ehtojen muodostamiseen. Yhdistämissuunnitelmat puolestaan näyttävät niiden dekoodatut identiteettiarvot tarkastelua, valintaa, ehdollista käyttöönottoa ja palautusta varten.
