跳转到正文

简介

merql 是一个纯 PHP 库,可对数据库状态执行三方合并。它会创建表快照、计算快照之间的变更集、基于共同基线合并两组变更,并检测列级冲突。它采用 git 处理源文件时使用的同一算法,但处理对象是数据库的行和列,而不是文本行。

无需外部工具。通过可插拔的连接适配器和方言驱动程序支持 MySQL 和 SQLite。

快速示例

合并需要同一数据的三种状态:两个副本共同起始的基线,以及它们各自独立变更后的两个副本。这里,沙盒是从线上数据库复制而来,此后两者都经过了编辑。

PHP
use Merql\Connection;
use Merql\Merql;

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

// Base: live at the moment the sandbox was copied from it.
Merql::init($live);
Merql::snapshot('base');

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

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

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

if ($result->isClean()) {
    foreach ($result->operations() as $operation) {
        echo "{$operation->type} {$operation->table} #{$operation->rowKey} ({$operation->source})\n";
    }
} else {
    echo "Conflicts: {$result->conflictCount()}\n";
}

每个快照都从保存相应状态的数据库中获取。依次从同一个数据库获取全部三个快照,无法体现两个分叉的副本:对方的状态会包含我方的变更,而我方插入的每一行都会被报告为 insert_insert 冲突。

结果会列出将基线转换为合并状态所需的操作,涵盖双方变更,并为每项标注来源(ours、theirs 或 merged)。若要将沙盒中的变更写入线上数据库,请使用 受保护应用,应用线上数据库尚未包含的操作。

也可以从 CLI 获取,它会连接到每个 snapshot 在 MERQL_DB_DSN 中指定的数据库:

命令行
MERQL_DB_DSN="sqlite:/data/live.sqlite" vendor/bin/merql snapshot base
# ... copy live.sqlite to sandbox.sqlite, then both databases change ...
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

为什么选择 merql

大多数用于比较数据库状态的工具都在行级别操作:如果同一行的任意列在两个版本间不同,整行就会被标记为冲突。两个人编辑同一行的不同列时,这会造成误报冲突。

merql 在列级别进行合并。如果一方修改了 title,另一方修改了 status,这些变更可以无冲突地合并。只有双方将同一列改成不同值时才会产生冲突。

对于 TEXT 和 JSON 列,merql 可以进一步执行单元格级合并。文本列中不同文本行的两处编辑,或 JSON 列中不同键的两处编辑,都可以使用同一三方算法顺利合并。单元格级合并需显式启用:向 ThreeWayMerge 传入 CellMergeConfig。Merql 门面和 CLI 仅在列级别进行合并。参见 单元格级合并。

合并模型

代码
         Base (snapshot at fork time)
        /                              \
   Ours (one copy's changes)            Theirs (the other copy's changes)
        \                              /
         --------- MERGE ----------
                     |
              Merged result
              + conflicts (if any)

三个状态,与 git 相同。基线是共同祖先。我方和对方是分叉后各自的副本,分别从各自的数据库中获取。合并规则对双方一视同仁。这些名称会出现在每项操作的 source、冲突类型(update_delete 表示我方更新、对方删除)以及 OursWins 和 TheirsWins 冲突解决策略中。没有共同基线,就无法区分“新增”和“未变”。

合并本身不会写入任何内容。合并操作会将基线转换为合并状态,因此 Merql::apply() 只有在仍保存基线的数据库上运行时,才能顺利执行全部操作。若要将一方的变更带入已保存另一方变更的数据库(例如将沙盒编辑内容发布到线上),只应用该数据库尚缺少的操作;参见 受保护应用。

要求

  • PHP 8.2 或更高版本。
  • ext-mbstring 和 ext-zlib,由提供文本差异功能的 pitmaster/pitmaster 依赖项所需。
  • 所用适配器对应的数据库扩展:ext-pdo 搭配 ext-pdo_sqlite 或 ext-pdo_mysql,或者使用 ext-mysqli。merql 的 composer.json 仅会建议安装 ext-pdo 和 ext-mysqli,因此 Composer 不会检查它们。
  • MySQL 或 SQLite 数据库。其他数据库引擎需要自定义驱动程序。

它不是什么

merql 不是架构迁移工具。它不会复制数据库,也不会实时同步数据库。它只解决一个特定问题:给定三个数据库状态,生成一个已标明冲突的合并后第四种状态。

文档