
# Справочник PHP API

Полный справочник всех общедоступных классов и методов в merql.

## Merql (фасад)

`Merql\Merql` — статическая точка входа для типовых операций. Она оборачивает низкоуровневые классы и автоматически управляет сохранением снимков.

```php
use Merql\Connection;
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;
use Merql\Merql;

$connection = Connection::sqlite(':memory:');
Merql::init($connection, identityRules: new IdentityRuleSet([
    'options' => IdentityRule::natural(['option_name']),
]));
```

### `init(DatabaseConnection $connection, ?Driver $driver = null, ?IdentityRuleSet $identityRules = null): void`

Инициализирует фасад подключением к базе данных. Необходимо вызвать до `snapshot()`, `apply()` или `applyGuarded()`; вызовите ещё раз, чтобы переключить базу данных, например, для создания снимков нашей и другой сторон из двух разных копий. Если не передан `$driver`, метод вызывает `DriverFactory::create()`, который выдаёт исключение для движков, отличных от MySQL и SQLite, если не зарегистрирован драйвер. Передайте драйвер, чтобы переопределить автоматическое определение, или набор правил идентификации, чтобы переопределить идентификацию строк для каждой таблицы при создании снимка.

### `snapshot(string $name, array $tables = []): Snapshot`

Сохраняет текущее состояние базы данных на диске. Передайте массив имён таблиц, чтобы создать снимки только указанных таблиц, или опустите аргумент для всех таблиц.

```php
$snapshot = Merql::snapshot('baseline');
$snapshot = Merql::snapshot('baseline', ['posts', 'users']);
```

### `diff(string $base, string $current): Changeset`

Загружает два именованных снимка и вычисляет набор изменений между ними. `init()` не требуется, поскольку метод использует сохранённые снимки.

```php
$changeset = Merql::diff('before', 'after');
```

### `merge(string $base, string $ours, string $theirs): MergeResult`

Трёхстороннее слияние трёх именованных снимков. Возвращает результат с чистыми операциями и всеми конфликтами. Слияние выполняется на уровне столбцов: фасад формирует `ThreeWayMerge` без `CellMergeConfig`, поэтому слияние на уровне ячеек не применяется. Для этого используйте напрямую [`ThreeWayMerge`](#threewaymerge).

```php
$result = Merql::merge('base', 'ours', 'theirs');
```

### `patch(string $base, string $changes): MergeResult`

Двустороннее слияние: применяет изменения к исходной версии. Эквивалентно `merge($base, $base, $changes)`. Всегда даёт результат без конфликтов, поскольку одновременных правок нет. Только на уровне столбцов, как и `merge()`.

```php
$result = Merql::patch('base', 'changes');
```

### `apply(MergeResult $result): ApplyResult`

Выполняет результат слияния без конфликтов для базы данных, переданной в `init()`. Требуется `init()`. Выдаёт исключение `ConflictException`, если остались неразрешённые конфликты.

Выполняются все операции результата, поэтому полный результат `merge()` корректно применяется только к базе данных, в которой всё ещё хранится исходное состояние. Если база данных уже содержит одну из сторон, примените только отсутствующие в ней операции; см. [Применение с проверками](https://merql.inline0.dev/ru/docs/apply/guarded).

```php
$applied = Merql::apply($result);
echo $applied->rowsAffected();
```

### `plan(string $id, string $base, string $ours, string $theirs, array $metadata = []): MergePlan`

Создаёт план слияния для UI на основе трёх именованных снимков. План включает стабильные ID операций, ID групп изменений, контекст операций, конфликты, несоответствия схем, сводку, метаданные и хеш плана. Слияние выполняется на уровне столбцов, как в `merge()`; передайте результат с объединёнными ячейками в `MergePlanBuilder::build()` для слияния на уровне ячеек.

```php
$plan = Merql::plan('plan-1', 'base', 'ours', 'theirs', [
    'source' => 'review-ui',
]);
```

### `applyGuarded(MergeResult $result, Snapshot $expectedLive): ApplyResult`

Применяет результат слияния без конфликтов с оптимистической проверкой предварительных условий для строк в базе данных. Для обновления и удаления необходимо, чтобы текущие строки соответствовали `$expectedLive`; для вставки целевой идентификатор не должен существовать. `$expectedLive` — это снимок целевой базы данных (другой стороны, если наша версия переносится в базу, из которой был создан снимок другой стороны). Если какой-либо запрос затрагивает ноль строк, транзакция откатывается.

```php
use Merql\Snapshot\SnapshotStore;

$applied = Merql::applyGuarded($selectedResult, SnapshotStore::load('theirs'));
```

### `reset(): void`

Очищает внутренние экземпляры подключения и Snapshotter. В основном используется в тестах для сброса состояния между запусками.

---

## Подключение

`Merql\Connection` создаёт адаптеры подключения с разумными настройками по умолчанию. При ошибке PDO-подключения выдают исключения, используют ассоциативный режим получения данных, отключают эмуляцию подготовленных выражений и возвращают значения в виде строк. Адаптер mysqli приводит полученные значения к тому же формату: строка или null.

### `mysql(string $host, string $database, string $username, string $password = '', int $port = 3306, string $charset = 'utf8mb4'): PdoDatabaseConnection`

```php
use Merql\Connection;

$connection = Connection::mysql('127.0.0.1', 'my_app', 'root', 'secret');
```

### `mysqli(string $host, string $database, string $username, string $password = '', int $port = 3306, string $charset = 'utf8mb4'): MysqliDatabaseConnection`

```php
$connection = Connection::mysqli('127.0.0.1', 'my_app', 'root', 'secret');
```

### `sqlite(string $path = ':memory:'): PdoDatabaseConnection`

Создаёт подключение SQLite. Автоматически включает внешние ключи с помощью `PRAGMA foreign_keys = ON`.

```php
$connection = Connection::sqlite('/var/data/app.sqlite');
$connection = Connection::sqlite(':memory:');
```

### `fromDsn(string $dsn, string $username = '', string $password = ''): PdoDatabaseConnection`

Подключается по любой строке DSN для PDO. В merql есть драйверы только для `mysql` и `sqlite`; для других DSN подключение устанавливается, но `Merql::init()`, `Snapshotter` и средства применения выдают `Unsupported database driver`, если драйвер не зарегистрирован через `DriverFactory::register()`.

```php
$connection = Connection::fromDsn('mysql:host=localhost;dbname=app', 'root', 'pass');
```

### `fromPdo(PDO $pdo): PdoDatabaseConnection`

Оборачивает существующий экземпляр PDO.

### `fromMysqli(mysqli $mysqli, string $charset = 'utf8mb4'): MysqliDatabaseConnection`

Оборачивает существующий экземпляр mysqli.

### Контракт адаптера

Все адаптеры реализуют `Merql\Database\DatabaseConnection`:

```php
interface DatabaseConnection
{
    public function driverName(): string;
    public function execute(string $sql, array $params = []): int;
    public function query(string $sql, array $params = []): array;
    public function scalar(string $sql, array $params = []): ?string;
    public function beginTransaction(): void;
    public function commit(): void;
    public function rollBack(): void;
    public function lastInsertId(): string;
}
```

`execute()`, `query()` и `scalar()` принимают позиционные параметры `list<scalar|null>` для заполнителей `?`. `query()` возвращает `list<array<string, scalar|null>>`; значения в адаптерах PDO и mysqli приводятся к одному формату — строка или null.

---

## Создатель снимков

`Merql\Snapshot\Snapshotter` создаёт снимки состояния базы данных. В отличие от фасада, он даёт полный контроль над фильтрами и не сохраняет данные автоматически.

### `__construct(DatabaseConnection $connection, ?Driver $driver = null, ?IdentityRuleSet $identityRules = null)`

Если драйвер не указан, он определяется автоматически по идентификатору подключения. Передайте `IdentityRuleSet`, если правила идентификации строк для отдельных таблиц должны переопределять значения по умолчанию из схемы.

### `capture(string $name, array $tables = [], array $filters = []): Snapshot`

Создаёт снимок текущего состояния. Передайте имена таблиц, чтобы ограничить область действия. Передайте объекты фильтров, чтобы исключить таблицы, столбцы или строки.

```php
use Merql\Snapshot\Snapshotter;
use Merql\Filter\TableFilter;
use Merql\Filter\ColumnFilter;
use Merql\Filter\RowFilter;
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;

$snapshotter = new Snapshotter(
    $connection,
    identityRules: new IdentityRuleSet([
        'wp_options' => IdentityRule::natural(['option_name']),
    ]),
);

// All tables.
$snapshot = $snapshotter->capture('full');

// Specific tables with filters.
$snapshot = $snapshotter->capture('filtered', ['posts', 'users'], [
    TableFilter::exclude(['cache_*', 'sessions']),
    ColumnFilter::ignore(['updated_at', 'created_at']),
    RowFilter::create(fn(string $table, array $row) => $row['status'] !== 'trash'),
]);
```

### `captureAliased(string $name, array $physicalToCanonicalTableMap, array $filters = []): Snapshot`

Создаёт снимки физических таблиц, используя канонические имена таблиц. Используйте это, если одна и та же логическая таблица имеет разные физические имена в разных средах.

```php
$snapshot = $snapshotter->captureAliased('sandbox', [
    'wp_onumia_abcd_posts' => 'wp_posts',
    'wp_onumia_abcd_postmeta' => 'wp_postmeta',
]);
```

В результирующем снимке сохраняются канонические имена таблиц и физическая схема таблиц, настроенные столбцы идентификации, ключи строк и фильтры.

### `fromData(string $name, array $tableData): Snapshot` (статический метод)

Создаёт снимок из исходных данных без подключения к базе данных. Удобно для тестирования или импорта данных из другого источника.

```php
use Merql\Snapshot\Snapshotter;
use Merql\Snapshot\TableSnapshotData;
use Merql\Schema\TableSchema;

$schema = new TableSchema('posts', ['id' => 'int', 'title' => 'varchar'], ['id']);

$snapshot = Snapshotter::fromData('test', [
    'posts' => new TableSnapshotData($schema, [
        ['id' => '1', 'title' => 'Hello'],
        ['id' => '2', 'title' => 'World'],
    ], ['id']),
]);
```

---

## Снимок

`Merql\Snapshot\Snapshot` — неизменяемый объект-значение, представляющий полное состояние базы данных.

```php
$snapshot->name;                    // string: snapshot identifier
$snapshot->tables;                  // array<string, TableSnapshot>
$snapshot->tableNames();            // list<string>
$snapshot->hasTable('posts');       // bool
$snapshot->getTable('posts');       // ?TableSnapshot
```

---

## TableSnapshot

`Merql\Snapshot\TableSnapshot` хранит строки, контрольные отпечатки и схему одной таблицы.

```php
$table = $snapshot->getTable('posts');

$table->schema;                    // TableSchema
$table->identityColumns;           // list<string>: columns used for row identity
$table->fingerprints;              // array<string, string>: row key to hash
$table->rows;                      // array<string, array<string, mixed>>

$table->rowKeys();                 // list<string>
$table->rowCount();                // int
$table->hasRow('42');              // bool
$table->getRow('42');              // ?array<string, mixed>
$table->getFingerprint('42');      // ?string
```

---

## TableSnapshotData

`Merql\Snapshot\TableSnapshotData` — входные данные для `Snapshotter::fromData()`.

```php
use Merql\Snapshot\TableSnapshotData;
use Merql\Schema\TableSchema;

$data = new TableSnapshotData(
    schema: new TableSchema('posts', ['id' => 'int', 'title' => 'varchar'], ['id']),
    rows: [
        ['id' => '1', 'title' => 'First Post'],
    ],
    identityColumns: ['id'],
);
```

---

## IdentityRule

`Merql\Identity\IdentityRule` определяет столбцы, используемые для создания ключей строк.

```php
use Merql\Identity\IdentityRule;

$primary = IdentityRule::primary(['id']);
$natural = IdentityRule::natural(['option_name']);
$composite = IdentityRule::composite(['object_id', 'term_taxonomy_id']);
$content = IdentityRule::content(['name', 'value']);

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

### `forSchema(TableSchema $schema): IdentityRule` (статический метод)

Определяет правило идентификации по умолчанию на основе схемы: первичный ключ, первый уникальный ключ, затем резервный вариант на основе содержимого.

### `toArray(): array`

Сериализует правило в формате `['type' => string, 'columns' => list<string>]`.

### `fromArray(array $data): IdentityRule` (статический метод)

Восстанавливает правило из сериализованных данных.

---

## IdentityRuleSet

`Merql\Identity\IdentityRuleSet` хранит переопределения идентификации для отдельных таблиц.

```php
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;

$rules = new IdentityRuleSet([
    'wp_options' => IdentityRule::natural(['option_name']),
]);

$rule = $rules->ruleFor($schema);
```

### `with(string $table, IdentityRule $rule): IdentityRuleSet`

Возвращает новый набор правил с переопределением для одной таблицы.

### `conflictsFor(string $table, IdentityRule $rule, array $rows): array`

Возвращает записи `IdentityConflict`, когда несколько строк дают одинаковый ключ по заданному правилу.

### `toArray(): array`

Сериализует правила таблиц в массив, безопасный для JSON.

### `fromArray(array $data): IdentityRuleSet` (статический метод)

Восстанавливает набор правил из сериализованных данных.

---

## SnapshotStore

`Merql\Snapshot\SnapshotStore` сохраняет и загружает снимки в виде файлов JSON.

### `setDirectory(string $directory): void`

Задаёт каталог для хранения. По умолчанию используется `.merql/snapshots`.

### `save(Snapshot $snapshot): void`

Записывает снимок на диск.

### `load(string $name): Snapshot`

Загружает снимок по имени. Если снимок не найден, выдаёт `SnapshotException`.

### `exists(string $name): bool`

Проверяет, существует ли файл снимка.

### `delete(string $name): void`

Удаляет файл снимка.

---

## Сравниватель

`Merql\Diff\Differ` сравнивает два снимка и формирует набор изменений.

### `diff(Snapshot $base, Snapshot $current): Changeset`

```php
use Merql\Diff\Differ;

$differ = new Differ();
$changeset = $differ->diff($baseSnapshot, $currentSnapshot);
```

### `valuesEqual(mixed $a, mixed $b): bool` (статический метод)

Сравнивает два значения после преобразования в строки. NULL считается отдельным значением: `NULL === NULL` — истина, `NULL === ''` — ложь.

---

## Набор изменений

`Merql\Diff\Changeset` — неизменяемая коллекция операций на уровне строк.

```php
$changeset->inserts();             // list<RowInsert>
$changeset->updates();             // list<RowUpdate>
$changeset->deletes();             // list<RowDelete>
$changeset->isEmpty();             // bool
$changeset->count();               // int: total operations
$changeset->forTable('posts');     // Changeset: filtered to one table
```

---

## RowInsert, RowUpdate, RowDelete

Неизменяемые объекты-значения, представляющие изменения отдельных строк.

### RowInsert

```php
$insert->table;                    // string
$insert->rowKey;                   // string
$insert->values;                   // array<string, mixed>
```

### RowUpdate

```php
$update->table;                    // string
$update->rowKey;                   // string
$update->columnDiffs;              // list<ColumnDiff>
$update->fullRow;                  // array<string, mixed>: complete current row
$update->columnDiffMap();          // array<string, ColumnDiff>: keyed by column name
```

### RowDelete

```php
$delete->table;                    // string
$delete->rowKey;                   // string
$delete->oldValues;                // array<string, mixed>: values before deletion
```

---

## ColumnDiff

`Merql\Diff\ColumnDiff` представляет изменение одного столбца при обновлении строки.

```php
$diff->column;                     // string
$diff->oldValue;                   // mixed
$diff->newValue;                   // mixed
```

---

## ThreeWayMerge

`Merql\Merge\ThreeWayMerge` — основной алгоритм слияния.

### `__construct(?CellMergeConfig $cellMergeConfig = null)`

Передайте `CellMergeConfig`, чтобы включить слияние на уровне ячеек для столбцов TEXT и JSON.

```php
use Merql\Merge\ThreeWayMerge;
use Merql\CellMerge\CellMergeConfig;

// Default: column-level merge only.
$merge = new ThreeWayMerge();

// With cell-level merge for TEXT and JSON columns.
$merge = new ThreeWayMerge(CellMergeConfig::auto());
```

### `merge(Snapshot $base, Snapshot $ours, Snapshot $theirs): MergeResult`

Выполняет трёхстороннее слияние. Внутри вычисляет наборы изменений (от исходной версии к нашей и от исходной версии к другой) и объединяет их.

```php
$result = $merge->merge($baseSnapshot, $oursSnapshot, $theirsSnapshot);
```

### `patch(Snapshot $base, Snapshot $changes): MergeResult`

Сокращённый вариант двустороннего слияния. Эквивалентно `merge($base, $base, $changes)`.

### `schemaMismatches(): array`

Возвращает `SchemaException[]` для всех различий схемы, обнаруженных при последнем слиянии (столбцы добавлены или удалены между снимками).

---

## MergeResult

`Merql\Merge\MergeResult` — неизменяемый объект-значение с результатом слияния.

```php
$result->isClean();                // bool: no conflicts
$result->operations();             // list<MergeOperation>
$result->conflicts();              // list<Conflict>
$result->operationCount();         // int
$result->conflictCount();          // int
$result->baseSnapshot();           // ?Snapshot
$result->schemaMismatches();       // list<SchemaException>
$result->hasSchemaMismatches();    // bool
```

---

## MergePlan

`Merql\Plan\MergePlan` — сериализуемая модель для проверки слияния.

```php
$plan->id;                        // string
$plan->baseSnapshot;              // string
$plan->oursSnapshot;              // string
$plan->theirsSnapshot;            // string
$plan->summary;                   // MergePlanSummary
$plan->changeGroups;              // list<MergePlanChangeGroup>
$plan->operations;                // list<MergePlanOperation>
$plan->conflicts;                 // list<MergePlanConflict>
$plan->schemaMismatches;          // list<string>
$plan->hash;                      // string
$plan->metadata;                  // array<string, mixed>
```

Используйте `MergePlanSerializer::toJson()` и `::fromJson()` для стабильных циклов преобразования JSON.

## Выбор операций

`ChangeGroupSelection` хранит ID подготовленных групп изменений. Преобразуйте его в `OperationSelection` при создании результатов слияния выбранных изменений или планов отката.

```php
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;

$selection = ChangeGroupSelection::fromIds([$groupId]);
$operationSelection = $selection->toOperationSelection($plan);

$selected = (new SelectedMergeResultFactory())
    ->fromOperationSelection($plan, $operationSelection);
```

## RollbackPlan

`Merql\Rollback\RollbackPlan` хранит обратные операции и ожидаемые расхождения для выборочного применения.

```php
use Merql\Rollback\RollbackPlanBuilder;
use Merql\Rollback\RollbackPlanSerializer;

$rollback = (new RollbackPlanBuilder())->build(
    'rollback-1',
    $plan,
    $operationSelection,
    $liveBeforeRows,
);

$json = RollbackPlanSerializer::toJson($rollback);
```

Используйте `RollbackDrift` перед применением отката, а затем `RollbackPlanApplier` для выполнения обратных операций.

---

## MergeOperation

`Merql\Merge\MergeOperation` — одна разрешённая операция слияния.

```php
$op->type;                         // string: 'insert' | 'update' | 'delete'
$op->table;                        // string
$op->rowKey;                       // string
$op->values;                       // array<string, mixed>
$op->source;                       // string: 'ours' | 'theirs' | 'merged'
```

Константы типов:

```php
MergeOperation::TYPE_INSERT;       // 'insert'
MergeOperation::TYPE_UPDATE;       // 'update'
MergeOperation::TYPE_DELETE;       // 'delete'
```

---

## Конфликт

`Merql\Merge\Conflict` представляет неразрешённый конфликт слияния.

```php
$conflict->table();                // string
$conflict->rowKey();               // string
$conflict->type();                 // string: 'update_update' | 'update_delete' | 'delete_update' | 'insert_insert'
$conflict->column();               // ?string (null for structural conflicts)
$conflict->oursValue();            // mixed
$conflict->theirsValue();          // mixed
$conflict->baseValue();            // mixed
$conflict->primaryKey(['id']);     // array<string, string>: decoded from rowKey
```

### Типы конфликтов

| Тип | Значение |
|---|---|
| `update_update` | Обе стороны изменили один столбец на разные значения |
| `update_delete` | Наша сторона обновила строку, удалённую другой стороной |
| `delete_update` | Наша сторона удалила строку, обновлённую другой стороной |
| `insert_insert` | Обе стороны добавили строку с одинаковым первичным ключом |

---

## ConflictResolver

`Merql\Merge\ConflictResolver` применяет политику разрешения ко всем конфликтам в результате слияния.

### `resolve(MergeResult $result, ConflictPolicy $policy): MergeResult` (статический метод)

Возвращает новый `MergeResult` с конфликтами, разрешёнными согласно политике.

```php
use Merql\Merge\ConflictResolver;
use Merql\Merge\ConflictPolicy;

// Accept all "theirs" values when conflicts arise.
$resolved = ConflictResolver::resolve($result, ConflictPolicy::TheirsWins);

// Accept all "ours" values.
$resolved = ConflictResolver::resolve($result, ConflictPolicy::OursWins);

// Return unchanged (for manual inspection).
$resolved = ConflictResolver::resolve($result, ConflictPolicy::Manual);
```

---

## ConflictPolicy

`Merql\Merge\ConflictPolicy` — перечисление с тремя вариантами.

```php
use Merql\Merge\ConflictPolicy;

ConflictPolicy::OursWins;          // Resolve all conflicts in favor of "ours"
ConflictPolicy::TheirsWins;        // Resolve all conflicts in favor of "theirs"
ConflictPolicy::Manual;            // Leave conflicts unresolved for manual handling
```

---

## ColumnMerge

`Merql\Merge\ColumnMerge` выполняет логику слияния отдельных столбцов одной строки. Используется внутри `ThreeWayMerge`.

### `merge(string $table, string $rowKey, array $base, array $ours, array $theirs, ?CellMergeConfig $cellMergeConfig = null, array $columnTypes = []): array` (статический метод)

Возвращает `['values' => array, 'conflicts' => list<Conflict>]`.

```php
use Merql\Merge\ColumnMerge;

$result = ColumnMerge::merge(
    'posts',
    '42',
    ['title' => 'Hello', 'status' => 'draft'],     // base
    ['title' => 'Hello', 'status' => 'published'],  // ours
    ['title' => 'New Title', 'status' => 'draft'],   // theirs
);

// $result['values'] = ['title' => 'New Title', 'status' => 'published']
// $result['conflicts'] = [] (clean merge)
```

---

## Применитель

`Merql\Apply\Applier` применяет результат слияния к базе данных.

### `__construct(DatabaseConnection $connection, ?Driver $driver = null)`

### `apply(MergeResult $result, ?Snapshot $base = null): ApplyResult`

Выполняет все операции в одной транзакции. При любой ошибке откатывает транзакцию и указывает ошибку в `ApplyResult::errors()` вместо того, чтобы выдавать исключение. Выдаёт исключение `ConflictException`, если в результате остались неразрешённые конфликты.

```php
use Merql\Apply\Applier;

$applier = new Applier($connection);
$applied = $applier->apply($result);

echo "Rows affected: {$applied->rowsAffected()}\n";
if ($applied->hasErrors()) {
    print_r($applied->errors());
}
```

Порядок внешних ключей обрабатывается автоматически: вставки сортируются в порядке зависимостей, удаления — в обратном порядке зависимостей.

---

## ApplyResult

`Merql\Apply\ApplyResult` — неизменяемый объект-значение, возвращаемый средством применения.

```php
$applied->rowsAffected();         // int
$applied->errors();               // list<string>
$applied->hasErrors();            // bool
```

После отката применения `rowsAffected()` по-прежнему учитывает запросы, выполненные до сбоя ([#4](https://github.com/inline0/merql/issues/4)). Прежде чем полагаться на это число, проверьте `hasErrors()`.

---

## DryRun

`Merql\Apply\DryRun` формирует понятные человеку запросы SQL, не выполняя их.

### `generate(MergeResult $result, ?Snapshot $base = null, array $fkDependencies = [], ?Driver $driver = null): array` (статический метод)

Возвращает список строк SQL со встроенными значениями (только для отображения).

```php
use Merql\Apply\DryRun;

$statements = DryRun::generate($result);

foreach ($statements as $sql) {
    echo "{$sql};\n";
}

// Output:
// INSERT INTO `posts` (`id`, `title`) VALUES ('10', 'New Post');
// UPDATE `posts` SET `title` = 'Updated' WHERE `id` = '5';
// DELETE FROM `comments` WHERE `id` = '3';
```

---

## SqlGenerator

`Merql\Apply\SqlGenerator` формирует параметризованные запросы SQL. Используется внутри обоих средств: `Applier` и `DryRun`.

### `generate(MergeResult $result, ?Snapshot $base = null, array $fkDependencies = [], ?Driver $driver = null): array` (статический метод)

Возвращает `list<array{sql: string, params: list<scalar|null>}>`.

```php
use Merql\Apply\SqlGenerator;

$statements = SqlGenerator::generate($result);

foreach ($statements as $stmt) {
    $connection->execute($stmt['sql'], $stmt['params']);
}
```

---

## CellMergeConfig

`Merql\CellMerge\CellMergeConfig` настраивает стратегии слияния на уровне ячеек для отдельных столбцов или типов столбцов.

### `auto(): self` (статический метод)

Фабричный метод для удобства: назначает `TextCellMerger` столбцам TEXT/LONGTEXT и `JsonCellMerger` столбцам JSON.

```php
use Merql\CellMerge\CellMergeConfig;

$config = CellMergeConfig::auto();
```

### `forColumn(string $column, CellMerger $merger): self`

Назначает средство слияния конкретному столбцу. Используйте `table.column` для полных имён или только `column` для всех таблиц.

```php
use Merql\CellMerge\CellMergeConfig;
use Merql\CellMerge\TextCellMerger;

$config = (new CellMergeConfig())
    ->forColumn('posts.content', new TextCellMerger())
    ->forColumn('description', new TextCellMerger());
```

### `forType(string $typePattern, CellMerger $merger): self`

Назначает средство слияния всем столбцам, соответствующим шаблону типа (поиск подстроки без учёта регистра).

```php
use Merql\CellMerge\CellMergeConfig;
use Merql\CellMerge\JsonCellMerger;

$config = (new CellMergeConfig())
    ->forType('json', new JsonCellMerger());
```

### `getMerger(string $table, string $column, string $columnType = ''): CellMerger`

Находит средство слияния для указанной таблицы, столбца и типа. Приоритет: полное имя столбца, имя столбца без таблицы, шаблон типа, значение по умолчанию (непрозрачное).

---

## CellMerger (интерфейс)

`Merql\CellMerge\CellMerger` — интерфейс для стратегий слияния на уровне ячеек.

```php
use Merql\CellMerge\CellMerger;
use Merql\CellMerge\CellMergeResult;

interface CellMerger
{
    public function merge(mixed $base, mixed $ours, mixed $theirs): CellMergeResult;
}
```

Предусмотрены три встроенные реализации.

### TextCellMerger

`Merql\CellMerge\TextCellMerger` объединяет многострочный текст с помощью трёхстороннего построчного сравнения (того же алгоритма, который git использует для содержимого файлов). Если стороны редактируют разные строки, слияние проходит без конфликтов. Если обе стороны по-разному изменяют одну строку, сообщается о конфликте.

```php
use Merql\CellMerge\TextCellMerger;

$merger = new TextCellMerger();
$result = $merger->merge(
    "line 1\nline 2\nline 3",
    "line 1\nline 2 modified\nline 3",
    "line 1\nline 2\nline 3 modified",
);
// $result->clean === true
// $result->value === "line 1\nline 2 modified\nline 3 modified"
```

### JsonCellMerger

`Merql\CellMerge\JsonCellMerger` объединяет значения JSON, сравнивая ключи верхнего уровня независимо друг от друга. Каждый ключ объединяется по трехсторонней логике. Вложенные объекты рассматриваются как непрозрачные строки JSON (без рекурсивного объединения).

```php
use Merql\CellMerge\JsonCellMerger;

$merger = new JsonCellMerger();
$result = $merger->merge(
    '{"a": 1, "b": 2}',
    '{"a": 10, "b": 2}',        // changed "a"
    '{"a": 1, "b": 20}',         // changed "b"
);
// $result->clean === true
// $result->value === '{"a":10,"b":20}'
```

### OpaqueCellMerger

`Merql\CellMerge\OpaqueCellMerger` используется по умолчанию. Он рассматривает значения как непрозрачные строки и всегда сообщает о конфликте, если они различаются.

---

## CellMergeResult

`Merql\CellMerge\CellMergeResult` — тип возвращаемого значения всех объединителей ячеек.

```php
$result->clean;                    // bool: true if merge succeeded
$result->value;                    // mixed: merged value (or ours on conflict)
$result->conflicts;                // int: number of conflicts

// Factory methods.
CellMergeResult::resolved($value);           // clean merge
CellMergeResult::conflict($oursValue, $n);   // conflict with count
```

---

## TableSchema

`Merql\Schema\TableSchema` — представление структуры таблиц только для чтения.

```php
use Merql\Schema\TableSchema;

$schema = new TableSchema(
    name: 'posts',
    columns: ['id' => 'int', 'title' => 'varchar(255)', 'content' => 'text'],
    primaryKey: ['id'],
    uniqueKeys: [['slug']],
);

$schema->name;                     // string
$schema->columns;                  // array<string, string>: name to type
$schema->primaryKey;               // list<string>
$schema->uniqueKeys;               // list<list<string>>
$schema->hasPrimaryKey();          // bool
$schema->hasUniqueKeys();          // bool
$schema->columnNames();            // list<string>
```

---

## Система драйверов

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

### Driver (интерфейс)

`Merql\Driver\Driver` задает контракт, который реализуют все драйверы баз данных.

```php
interface Driver
{
    public function quoteIdentifier(string $name): string;
    public function listTables(DatabaseConnection $connection): array;
    public function readSchema(DatabaseConnection $connection, string $table): TableSchema;
    public function readForeignKeys(DatabaseConnection $connection): array;
    public function selectAll(string $table): string;
}
```

Встроенные реализации: `MysqlDriver` и `SqliteDriver`.

### DriverFactory

`Merql\Driver\DriverFactory` автоматически определяет драйвер по подключению.

```php
use Merql\Driver\DriverFactory;

$driver = DriverFactory::create($connection);
```

Зарегистрируйте собственный драйвер для других баз данных:

```php
DriverFactory::register('pgsql', MyPostgresDriver::class);
```

---

## Фильтры

### TableFilter

`Merql\Filter\TableFilter` включает таблицы или исключает их по глоб-шаблонам.

```php
use Merql\Filter\TableFilter;

$filter = TableFilter::exclude(['cache_*', 'sessions', 'temp_*']);
$filter = TableFilter::include(['posts', 'users', 'comments']);
```

### ColumnFilter

`Merql\Filter\ColumnFilter` удаляет определенные столбцы из снимков.

```php
use Merql\Filter\ColumnFilter;

$filter = ColumnFilter::ignore(['updated_at', 'created_at', 'modified_date']);
```

### RowFilter

`Merql\Filter\RowFilter` фильтрует строки с помощью функции-предиката.

```php
use Merql\Filter\RowFilter;

$filter = RowFilter::create(function (string $table, array $row): bool {
    // Exclude trashed posts.
    if ($table === 'posts' && ($row['status'] ?? '') === 'trash') {
        return false;
    }
    return true;
});
```

---

## Исключения

### SnapshotException

`Merql\Exceptions\SnapshotException` выбрасывается, если снимок не найден или его имя недопустимо.

```php
SnapshotException::notFound('baseline', '/path/to/baseline.json');
```

### MergeException

`Merql\Exceptions\MergeException` выбрасывается, если для слияния отсутствует обязательный снимок.

```php
MergeException::snapshotRequired('base');
```

### ConflictException

`Merql\Exceptions\ConflictException` выбрасывается при попытке применить результат слияния с неразрешенными конфликтами.

```php
ConflictException::unresolved(3);
// Message: "Cannot apply merge with 3 unresolved conflict(s)"
```

### SchemaException

`Merql\Exceptions\SchemaException` выбрасывается (или собирается), если схемы снимков различаются.

```php
SchemaException::mismatch('posts', 'column "tags" exists in current but not in base');
```

Несовпадения схем не останавливают слияние. Они собираются и доступны для проверки через `$result->schemaMismatches()`.
