
# Ідентичність рядка

Ідентичність рядка визначає, як merql зіставляє рядки між знімками. Порівнюючи base з ours, потрібно знати, що рядок X у base — це той самий логічний рядок, що й рядок Y в ours, навіть якщо значення деяких стовпців змінилися. Правило ідентичності визначає, які стовпці утворюють ключ рядка.

```php
use Merql\Identity\PrimaryKeyIdentity;

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

## Інтерфейс RowIdentity

Усі стратегії ідентичності реалізують два методи:

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

`key()` створює унікальний рядковий ідентифікатор на основі даних рядка. `columns()` повертає назви стовпців, що використовуються для ідентифікації.

## PrimaryKeyIdentity

Стратегія за замовчуванням. Зіставляє рядки за стовпцями первинного ключа.

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

Саме цю стратегію merql використовує за замовчуванням, якщо схема таблиці визначає первинний ключ.

## NaturalKeyIdentity

Для таблиць без первинного ключа з автоінкрементом зіставляє рядки за стовпцями унікального обмеження. Корисно, коли PK — це UUID або коли логічною ідентичністю таблиці є складене унікальне обмеження.

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

Резервний варіант для таблиць без первинного ключа й унікальних обмежень. Обчислює хеш SHA-256 усіх значень стовпців. Два рядки вважаються однаковими, якщо збігаються всі стовпці.

```php
use Merql\Identity\ContentHashIdentity;

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

Ідентичність на основі хешу вмісту має обмеження: якщо вміст рядка змінюється, змінюється і його хеш, тож рядок позначається як видалення старого хешу та вставлення нового, а не як оновлення. Це неминучий компроміс за відсутності стабільних стовпців ідентичності.

## IdentityRule

`IdentityRule` — це серіалізована конфігурація ідентичності, яку використовують знімання даних і плани злиття.

```php
use Merql\Identity\IdentityRule;

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

Типи правил:

| Тип | Значення |
|---|---|
| `primary` | Стовпці первинного ключа бази даних |
| `natural` | Стабільні унікальні бізнес-стовпці |
| `composite` | Стовпці складеної логічної ідентичності |
| `content` | Резервний варіант із хешем вмісту |

`IdentityRule::forSchema($schema)` визначає правило за замовчуванням:

1. Якщо таблиця має первинний ключ, використовуються його стовпці.
2. Якщо таблиця має унікальні ключі, використовуються стовпці першого з них.
3. Якщо немає ні того, ні іншого, використовуються всі стовпці (резервний варіант із хешем вмісту).

## IdentityRuleSet

Використовуйте `IdentityRuleSet`, коли хост-застосунок знає краще правило ідентичності, ніж те, що визначає схема за замовчуванням.

```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` визначає `IdentityRule` для кожної знятої таблиці. Вибрані стовпці зберігаються в `TableSnapshot::identityColumns` і використовуються під час усіх подальших операцій порівняння відмінностей, злиття, планування, SQL та відкату.

Під час використання статичного фасаду `Merql::init()` може отримувати той самий набір правил:

```php
use Merql\Merql;

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

Набори правил мають стабільну серіалізацію масивів:

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

## Неоднозначна ідентичність

Якщо два зняті рядки мають однаковий ключ ідентичності, знімання даних завершується помилкою, а не перезаписує один рядок іншим.

```php
use Merql\Exceptions\SnapshotException;

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

Хост-застосунки можуть попередньо перевіряти необроблені рядки за допомогою `IdentityRuleSet::conflictsFor()`, щоб виявити неоднозначність до знімання даних:

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

Для метатаблиць WordPress `post_id + meta_key` зазвичай корисний для групування та відображення, але не підходить як ідентичність рядка, оскільки повторювані метаключі допустимі. Для ідентичності рядка використовуйте первинний ключ таблиці, якщо хост-застосунок не може довести унікальність природного ключа.

## Кодування ключа рядка

Ключі рядків — це закодовані рядки, що об’єднують значення кількох стовпців роздільником одиниць (`\x1F`). Значення, що містять роздільник або знак відсотка, екрануються, щоб уникнути колізій:

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

Правила екранування:

| Оригінал | Екрановане значення |
|---|---|
| `%` | `%25` |
| `\x1F` (роздільник одиниць) | `%1F` |

Це гарантує, що значення, яке містить `\x1F`, не розіб’ється на кілька частин ключа. Кодування є детермінованим і зворотним.

## Розбіжність автоінкременту

Коли обидві сторони додають нові рядки, ID автоінкременту відрізнятимуться між гілками. Рядок #100 в ours може бути зовсім іншим записом, ніж рядок #100 у theirs.

merql вирішує це зіставленням за первинним ключем. Якщо обидві сторони додали рядок з однаковим ID автоінкременту, його позначено як конфлікт `insert_insert`. Щоб уникнути цього, використовуйте природні ключі (унікальні стовпці, стабільні між гілками) для таблиць, у які очікується паралельне додавання записів.

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

## Ідентичність у знімках

Стовпці ідентичності, вибрані під час знімання даних, зберігаються разом із кожним `TableSnapshot`:

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

`SqlGenerator` використовує ці стовпці для створення умов WHERE в операторах UPDATE і DELETE, а плани злиття — щоб показувати декодовані значення ідентичності для перевірки, вибору, контрольованого застосування та відкату.
