Початок роботи
Цей посібник описує встановлення, підключення до бази даних, створення знімків, обчислення різниць і виконання тристороннього злиття.
Встановлення
composer require merql/merql
Для merql потрібен PHP 8.2+ з ext-mbstring і ext-zlib (обидва потрібні залежності pitmaster/pitmaster). Також потрібне розширення для використовуваного адаптера: ext-pdo з ext-pdo_sqlite або ext-pdo_mysql для адаптера PDO чи ext-mysqli для адаптера mysqli. Composer лише рекомендує їх, тому перевірте, чи встановлено потрібне.
Пакет установлює CLI як vendor/bin/merql. Для CLI використовуйте merql 0.4.1 або новішу версію; у попередніх випусках виконуваний файл не міг знайти автозавантажувач, якщо merql було встановлено як залежність.
Підключення до бази даних
merql працює через невеликий інтерфейс адаптера підключення. Вбудовані адаптери доступні для PDO та mysqli, а вбудовані драйвери діалектів — для MySQL і SQLite.
use Merql\Connection;
// SQLite (in-memory or file-based, through PDO).
$connection = Connection::sqlite(':memory:');
$connection = Connection::sqlite('/path/to/database.sqlite');
// MySQL through PDO.
$connection = Connection::mysql('127.0.0.1', 'my_database', 'root', 'password');
// MySQL through mysqli.
$connection = Connection::mysqli('127.0.0.1', 'my_database', 'root', 'password');
// Any MySQL or SQLite PDO DSN.
$connection = Connection::fromDsn('mysql:host=127.0.0.1;dbname=my_database;charset=utf8mb4', 'root', 'password');
fromDsn() відкриває будь-який DSN, який підтримує PDO, але merql містить драйвери діалектів лише для MySQL і SQLite. З іншим рушієм, наприклад pgsql:, підключення відкриється, але Merql::init() видасть Unsupported database driver: pgsql, доки ви не зареєструєте для нього драйвер. Див. Драйвери баз даних.
Створення знімка
Знімок записує кожен рядок кожної таблиці (або вибраної підмножини таблиць) як відбитки та дані. Відбитки дають змогу швидко виявляти зміни; повні дані рядків доступні, коли потрібна різниця.
use Merql\Merql;
Merql::init($connection);
// Snapshot all tables.
$snapshot = Merql::snapshot('baseline');
// Snapshot specific tables.
$snapshot = Merql::snapshot('baseline', ['posts', 'comments', 'users']);
За замовчуванням знімки зберігаються як файли JSON у .merql/snapshots/. Це розташування можна змінити:
use Merql\Snapshot\SnapshotStore;
SnapshotStore::setDirectory('/var/data/merql-snapshots');
Обчислення різниці
Різниця порівнює два знімки й повертає набір змін: вставлення, оновлення та видалення. Обидва знімки мають уже існувати; тут current — це другий знімок, зроблений після зміни бази даних.
$changeset = Merql::diff('baseline', 'current');
echo "Inserts: " . count($changeset->inserts()) . "\n";
echo "Updates: " . count($changeset->updates()) . "\n";
echo "Deletes: " . count($changeset->deletes()) . "\n";
// Inspect changes for a single table.
$postChanges = $changeset->forTable('posts');
// Check per-column diffs on updates.
foreach ($changeset->updates() as $update) {
echo "Row {$update->rowKey} in {$update->table}:\n";
foreach ($update->columnDiffs as $diff) {
echo " {$diff->column}: {$diff->oldValue} -> {$diff->newValue}\n";
}
}
Тристороннє злиття
Основна операція. Вона бере три іменовані знімки (base, ours, theirs) і об’єднує два набори змін.
Base — це стан, з якого почали обидві копії. Ours і theirs — дві копії після незалежних змін, тому кожну потрібно знімати з бази даних, у якій вона зберігається. Наприклад, коли ізольоване середовище скопіювали з робочої бази даних:
use Merql\Connection;
use Merql\Merql;
$live = Connection::sqlite('/data/live.sqlite');
$sandbox = Connection::sqlite('/data/sandbox.sqlite');
Merql::init($live);
Merql::snapshot('base'); // before the sandbox is copied from live
// ... copy live.sqlite to sandbox.sqlite, then both databases change ...
Merql::init($sandbox);
Merql::snapshot('ours'); // the sandbox with our edits
Merql::init($live);
Merql::snapshot('theirs'); // live with its own changes
$result = Merql::merge('base', 'ours', 'theirs');
echo "Operations: {$result->operationCount()}\n";
echo "Conflicts: {$result->conflictCount()}\n";
echo "Clean: " . ($result->isClean() ? 'yes' : 'no') . "\n";
Не створюйте всі три знімки послідовно з однієї бази даних. Тоді theirs уже міститиме зміни ours, а кожен вставлений ours рядок буде визначено як конфлікт insert_insert.
Merql::merge() виконує злиття на рівні стовпців. Щоб також об’єднувати вміст значень TEXT і JSON, використовуйте ThreeWayMerge з CellMergeConfig; див. Злиття на рівні комірок.
Опрацювання конфліктів
Якщо обидві сторони змінюють той самий стовпець на різні значення, merql повідомляє про конфлікт. Ви можете розв’язати конфлікти автоматично або перевірити їх вручну.
use Merql\Merge\ConflictPolicy;
use Merql\Merge\ConflictResolver;
if (!$result->isClean()) {
// Auto-resolve: one side wins.
$resolved = ConflictResolver::resolve($result, ConflictPolicy::TheirsWins);
// Or inspect each conflict.
foreach ($result->conflicts() as $conflict) {
echo "Conflict on {$conflict->table()} row {$conflict->rowKey()}:\n";
echo " Type: {$conflict->type()}\n";
echo " Column: {$conflict->column()}\n";
echo " Ours: " . var_export($conflict->oursValue(), true) . "\n";
echo " Theirs: " . var_export($conflict->theirsValue(), true) . "\n";
}
}
Застосування злиття
Коли результат злиття готовий (невирішених конфліктів немає), застосуйте його до бази даних, переданої в Merql::init().
Операції в результаті злиття перетворюють base на об’єднаний стан і охоплюють зміни обох сторін. Merql::apply() виконує кожну з них, тож результат можна застосувати без помилок до бази даних, у якій досі зберігається стан base. Якщо застосувати його до бази даних, яка вже містить зміни однієї зі сторін, будь-яке вставлення з цієї сторони зіткнеться з наявним рядком, завершиться помилкою і скасує все злиття. Щоб перенести зміни однієї сторони в базу даних, яка вже містить зміни іншої (наприклад, зміни в ізольованому середовищі — у робочу базу), застосуйте лише відсутні в ній операції за допомогою Захищене застосування.
$applied = Merql::apply($result);
echo "Rows affected: {$applied->rowsAffected()}\n";
if ($applied->hasErrors()) {
foreach ($applied->errors() as $error) {
echo "Error: {$error}\n";
}
}
Засіб застосування загортає всі операції в транзакцію. Якщо будь-який оператор завершується помилкою, усе злиття відкочується, а помилка повертається в errors(). У такому разі rowsAffected() усе одно враховує оператори, виконані до помилки, хоча їх було відкочено (#4); спершу перевірте hasErrors().
Тестовий запуск
Перегляньте SQL, які згенерувало б злиття, не виконуючи його.
use Merql\Apply\DryRun;
$statements = DryRun::generate($result);
foreach ($statements as $sql) {
echo "{$sql};\n";
}
Двосторонній патч
Якщо у вас є лише base і набір змін (без паралельних змін «ours»), використовуйте patch(). Це рівнозначно злиттю, у якому ours не відрізняється від base.
$result = Merql::patch('base', 'changes');
// Always clean: no concurrent edits means no conflicts.
Merql::apply($result);
Використання CLI
Усі ці операції також доступні в командному рядку. Повний опис див. у CLI Довіднику.
export MERQL_DB_DSN="sqlite:mydb.sqlite"
vendor/bin/merql snapshot base
# ... change the database ...
vendor/bin/merql snapshot current
vendor/bin/merql diff base current
Для тристороннього злиття створіть кожен знімок із бази даних, у якій зберігається відповідний стан, а потім виконайте злиття:
MERQL_DB_DSN="sqlite:/data/live.sqlite" vendor/bin/merql snapshot base
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
Фільтрування таблиць і стовпців
Зі знімків можна виключити таблиці (наприклад, таблиці кешу або сеансів) і ігнорувати мінливі стовпці (наприклад, позначки часу updated_at).
use Merql\Filter\TableFilter;
use Merql\Filter\ColumnFilter;
use Merql\Snapshot\Snapshotter;
$snapshotter = new Snapshotter($connection);
$snapshot = $snapshotter->capture('baseline', filters: [
TableFilter::exclude(['sessions', 'cache_*']),
ColumnFilter::ignore(['updated_at', 'modified_date']),
]);
Наступні кроки
- Повний опис інтерфейсу командного рядка — у CLI Довіднику.
- Детальна документація кожного класу й методу — у PHP API Довіднику.