
# 快速入门

本指南将介绍安装、连接数据库、捕获快照、计算差异以及执行三方合并。

## 安装

```bash
composer require merql/merql
```

merql 需要 PHP 8.2+，以及 `ext-mbstring` 和 `ext-zlib`（均为 `pitmaster/pitmaster` 依赖项所需）。你还需要安装所用适配器对应的扩展：PDO 适配器需要 `ext-pdo` 和 `ext-pdo_sqlite` 或 `ext-pdo_mysql`，mysqli 适配器需要 `ext-mysqli`。Composer 仅会提出这些建议，因此请确认所需扩展已安装。

该软件包会将 CLI 安装为 `vendor/bin/merql`。使用 merql 0.4.1 或更高版本来获得 CLI；在较早版本中，merql 作为依赖安装时，二进制文件无法找到自动加载器。

## 连接数据库

merql 通过一个小型连接适配器接口运行。它内置了 PDO 和 mysqli 适配器，以及适用于 MySQL 和 SQLite 的方言驱动程序。

```php
use Merql\Connection;

// SQLite (in-memory or file-based, through PDO).
$connection = Connection::sqlite(':memory:');
$connection = Connection::sqlite('/path/to/database.sqlite');

// MySQL through PDO.
$connection = Connection::mysql('127.0.0.1', 'my_database', 'root', 'password');

// MySQL through mysqli.
$connection = Connection::mysqli('127.0.0.1', 'my_database', 'root', 'password');

// Any MySQL or SQLite PDO DSN.
$connection = Connection::fromDsn('mysql:host=127.0.0.1;dbname=my_database;charset=utf8mb4', 'root', 'password');
```

`fromDsn()` 可以打开 PDO 支持的任何 DSN，但 merql 仅内置适用于 MySQL 和 SQLite 的方言驱动程序。使用其他引擎（例如 `pgsql:`）时，连接可以打开，但在为其注册驱动程序之前，`Merql::init()` 会抛出 `Unsupported database driver: pgsql`。请参阅[数据库驱动程序](https://merql.inline0.dev/zh/docs/apply/drivers)。

## 捕获快照

快照会将每个表（或部分表）中的每一行记录为指纹和数据。指纹可用于快速检测更改；需要计算差异时则可使用完整行数据。

```php
use Merql\Merql;

Merql::init($connection);

// Snapshot all tables.
$snapshot = Merql::snapshot('baseline');

// Snapshot specific tables.
$snapshot = Merql::snapshot('baseline', ['posts', 'comments', 'users']);
```

默认情况下，快照会以 JSON 文件的形式持久化到 `.merql/snapshots/` 中。你可以更改此位置：

```php
use Merql\Snapshot\SnapshotStore;

SnapshotStore::setDirectory('/var/data/merql-snapshots');
```

## 计算差异

差异计算会比较两个快照，并返回包含插入、更新和删除操作的变更集。两个快照都必须已存在；此处的 `current` 是数据库发生更改后捕获的第二个快照。

```php
$changeset = Merql::diff('baseline', 'current');

echo "Inserts: " . count($changeset->inserts()) . "\n";
echo "Updates: " . count($changeset->updates()) . "\n";
echo "Deletes: " . count($changeset->deletes()) . "\n";

// Inspect changes for a single table.
$postChanges = $changeset->forTable('posts');

// Check per-column diffs on updates.
foreach ($changeset->updates() as $update) {
    echo "Row {$update->rowKey} in {$update->table}:\n";
    foreach ($update->columnDiffs as $diff) {
        echo "  {$diff->column}: {$diff->oldValue} -> {$diff->newValue}\n";
    }
}
```

## 三方合并

核心操作。接收三个命名快照（base、ours、theirs），并合并两组更改。

Base 是两个副本共同开始时的状态。Ours 和 theirs 是各自独立更改后的两个副本，因此必须分别从保存对应状态的数据库中捕获。例如，沙盒从实时数据库复制而来时：

```php
use Merql\Connection;
use Merql\Merql;

$live    = Connection::sqlite('/data/live.sqlite');
$sandbox = Connection::sqlite('/data/sandbox.sqlite');

Merql::init($live);
Merql::snapshot('base');      // before the sandbox is copied from live

// ... copy live.sqlite to sandbox.sqlite, then both databases change ...

Merql::init($sandbox);
Merql::snapshot('ours');      // the sandbox with our edits
Merql::init($live);
Merql::snapshot('theirs');    // live with its own changes

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

echo "Operations: {$result->operationCount()}\n";
echo "Conflicts:  {$result->conflictCount()}\n";
echo "Clean:      " . ($result->isClean() ? 'yes' : 'no') . "\n";
```

不要依次从同一个数据库捕获全部三个快照。这样 theirs 会包含 ours 的更改，ours 插入的每一行都会显示为 `insert_insert` 冲突。

`Merql::merge()` 在列级别进行合并。如需同时合并 TEXT 和 JSON 值内部的内容，请将 `ThreeWayMerge` 与 `CellMergeConfig` 配合使用；参阅[单元格级合并](https://merql.inline0.dev/zh/docs/merge/cell-level)。

### 处理冲突

当双方将同一列更改为不同值时，merql 会引发冲突。你可以自动解决冲突，也可以手动检查。

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

if (!$result->isClean()) {
    // Auto-resolve: one side wins.
    $resolved = ConflictResolver::resolve($result, ConflictPolicy::TheirsWins);

    // Or inspect each conflict.
    foreach ($result->conflicts() as $conflict) {
        echo "Conflict on {$conflict->table()} row {$conflict->rowKey()}:\n";
        echo "  Type:   {$conflict->type()}\n";
        echo "  Column: {$conflict->column()}\n";
        echo "  Ours:   " . var_export($conflict->oursValue(), true) . "\n";
        echo "  Theirs: " . var_export($conflict->theirsValue(), true) . "\n";
    }
}
```

## 应用合并

获得干净的合并结果（没有未解决的冲突）后，将其应用到传给 `Merql::init()` 的数据库。

合并结果中的操作会将基准状态转换为合并后的状态，并包含双方的更改。`Merql::apply()` 会执行所有操作，因此可干净地应用到仍处于基准状态的数据库。如果应用到已包含一方更改的数据库，则该方的任何插入操作都会遇到已存在的行而失败，并回滚整个合并。如需将一方的更改提升到已包含另一方更改的数据库中（例如将沙盒编辑同步到实时环境），请使用 [带保护条件的应用](https://merql.inline0.dev/zh/docs/apply/guarded)，仅应用该数据库缺少的操作。

```php
$applied = Merql::apply($result);

echo "Rows affected: {$applied->rowsAffected()}\n";

if ($applied->hasErrors()) {
    foreach ($applied->errors() as $error) {
        echo "Error: {$error}\n";
    }
}
```

执行器会将所有操作包装在一个事务中。如果任一语句失败，整个合并都会回滚，错误会在 `errors()` 中返回。此时，`rowsAffected()` 仍会统计失败前已运行的语句，即使这些语句已经回滚（[#4](https://github.com/inline0/merql/issues/4)）；请先检查 `hasErrors()`。

## 试运行

预览合并将生成的 SQL，而不执行它。

```php
use Merql\Apply\DryRun;

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

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

## 双向补丁

如果你只有一个基准和一组更改（没有并发的“ours”修改），请使用 `patch()`。这等同于 ours 与基准保持不变的合并。

```php
$result = Merql::patch('base', 'changes');
// Always clean: no concurrent edits means no conflicts.
Merql::apply($result);
```

## 使用 CLI

所有这些操作也都可以通过命令行使用。完整详情请参阅 [CLI 参考](https://merql.inline0.dev/zh/docs/cli)。

```bash
export MERQL_DB_DSN="sqlite:mydb.sqlite"

vendor/bin/merql snapshot base
# ... change the database ...
vendor/bin/merql snapshot current
vendor/bin/merql diff base current
```

对于三方合并，请从保存对应状态的数据库捕获每个快照，然后进行合并：

```bash
MERQL_DB_DSN="sqlite:/data/live.sqlite" vendor/bin/merql snapshot base
MERQL_DB_DSN="sqlite:/data/sandbox.sqlite" vendor/bin/merql snapshot ours
MERQL_DB_DSN="sqlite:/data/live.sqlite" vendor/bin/merql snapshot theirs
vendor/bin/merql merge base ours theirs --dry-run
```

## 筛选表和列

快照可以排除表（例如缓存表或会话表），并忽略易变列（例如 `updated_at` 时间戳）。

```php
use Merql\Filter\TableFilter;
use Merql\Filter\ColumnFilter;
use Merql\Snapshot\Snapshotter;

$snapshotter = new Snapshotter($connection);

$snapshot = $snapshotter->capture('baseline', filters: [
    TableFilter::exclude(['sessions', 'cache_*']),
    ColumnFilter::ignore(['updated_at', 'modified_date']),
]);
```

## 后续步骤

- 完整命令行界面请参阅 [CLI 参考](https://merql.inline0.dev/zh/docs/cli)。
- 每个类和方法的详细文档请参阅 [PHP API 参考](https://merql.inline0.dev/zh/docs/api)。
