コンテンツへスキップ

はじめに

merqlは、データベースの状態を三者マージする純粋な PHP ライブラリです。テーブルのスナップショットを取得し、スナップショット間の変更セットを計算し、共通のbaseに対して2組の変更をマージして、列単位で競合を検出します。ソースファイルに対するgitと同じアルゴリズムを、行や列に適用します。

外部ツールは不要です。プラグイン可能な接続アダプターと方言ドライバーを通じて、MySQLとSQLiteをサポートします。

使用例

マージには同じデータの3つの状態が必要です。両方のコピーが出発点としたbaseと、それぞれ独立して変更された2つのコピーです。ここでは、サンドボックスを本番データベースからコピーし、その後、両方に編集を加えています。

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";
}

各スナップショットは、その状態を保持するデータベースから取得します。1つのデータベースから3つすべてを順番に取得しても、分岐した2つのコピーを表せません。theirsにはすでにoursの変更が含まれるため、oursが挿入したすべての行が insert_insert の競合として報告されます。

結果には、baseをマージ後の状態に変換する両側の操作が一覧表示され、それぞれの出所(ours、theirs、merged)が示されます。サンドボックスの変更を本番環境に書き込むには、Guarded Applyを使って、本番環境にまだない操作だけを適用します。

または 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を選ぶ理由

状態を比較するデータベースツールの多くは行単位で処理します。2つのバージョン間で行の列が1つでも異なると、行全体を競合として扱います。その結果、2人が同じ行の異なる列を編集した場合に、誤った競合が発生します。

merqlは列単位でマージします。一方が title を変更し、もう一方が status を変更した場合、競合せずにマージできます。両側が同じ列を異なる値に変更した場合にのみ、競合が発生します。

TEXTと JSON の列では、セル単位のマージを使ってさらに細かく処理できます。テキスト列の異なる行、または JSON 列の異なるキーに対する2つの編集は、同じ三者マージアルゴリズムを使って競合せずにマージできます。セル単位のマージはオプトインです。ThreeWayMerge に CellMergeConfig を渡してください。Merql ファサードと CLI は列単位でのみマージします。Cell-Level Mergeを参照してください。

マージモデル

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

gitと同じ3つの状態を使います。baseは共通の祖先です。oursとtheirsは分岐後の2つのコピーで、それぞれのデータベースから取得します。マージ規則は両側を同等に扱います。これらの名前は、各操作の source、競合の種類(update_delete はoursが更新し、theirsが削除したことを意味します)、OursWins と TheirsWins の解決ポリシーに使われます。共通のbaseがなければ、「追加」と「変更なし」を区別できません。

マージ自体は何も書き込みません。マージ操作はbaseをマージ後の状態に変換するため、Merql::apply() がすべての操作を問題なく実行できるのは、baseの状態を保持するデータベースに対してのみです。サンドボックスの編集を本番環境に反映するなど、すでに片側の変更が入っているデータベースにもう一方の変更を取り込むには、まだ適用されていない操作だけを適用します。Guarded Applyを参照してください。

必要条件

  • PHP 8.2以降。
  • テキスト差分を提供する pitmaster/pitmaster の依存関係として必要な ext-mbstring と ext-zlib。
  • 使用するアダプターに対応したデータベース拡張機能: ext-pdo と ext-pdo_sqlite または ext-pdo_mysql、あるいは ext-mysqli。merqlの composer.json は ext-pdo と ext-mysqli を推奨するだけなので、Composerはこれらの有無を確認しません。
  • MySQLまたはSQLiteのデータベース。その他のエンジンにはカスタムdriverが必要です。

merqlでできないこと

merqlはスキーマ移行ツールではありません。データベースを複製したり、リアルタイムで同期したりするものでもありません。特定の問題を解決するツールです。3つのデータベース状態を受け取り、競合を特定したうえで、マージ後の4つ目の状態を生成します。

ドキュメント

  • Getting Started -- インストール、最初のスナップショット、最初のマージ。
  • CLI Reference -- vendor/bin/merql コマンドの全リファレンス。
  • PHP API Reference -- すべてのクラスの完全な API ドキュメント。
  • Merge Plans -- 安定した操作ID、変更グループ、選択。
  • Guarded Apply -- 本番行の事前条件を使った選択的な書き込み。
  • Rollback -- ロールバック用アーティファクトと差異チェック。