列単位のマージ
列単位のマージは、単純な行単位の比較に対するmerqlの主な利点です。両側が同じ行を変更した場合、merqlは行全体を競合として扱うのではなく、各列を個別に比較します。
行単位マージの問題点
2人が同じ行を編集する posts テーブルを考えてみましょう:
Base: post #42 { title: "Hello", content: "Body", status: "draft" }
Ours: post #42 { title: "Hello", content: "Body v2", status: "draft" }
Theirs: post #42 { title: "New Title", content: "Body", status: "publish" }
行単位のマージでは、両側で行が変更されたことを検出すると、競合と判断します。しかし、変更はまったく重複していません。oursは content を編集し、theirsは title と status を編集しました。
列単位マージによる解決方法
merqlは、各列に対して同じ三者マージ規則を個別に適用します:
title: base="Hello", ours="Hello", theirs="New Title" -> accept theirs
content: base="Body", ours="Body v2", theirs="Body" -> accept ours
status: base="draft", ours="draft", theirs="publish" -> accept theirs
結果: { title: "New Title", content: "Body v2", status: "publish" }。競合のないクリーンなマージです。
仕組み
ThreeWayMerge が両側で同じ行を更新したことを検出すると、ColumnMerge::merge() に処理を委ねます:
use Merql\Merge\ColumnMerge;
$result = ColumnMerge::merge(
table: 'posts',
rowKey: '42',
base: ['title' => 'Hello', 'content' => 'Body', 'status' => 'draft'],
ours: ['title' => 'Hello', 'content' => 'Body v2', 'status' => 'draft'],
theirs: ['title' => 'New Title', 'content' => 'Body', 'status' => 'publish'],
);
// $result['values'] -> merged column values
// $result['conflicts'] -> list of Conflict objects (empty if clean)
各列には同じ規則が適用されます:
| 基準 | ours | theirs | 結果 |
|---|---|---|---|
| A | A | A | 基準を維持(変更なし) |
| A | A | B | theirsを採用 |
| A | B | A | oursを採用 |
| A | B | B | 採用(両者一致) |
| A | B | C | 競合(値が異なる) |
競合が発生する場合
両側が同じ列を異なる値に変更すると、列レベルの競合が発生します:
Base: post #42 { title: "Hello" }
Ours: post #42 { title: "Welcome" }
Theirs: post #42 { title: "Greetings" }
両側がtitleを異なる値に変更しました。これは解決が必要な実際の競合です。
$result = ColumnMerge::merge(
'posts', '42',
['title' => 'Hello'],
['title' => 'Welcome'],
['title' => 'Greetings'],
);
// $result['conflicts'] contains one Conflict:
// table: "posts", column: "title"
// oursValue: "Welcome", theirsValue: "Greetings", baseValue: "Hello"
部分的な競合
1つの行に、正常にマージされる列と競合する列の両方が含まれることがあります。ある列は正常にマージできても、別の列で競合する場合、行のMergeOperationには正常にマージできた列のマージ後の値と、競合した列のoursの値(デフォルト)が含まれます。競合は別途報告されます。
Base: { title: "Hello", content: "Body", status: "draft" }
Ours: { title: "Welcome", content: "Body v2", status: "draft" }
Theirs: { title: "Greetings", content: "Body", status: "publish" }
ここではtitleが競合し(両側が異なる変更を行い)、contentは正常にマージされ(oursのみ変更)、statusも正常にマージされます(theirsのみ変更)。この操作には次が含まれます:
// values: { title: "Welcome", content: "Body v2", status: "publish" }
// One conflict on the "title" column.
値の比較
列の値はキャスト後の文字列として比較されます。NULLは別個の値です:
NULLからNULLへの変更 = 変更なし。NULLから"hello"への変更 = 更新。"hello"からNULLへの変更 = 更新。"1"から1への変更 = 変更なし(どちらも文字列"1"にキャストされます)。
これはMerQL接続アダプターの取得セマンティクスと一致します。データベースの値はすべて文字列(またはNULL)として返されます。
セルレベルのマージ
列レベルのマージで競合が残る場合(両側が同じ列を異なる値に変更した場合)、merqlはさらに踏み込んだセルレベルのマージを実行できます。これはTEXT列(行単位の差分)とJSON列(キー単位の差分)に適用され、同じ列の値の異なる部分を両側が編集したケースを解決します。