Primeros pasos
Esta guía explica cómo instalar el paquete, conectarse a una base de datos, capturar instantáneas, calcular comparaciones y realizar una fusión de tres vías.
Instalación
composer require merql/merql
merql requiere PHP 8.2+ con ext-mbstring y ext-zlib (ambos requisitos de la dependencia pitmaster/pitmaster). También necesita la extensión del adaptador que use: ext-pdo con ext-pdo_sqlite o ext-pdo_mysql para el adaptador PDO, o ext-mysqli para el adaptador mysqli. Composer solo los sugiere, así que compruebe que esté instalada la que necesita.
El paquete instala el archivo CLI como vendor/bin/merql. Use merql 0.4.1 o una versión posterior para el CLI; en versiones anteriores, el binario no podía encontrar el cargador automático cuando merql se instalaba como dependencia.
Conexión a una base de datos
merql funciona mediante una pequeña interfaz de adaptador de conexión. Incluye adaptadores para PDO y mysqli, con controladores de dialecto integrados para MySQL y 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() abre cualquier DSN compatible con PDO, pero merql solo incluye controladores de dialecto para MySQL y SQLite. Con otro motor, como pgsql:, la conexión se abre, pero Merql::init() lanza Unsupported database driver: pgsql hasta que registre un controlador para ese motor. Consulte Controladores de bases de datos.
Captura de una instantánea
Una instantánea registra cada fila de cada tabla (o de un subconjunto de tablas) como huellas digitales y datos. Las huellas digitales permiten detectar cambios rápidamente; los datos completos de las filas están disponibles cuando se necesita una comparación.
use Merql\Merql;
Merql::init($connection);
// Snapshot all tables.
$snapshot = Merql::snapshot('baseline');
// Snapshot specific tables.
$snapshot = Merql::snapshot('baseline', ['posts', 'comments', 'users']);
De forma predeterminada, las instantáneas se guardan como archivos JSON en .merql/snapshots/. Puede cambiar esta ubicación:
use Merql\Snapshot\SnapshotStore;
SnapshotStore::setDirectory('/var/data/merql-snapshots');
Cálculo de una comparación
Una comparación coteja dos instantáneas y devuelve un conjunto de cambios con inserciones, actualizaciones y eliminaciones. Ambas instantáneas deben existir de antemano; aquí current es una segunda instantánea capturada después de que cambiara la base de datos.
$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";
}
}
Fusión de tres vías
Es la operación principal. Toma tres instantáneas con nombre (base, ours, theirs) y fusiona los dos conjuntos de cambios.
Base es el estado del que partieron ambas copias. Ours y theirs son las dos copias tras modificarse de forma independiente, por lo que cada una debe capturarse de la base de datos que la contiene. Por ejemplo, si se creó un entorno de pruebas a partir de una base de datos en vivo:
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";
No capture las tres instantáneas de una misma base de datos, una tras otra. Theirs ya contendría los cambios de ours, y cada fila insertada por ours aparecería como un conflicto insert_insert.
Merql::merge() fusiona a nivel de columna. Para fusionar también dentro de valores TEXT y JSON, use ThreeWayMerge con un CellMergeConfig; consulte Fusión a nivel de celda.
Gestión de conflictos
Cuando ambos lados cambian la misma columna por valores distintos, merql genera un conflicto. Puede resolverlo automáticamente o examinarlo manualmente.
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";
}
}
Aplicación de la fusión
Cuando tenga un resultado de fusión limpio (sin conflictos sin resolver), aplíquelo a la base de datos indicada en Merql::init().
Las operaciones de un resultado de fusión transforman el estado base en el estado fusionado e incluyen los cambios de ambos lados. Merql::apply() las ejecuta todas, por lo que el resultado se aplica correctamente a una base de datos que aún conserva el estado base. Si se aplica a una base de datos que ya contiene los cambios de uno de los lados, cualquier inserción de ese lado afecta una fila existente, falla y revierte toda la fusión. Para trasladar los cambios de un lado a una base de datos que ya contiene los del otro (por ejemplo, los cambios del entorno de pruebas a la base de datos en vivo), aplique solo las operaciones que aún falten mediante Aplicación con protección.
$applied = Merql::apply($result);
echo "Rows affected: {$applied->rowsAffected()}\n";
if ($applied->hasErrors()) {
foreach ($applied->errors() as $error) {
echo "Error: {$error}\n";
}
}
El aplicador incluye todas las operaciones en una transacción. Si falla una instrucción, se revierte toda la fusión y el error se devuelve en errors(). En ese caso, rowsAffected() sigue contando las instrucciones que se ejecutaron antes del fallo, aunque se hayan revertido (#4); consulte hasErrors() primero.
Simulación
Previsualice las instrucciones SQL que generaría una fusión, sin ejecutarla.
use Merql\Apply\DryRun;
$statements = DryRun::generate($result);
foreach ($statements as $sql) {
echo "{$sql};\n";
}
Parche de dos vías
Si solo tiene una base y un conjunto de cambios (sin modificaciones concurrentes de «ours»), use patch(). Equivale a una fusión en la que ours no ha cambiado respecto de base.
$result = Merql::patch('base', 'changes');
// Always clean: no concurrent edits means no conflicts.
Merql::apply($result);
Uso de CLI
Todas estas operaciones también están disponibles desde la línea de comandos. Consulte la Referencia de CLI para obtener todos los detalles.
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
Para una fusión de tres vías, capture cada instantánea de la base de datos que contiene ese estado y, después, fusione:
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
Filtrado de tablas y columnas
Las instantáneas pueden excluir tablas (como las de caché o sesiones) e ignorar columnas volátiles (como las marcas de tiempo 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']),
]);
Pasos siguientes
- Referencia de CLI para consultar la interfaz completa de línea de comandos.
- Referencia de PHP API para consultar documentación detallada de todas las clases y métodos.