Aller au contenu

Premiers pas

Ce guide explique l'installation, la connexion à une base de données, la capture d'instantanés, le calcul de comparaisons et la réalisation d'une fusion à trois voies.

Installation

Shell
composer require merql/merql

merql nécessite PHP 8.2+ avec ext-mbstring et ext-zlib (tous deux requis par la dépendance pitmaster/pitmaster). Vous avez également besoin de l'extension correspondant à l'adaptateur utilisé : ext-pdo avec ext-pdo_sqlite ou ext-pdo_mysql pour l'adaptateur PDO, ou ext-mysqli pour l'adaptateur mysqli. Composer ne fait que suggérer ces extensions ; vérifiez donc que celle dont vous avez besoin est installée.

Le paquet installe le CLI sous le nom vendor/bin/merql. Utilisez merql 0.4.1 ou une version ultérieure pour le CLI ; dans les versions précédentes, le binaire ne parvenait pas à trouver le chargeur automatique lorsque merql était installé comme dépendance.

Connexion à une base de données

merql fonctionne avec une petite interface d'adaptateur de connexion. Des adaptateurs intégrés sont fournis pour PDO et mysqli, ainsi que des pilotes de dialecte intégrés pour MySQL et 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() ouvre toute base de données compatible avec PDO, mais merql ne fournit des pilotes de dialecte que pour MySQL et SQLite. Avec un autre moteur, comme pgsql:, la connexion s'ouvre, mais Merql::init() lance Unsupported database driver: pgsql jusqu'à ce que vous enregistriez un pilote pour ce moteur. Voir Pilotes de base de données.

Capture d'un instantané

Un instantané enregistre chaque ligne de chaque table (ou d'un sous-ensemble de tables) sous forme d'empreintes et de données. Les empreintes permettent de détecter rapidement les changements ; les données complètes des lignes sont disponibles lorsqu'une comparaison est nécessaire.

PHP
use Merql\Merql;

Merql::init($connection);

// Snapshot all tables.
$snapshot = Merql::snapshot('baseline');

// Snapshot specific tables.
$snapshot = Merql::snapshot('baseline', ['posts', 'comments', 'users']);

Par défaut, les instantanés sont enregistrés sous forme de fichiers JSON dans .merql/snapshots/. Vous pouvez modifier cet emplacement :

PHP
use Merql\Snapshot\SnapshotStore;

SnapshotStore::setDirectory('/var/data/merql-snapshots');

Calcul d'une comparaison

Une comparaison examine deux instantanés et renvoie un ensemble de changements comprenant des insertions, des mises à jour et des suppressions. Les deux instantanés doivent déjà exister ; ici, current est un second instantané pris après la modification de la base de données.

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";
    }
}

Fusion à trois voies

L'opération principale. Elle prend trois instantanés nommés (base, ours, theirs) et fusionne les deux ensembles de changements.

« Base » correspond à l'état de départ des deux copies. « Ours » et « theirs » sont les deux copies après leurs modifications indépendantes ; chacune doit donc être capturée depuis la base de données qui la contient. Par exemple, lorsqu'un environnement de test a été copié depuis une base de données active :

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";

Ne prenez pas les trois instantanés successivement à partir d'une seule base de données. « Theirs » contiendrait alors déjà les changements de « ours », et chaque ligne insérée par « ours » apparaîtrait comme un conflit insert_insert.

Merql::merge() effectue les fusions au niveau des colonnes. Pour également fusionner à l'intérieur des valeurs TEXT et JSON, utilisez ThreeWayMerge avec un CellMergeConfig ; voir Fusion au niveau des cellules.

Gestion des conflits

Lorsque les deux côtés modifient une même colonne avec des valeurs différentes, merql signale un conflit. Vous pouvez résoudre les conflits automatiquement ou les examiner manuellement.

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";
    }
}

Application de la fusion

Une fois le résultat de fusion propre (sans conflit non résolu), appliquez-le à la base de données transmise à Merql::init().

Les opérations d'un résultat de fusion transforment l'état de base en état fusionné et incluent les changements des deux côtés. Merql::apply() les exécute toutes ; le résultat s'applique donc sans problème à une base de données qui contient encore l'état de base. Si vous l'appliquez à une base qui contient déjà les changements d'un côté, toute insertion de ce côté vise une ligne qui existe déjà, échoue et annule toute la fusion. Pour promouvoir les changements d'un côté vers une base qui contient déjà ceux de l'autre (par exemple, les modifications de l'environnement de test vers la base active), appliquez uniquement les opérations manquantes avec Application protégée.

PHP
$applied = Merql::apply($result);

echo "Rows affected: {$applied->rowsAffected()}\n";

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

L'appliqueur regroupe toutes les opérations dans une transaction. Si une instruction échoue, toute la fusion est annulée et l'erreur est renvoyée dans errors(). Dans ce cas, rowsAffected() compte toujours les instructions exécutées avant l'échec, même si elles ont été annulées (#4) ; consultez d'abord hasErrors().

Simulation

Prévisualisez les instructions SQL qu'une fusion générerait, sans l'exécuter.

PHP
use Merql\Apply\DryRun;

$statements = DryRun::generate($result);

foreach ($statements as $sql) {
    echo "{$sql};\n";
}

Correctif à deux voies

Lorsque vous ne disposez que d'une base et d'un ensemble de changements (sans modifications concurrentes « ours »), utilisez patch(). Cela équivaut à une fusion où « ours » est identique à la base.

PHP
$result = Merql::patch('base', 'changes');
// Always clean: no concurrent edits means no conflicts.
Merql::apply($result);

Utilisation de CLI

Toutes ces opérations sont également disponibles depuis la ligne de commande. Consultez la CLI Référence pour tous les détails.

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

Pour une fusion à trois voies, prenez chaque instantané depuis la base de données qui contient cet état, puis lancez la fusion :

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

Filtrage des tables et colonnes

Les instantanés peuvent exclure des tables (comme celles du cache ou des sessions) et ignorer des colonnes volatiles (comme les horodatages updated_at).

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']),
]);

Étapes suivantes