跳转到正文

快速入门

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

安装

命令行
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。请参阅数据库驱动程序。

捕获快照

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

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 配合使用;参阅单元格级合并。

处理冲突

当双方将同一列更改为不同值时,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() 会执行所有操作,因此可干净地应用到仍处于基准状态的数据库。如果应用到已包含一方更改的数据库,则该方的任何插入操作都会遇到已存在的行而失败,并回滚整个合并。如需将一方的更改提升到已包含另一方更改的数据库中(例如将沙盒编辑同步到实时环境),请使用 带保护条件的应用,仅应用该数据库缺少的操作。

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);请先检查 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 参考。

命令行
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

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

命令行
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']),
]);

后续步骤