Планы слияния
MergeResult — исполняемый результат слияния. MergePlan — его форма, пригодная для проверки. Используйте его, если приложению нужно показать, подготовить, сериализовать или выборочно применить изменения базы данных перед записью в целевую базу данных.
use Merql\Merql;
$plan = Merql::plan(
id: 'plan-2026-06-06',
base: 'base',
ours: 'ours',
theirs: 'theirs',
metadata: ['sandbox_id' => 'ow_123'],
);
Merql::plan() загружает три именованных снимка и объединяет их на уровне столбцов, как Merql::merge(). Чтобы спланировать слияние с использованием слияния на уровне ячеек для столбцов TEXT и JSON, самостоятельно создайте результат и передайте его в MergePlanBuilder:
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Plan\MergePlanBuilder;
use Merql\Snapshot\SnapshotStore;
$base = SnapshotStore::load('base');
$ours = SnapshotStore::load('ours');
$theirs = SnapshotStore::load('theirs');
$result = (new ThreeWayMerge(CellMergeConfig::auto()))->merge($base, $ours, $theirs);
$plan = (new MergePlanBuilder())->build('plan-2026-06-06', $base, $ours, $theirs, $result);
План содержит:
- имена снимков, использованных для его создания,
- сводку операций, конфликтов, несовпадений схем, таблиц и групп,
- стабильные идентификаторы операций,
- стабильные идентификаторы групп изменений,
- столбцы идентификаторов строк и значения идентификаторов,
- значения строк из базы, нашей стороны, их стороны и результата для каждой операции,
- необязательные метаданные приложения-хоста,
- хеш плана,
- версию схемы.
Стабильные идентификаторы
Идентификаторы операций детерминированы для одинакового содержимого операций. Они вычисляются на основе имени таблицы, ключа строки, типа операции, источника операции, измененных столбцов и значений результата.
Идентификаторы групп изменений детерминированы для одинакового содержимого групп. По умолчанию построитель создает одну группу операций для каждой операции. Приложения-хосты могут создавать более крупные группы, специфичные для предметной области, на основе списка операций и сериализовать их с той же структурой DTO.
foreach ($plan->changeGroups as $group) {
echo $group->id . ': ' . implode(', ', $group->operationIds);
}
Структура операции
Каждый MergePlanOperation содержит достаточно контекста для проверки и отката.
$operation = $plan->operations[0];
$operation->id; // stable operation ID
$operation->type; // insert, update, delete
$operation->table; // canonical table name
$operation->rowKey; // encoded identity key
$operation->identityColumns; // columns used for identity
$operation->identityValues; // decoded identity values
$operation->source; // ours, theirs, or merged
$operation->baseRow; // row in base snapshot, or null
$operation->oursRow; // row in ours snapshot, or null
$operation->theirsRow; // row in theirs snapshot, or null
$operation->resultValues; // values the merge would write
$operation->changedColumns; // changed result columns
Выбор операций
Используйте ChangeGroupSelection, когда UI подготавливает группы. Преобразуйте его в OperationSelection при применении изменений или создании артефактов отката.
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;
$selection = ChangeGroupSelection::fromIds([
$plan->changeGroups[0]->id,
]);
$operationSelection = $selection->toOperationSelection($plan);
$selectedResult = (new SelectedMergeResultFactory())->fromOperationSelection(
$plan,
$operationSelection,
SnapshotStore::load($plan->baseSnapshot),
);
Выбранный результат — это чистый MergeResult, содержащий только выбранные операции. Его можно передать в Merql::apply() или Merql::applyGuarded().
Выбор отсутствующих в целевой базе данных изменений
План содержит операции обеих сторон, поскольку результат слияния преобразует базу в объединенное состояние. Если в целевой базе уже есть состояние одной из сторон, выберите только те операции, которых в ней еще нет. Для целевой базы с состоянием их стороны, например рабочей базы данных, в которую передаются изменения из тестовой копии, это все операции с источником ours, а также обновления merged, объединяющие изменения столбцов обеих сторон:
use Merql\Plan\OperationSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;
// Rows with an unresolved conflict stay out of the selection.
$conflicted = [];
foreach ($plan->conflicts as $conflict) {
$conflicted[$conflict->table . "\0" . $conflict->rowKey] = true;
}
$ids = [];
foreach ($plan->operations as $operation) {
$missing = $operation->source === 'ours'
|| ($operation->source === 'merged' && $operation->type === 'update');
if ($missing && !isset($conflicted[$operation->table . "\0" . $operation->rowKey])) {
$ids[] = $operation->id;
}
}
$selected = (new SelectedMergeResultFactory())->fromOperationSelection(
$plan,
OperationSelection::fromIds($ids),
SnapshotStore::load($plan->baseSnapshot),
);
Операции с источником theirs, а также удаления merged (строки, удаленные обеими сторонами) уже присутствуют в целевой базе.
План, построенный на основе слияния с конфликтами, по-прежнему содержит операцию для каждой конфликтующей строки, обновленной обеими сторонами. В результате для конфликтующего столбца сохраняется наше значение, а SelectedMergeResultFactory возвращает чистый результат для любого набора выбранных операций. И у операции, и у группы изменений значение conflictState даже в этом случае равно clean (#9). Поэтому самостоятельно проверьте $plan->conflicts, как описано выше, или разрешите конфликты с помощью ConflictResolver и создайте план на основе разрешенного результата с помощью MergePlanBuilder. См. раздел Защищенное применение, посвященный записи выбранных операций с предварительными условиями для актуальных строк.
Сериализация
Планы слияния и выборки имеют стабильную сериализацию JSON. Сохраните сериализованный план, если на следующем этапе применения нужно использовать точно тот же набор проверенных операций.
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\MergePlanSerializer;
$json = MergePlanSerializer::toJson($plan);
$restored = MergePlanSerializer::fromJson($json);
$selectionJson = json_encode($selection->toArray(), JSON_THROW_ON_ERROR);
$selection = ChangeGroupSelection::fromArray(
json_decode($selectionJson, true, flags: JSON_THROW_ON_ERROR),
);
Снимки с псевдонимами
В планах слияния используются канонические имена таблиц. Если физические имена таблиц отличаются в разных средах, перед планированием сохраните их под одним и тем же каноническим именем.
use Merql\Snapshot\SnapshotStore;
use Merql\Snapshot\Snapshotter;
$snapshotter = new Snapshotter($connection);
SnapshotStore::save($snapshotter->captureAliased('live', [
'wp_posts' => 'wp_posts',
]));
SnapshotStore::save($snapshotter->captureAliased('sandbox', [
'wp_onumia_abc_posts' => 'wp_posts',
]));
Схема, сохраненная в снимке, использует каноническое имя таблицы, сохраняя при этом столбцы, ключи, столбцы идентификаторов, ключи строк и фильтры физической таблицы.