はじめに
merqlは、データベースの状態を三者マージする純粋な PHP ライブラリです。テーブルのスナップショットを取得し、スナップショット間の変更セットを計算し、共通のbaseに対して2組の変更をマージして、列単位で競合を検出します。ソースファイルに対するgitと同じアルゴリズムを、行や列に適用します。
外部ツールは不要です。プラグイン可能な接続アダプターと方言ドライバーを通じて、MySQLとSQLiteをサポートします。
使用例
マージには同じデータの3つの状態が必要です。両方のコピーが出発点としたbaseと、それぞれ独立して変更された2つのコピーです。ここでは、サンドボックスを本番データベースからコピーし、その後、両方に編集を加えています。
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 -- ロールバック用アーティファクトと差異チェック。