Перейти к содержимому

Применение с проверкой

GuardedApplier применяет слияние без конфликтов MergeResult, только если целевая база данных по-прежнему соответствует ожидаемому актуальному снимку. Это полезно, когда план слияния был проверен ранее, а актуальные строки могли измениться до его применения.

Обычно изменения из изолированной среды переносят в рабочую базу данных. Базовая версия — это рабочая база на момент создания из нее копии изолированной среды, наша версия — измененная изолированная среда, а их версия — текущее состояние рабочей базы. Цель — рабочая база данных, поэтому ожидаемый актуальный снимок — theirs, а записать нужно операции, которых в рабочей базе еще нет (см. Выбор операций).

PHP
use Merql\Connection;
use Merql\Merql;
use Merql\Plan\OperationSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

Merql::init(Connection::sqlite('/data/live.sqlite'));

$plan = Merql::plan('promote-1', 'base', 'ours', 'theirs');

// Skip rows with unresolved conflicts.
$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('base'),
);

$applied = Merql::applyGuarded($selected, SnapshotStore::load('theirs'));

if ($applied->hasErrors()) {
    foreach ($applied->errors() as $error) {
        echo $error . "\n";
    }
}

Merql::applyGuarded() записывает данные в базу, переданную в Merql::init(). Ожидаемый актуальный снимок должен описывать ту же базу: получите его из целевой базы достаточно недавно, чтобы предварительные условия оставались актуальными.

Предварительные условия

Защищенное SQL добавляет к сгенерированным инструкциям предварительные условия для актуальных строк:

  • для обновления целевая строка должна существовать, а значения неидентификационных столбцов должны совпадать с ожидаемой актуальной строкой,
  • для удаления целевая строка должна существовать, а значения неидентификационных столбцов должны совпадать с ожидаемой актуальной строкой,
  • при вставке целевая запись с таким идентификатором не должна уже существовать.

Если какая-либо инструкция затрагивает ноль строк, merql считает план устаревшим. Транзакция откатывается, а в возвращенном ApplyResult содержится ошибка Optimistic precondition failed for guarded merge statement.

В MySQL обновление, значения которого уже совпадают с актуальной строкой, тоже затрагивает ноль строк, поскольку MySQL учитывает измененные строки, а не найденные. Такое обновление завершается ошибкой устаревшего плана, хотя строка совпадает (#2). SQLite учитывает найденные строки и принимает такое обновление. Это может произойти при merged обновлении, если обе стороны внесли одинаковое изменение в строку.

Выбор операций

Результат слияния содержит операции обеих сторон, поскольку он преобразует базовую версию в объединенное состояние. Если применить его к цели, которая уже содержит их версию, операции, пришедшие от них, описывают изменения, уже имеющиеся в цели, поэтому их предварительные условия не выполняются:

Операция Состояние цели, содержащей их версию Выбрать
ours вставка, обновление или удаление Отсутствует Да
merged обновление (обе стороны изменили строку) Содержит их столбцы, не содержит наших Да
theirs вставка Строка существует, поэтому защищенная вставка затрагивает ноль строк Нет
merged удаление (обе стороны удалили строку) Строка удалена, поэтому защищенное удаление затрагивает ноль строк Нет
theirs обновление или удаление Уже применено Нет

Поэтому передача всего результата Merql::merge() в applyGuarded() приведет к ошибке, как только их сторона вставит строку или обе стороны удалят одну и ту же строку. Сначала выберите операции, указав источник, как выше, или используя ChangeGroupSelection, созданный на основе проверки UI. Исключите строки из $plan->conflicts: выбранный результат всегда не содержит конфликтов, а операция для конфликтной строки содержит наше значение в конфликтующем столбце (см. Планы слияния).

Выборочное применение

Защищенное применение работает с выбранными результатами слияния, созданными на основе плана слияния, включая выбор по группам изменений.

PHP
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

$selection = ChangeGroupSelection::fromIds([$plan->changeGroups[0]->id]);
$selected = (new SelectedMergeResultFactory())->fromChangeGroupSelection(
    $plan,
    $selection,
    SnapshotStore::load($plan->baseSnapshot),
);

$applied = Merql::applyGuarded($selected, SnapshotStore::load($plan->theirsSnapshot));

Это позволяет принимающему приложению подготовить часть проверенных групп и при этом убедиться, что актуальные строки не изменились после создания плана.

Предварительный просмотр SQL

Используйте SqlGenerator::generateGuarded(), чтобы проверить защищенные инструкции, не выполняя их.

PHP
use Merql\Apply\SqlGenerator;
use Merql\Snapshot\SnapshotStore;

$statements = SqlGenerator::generateGuarded(
    $selected,
    SnapshotStore::load($plan->theirsSnapshot),
);

foreach ($statements as $statement) {
    echo $statement['sql'] . "\n";
}

Сгенерированные инструкции по-прежнему параметризованы. Предварительные условия выражаются дополнительными условиями WHERE для обновлений и удалений и проверками NOT EXISTS для вставок.

Защищенное или обычное применение

Merql::apply() выполняет те же операции без предварительных условий. Обновления и удаления в этом случае выполняются независимо от того, менялась ли актуальная строка после создания плана, а вставка уже существующей строки завершается ошибкой дублирования ключа базы данных. Используйте обычное применение для цели, которая, как вы уверены, по-прежнему содержит базовую версию, а защищенное — если другие процессы могли изменить цель после создания снимка.