
# 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()` 之前调用；如需切换数据库，请再次调用，例如从两个不同副本创建 ours 和 theirs 的快照。如果没有提供 `$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`

双向合并：将更改应用到 base。等同于 `merge($base, $base, $changes)`。由于没有并发编辑，因此始终会生成无冲突结果。仅按列合并，与 `merge()` 一样。

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

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

对传入 `init()` 的数据库执行无冲突合并结果。需要 `init()`。如果仍有未解决的冲突，则抛出 `ConflictException`。

结果中的每项操作都会执行，因此 `merge()` 的完整结果只有在数据库仍处于 base 状态时才能干净地应用。对于已经包含一侧更改的数据库，请选择应用其缺少的操作；请参阅 [受保护的应用](https://merql.inline0.dev/zh/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` 是目标数据库的快照（将 ours 提升到创建 theirs 快照的数据库时，即为 theirs）。任何影响零行的语句都会回滚事务。

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

使用任意 PDO DSN 字符串进行连接。merql 仅附带 `mysql` 和 `sqlite` 的驱动程序；其他 DSN 可以建立连接，但如果未通过 `DriverFactory::register()` 注册驱动程序，`Merql::init()`、`Snapshotter` 和 applier 都会抛出 `Unsupported database driver`。

```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` 为 true，`NULL === ''` 为 false。

---

## 变更集

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

执行三方合并。在内部计算变更集（base 到 ours、base 到 theirs）并将其合并。

```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` | ours 更新了 theirs 已删除的行 |
| `delete_update` | ours 删除了 theirs 已更新的行 |
| `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`（静态）

便捷工厂方法：为 TEXT/LONGTEXT 列指定 `TextCellMerger`，为 JSON 列指定 `JsonCellMerger`。

```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` 使用 glob 模式包含或排除表。

```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()` 查看。
