Erste Schritte
Diese Anleitung führt dich durch die Installation, die Verbindung mit einer Datenbank, das Erstellen von Snapshots, das Berechnen von Diffs und das Ausführen eines Drei-Wege-Merges.
Installation
composer require merql/merql
merql benötigt PHP 8.2+ mit ext-mbstring und ext-zlib (beide sind Abhängigkeiten von pitmaster/pitmaster). Außerdem brauchst du die Erweiterung für den verwendeten Adapter: ext-pdo mit ext-pdo_sqlite oder ext-pdo_mysql für den PDO-Adapter oder ext-mysqli für den mysqli-Adapter. Composer schlägt diese Erweiterungen nur vor. Prüfe also, ob die benötigte installiert ist.
Das Paket installiert CLI unter vendor/bin/merql. Verwende merql 0.4.1 oder neuer für CLI. In früheren Versionen fand die Binärdatei den Autoloader nicht, wenn merql als Abhängigkeit installiert war.
Verbindung mit einer Datenbank herstellen
merql verwendet eine schlanke Schnittstelle für Verbindungsadapter. Es enthält Adapter für PDO und mysqli sowie Dialekttreiber für MySQL und 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() öffnet jede von PDO unterstützte DSN, aber merql enthält nur Dialekttreiber für MySQL und SQLite. Bei einer anderen Engine wie pgsql: wird die Verbindung hergestellt, aber Merql::init() löst Unsupported database driver: pgsql aus, bis du einen Treiber dafür registrierst. Weitere Informationen findest du unter Database Drivers.
Snapshot erstellen
Ein Snapshot erfasst jede Zeile jeder Tabelle (oder einer Tabellenauswahl) als Fingerprints und Daten. Fingerprints ermöglichen eine schnelle Erkennung von Änderungen; die vollständigen Zeilendaten stehen für einen Diff bereit.
use Merql\Merql;
Merql::init($connection);
// Snapshot all tables.
$snapshot = Merql::snapshot('baseline');
// Snapshot specific tables.
$snapshot = Merql::snapshot('baseline', ['posts', 'comments', 'users']);
Snapshots werden standardmäßig als JSON-Dateien unter .merql/snapshots/ gespeichert. Du kannst den Speicherort ändern:
use Merql\Snapshot\SnapshotStore;
SnapshotStore::setDirectory('/var/data/merql-snapshots');
Diff berechnen
Ein Diff vergleicht zwei Snapshots und gibt eine Änderungsliste mit Einfügungen, Aktualisierungen und Löschungen zurück. Beide Snapshots müssen bereits vorhanden sein. Hier ist current ein zweiter Snapshot, der nach einer Änderung an der Datenbank erstellt wurde.
$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";
}
}
Drei-Wege-Merge
Der zentrale Vorgang. Er nimmt drei benannte Snapshots (Base, Ours, Theirs) und führt die beiden Änderungssätze zusammen.
Base ist der gemeinsame Ausgangszustand beider Kopien. Ours und Theirs sind die beiden Kopien nach unabhängigen Änderungen. Erfasse jede daher aus der Datenbank, in der sie vorliegt. Beispiel: Eine Sandbox wurde aus einer Live-Datenbank kopiert:
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";
Erstelle nicht alle drei Snapshots nacheinander aus derselben Datenbank. Theirs würde dann bereits die Änderungen von Ours enthalten, und jede von Ours eingefügte Zeile würde als insert_insert-Konflikt erscheinen.
Merql::merge() führt Änderungen auf Spaltenebene zusammen. Um auch Änderungen innerhalb von TEXT- und JSON-Werten zusammenzuführen, verwende ThreeWayMerge mit einem CellMergeConfig. Siehe Cell-Level Merge.
Konflikte behandeln
Wenn beide Seiten dieselbe Spalte auf unterschiedliche Werte ändern, meldet merql einen Konflikt. Du kannst Konflikte automatisch auflösen oder manuell prüfen.
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";
}
}
Merge anwenden
Sobald das Merge-Ergebnis sauber ist (keine ungelösten Konflikte), wende es auf die an Merql::init() übergebene Datenbank an.
Die Vorgänge im Merge-Ergebnis überführen Base in den zusammengeführten Zustand und umfassen die Änderungen beider Seiten. Merql::apply() führt sie alle aus und kann daher problemlos auf eine Datenbank angewendet werden, die noch den Base-Zustand enthält. Wird es auf eine Datenbank angewendet, die bereits die Änderungen einer Seite enthält, schlägt jede Einfügung dieser Seite fehl, weil die Zeile schon existiert. Das gesamte Merge wird dann zurückgerollt. Um die Änderungen einer Seite in eine Datenbank zu übernehmen, die bereits die andere Seite enthält (zum Beispiel Sandbox-Änderungen in Live), wende nur die noch fehlenden Vorgänge mit Guarded Apply an.
$applied = Merql::apply($result);
echo "Rows affected: {$applied->rowsAffected()}\n";
if ($applied->hasErrors()) {
foreach ($applied->errors() as $error) {
echo "Error: {$error}\n";
}
}
Der Anwender führt alle Vorgänge in einer Transaktion aus. Schlägt ein Befehl fehl, wird das gesamte Merge zurückgerollt und der Fehler in errors() zurückgegeben. In diesem Fall zählt rowsAffected() weiterhin die Befehle, die vor dem Fehler ausgeführt wurden, obwohl sie zurückgerollt wurden (#4). Prüfe zuerst hasErrors().
Probelauf
Sieh dir die SQL an, die ein Merge erzeugen würde, ohne es auszuführen.
use Merql\Apply\DryRun;
$statements = DryRun::generate($result);
foreach ($statements as $sql) {
echo "{$sql};\n";
}
Zwei-Wege-Patch
Wenn du nur einen Base-Zustand und eine Reihe von Änderungen hast (ohne gleichzeitige Änderungen auf „Ours“), verwende patch(). Das entspricht einem Merge, bei dem Ours gegenüber Base unverändert ist.
$result = Merql::patch('base', 'changes');
// Always clean: no concurrent edits means no conflicts.
Merql::apply($result);
CLI verwenden
Alle diese Vorgänge sind auch über die Kommandozeile verfügbar. Ausführliche Informationen findest du in der CLI Reference.
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
Erstelle für einen Drei-Wege-Merge jeden Snapshot aus der Datenbank, in der der jeweilige Zustand vorliegt, und führe dann das Merge aus:
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
Tabellen und Spalten filtern
Snapshots können Tabellen (etwa Cache- oder Sitzungstabellen) ausschließen und flüchtige Spalten (etwa updated_at-Zeitstempel) ignorieren.
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']),
]);
Nächste Schritte
- CLI Reference für die vollständige Kommandozeilenoberfläche.
- PHP API Reference mit ausführlicher Dokumentation aller Klassen und Methoden.