
# Идентификатор строки

Идентификатор строки определяет, как merql сопоставляет строки в разных снимках. При сравнении базы с нашим вариантом нужно определить, что строка X в базе соответствует той же логической строке Y в нашем варианте, даже если некоторые значения столбцов изменились. Правило идентификации задает столбцы, из которых формируется ключ строки.

```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` не разделится на несколько частей ключа. Кодирование детерминировано и обратимо.

## Расхождение автоинкрементных идентификаторов

Когда обе стороны добавляют новые строки, их автоинкрементные идентификаторы различаются. Строка #100 в нашем варианте может представлять совершенно другую запись, чем строка #100 в их варианте.

merql решает эту проблему, сопоставляя строки по первичному ключу. Если обе стороны добавили строку с одинаковым автоинкрементным идентификатором, она помечается как конфликт `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, а планы слияния — чтобы показывать декодированные значения идентификатора для проверки, выбора, защищенного применения и отката.
