Zum Inhalt springen

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

Shell
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.

PHP
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.

PHP
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:

PHP
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.

PHP
$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:

PHP
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.

PHP
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.

PHP
$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.

PHP
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.

PHP
$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.

Shell
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:

Shell
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.

PHP
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