Pular para o conteúdo

Primeiros passos

Este guia apresenta a instalação, a conexão a um banco de dados, a captura de snapshots, o cálculo de diferenças e a realização de uma mesclagem de três vias.

Instalação

Shell
composer require merql/merql

merql requer PHP 8.2+ com ext-mbstring e ext-zlib (ambos exigidos pela dependência pitmaster/pitmaster). Você também precisa da extensão do adaptador utilizado: ext-pdo com ext-pdo_sqlite ou ext-pdo_mysql para o adaptador PDO, ou ext-mysqli para o adaptador mysqli. Composer apenas sugere essas extensões, então verifique se a necessária está instalada.

O pacote instala o CLI como vendor/bin/merql. Use merql 0.4.1 ou posterior para o CLI; nas versões anteriores, o binário não conseguia encontrar o autoloader quando o merql era instalado como dependência.

Conexão a um banco de dados

merql funciona por meio de uma pequena interface de adaptador de conexão. Há adaptadores integrados para PDO e mysqli, com drivers de dialeto integrados para MySQL e 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() abre qualquer DSN compatível com PDO, mas o merql inclui drivers de dialeto apenas para MySQL e SQLite. Com outro mecanismo, como pgsql:, a conexão é aberta, mas Merql::init() lança Unsupported database driver: pgsql até que você registre um driver para ele. Consulte Drivers de banco de dados.

Captura de um snapshot

Um snapshot registra cada linha de cada tabela (ou de um subconjunto de tabelas) como impressões digitais e dados. As impressões digitais permitem detectar alterações rapidamente; os dados completos das linhas ficam disponíveis quando é preciso calcular um diff.

PHP
use Merql\Merql;

Merql::init($connection);

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

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

Por padrão, os snapshots são salvos como arquivos JSON em .merql/snapshots/. Você pode alterar esse local:

PHP
use Merql\Snapshot\SnapshotStore;

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

Cálculo de um diff

Um diff compara dois snapshots e retorna um conjunto de alterações com inserções, atualizações e exclusões. Os dois snapshots já precisam existir; aqui, current é um segundo snapshot capturado depois que o banco de dados foi alterado.

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

Mesclagem de três vias

A operação principal. Recebe três snapshots nomeados (base, nosso, deles) e mescla os dois conjuntos de alterações.

A base é o estado inicial comum às duas cópias. Nosso e deles são as duas cópias após terem sido alteradas de forma independente, então cada uma precisa ser capturada do banco de dados que a contém. Por exemplo, quando uma sandbox foi copiada de um banco de dados ativo:

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

Não capture os três snapshots do mesmo banco de dados, em sequência. Nesse caso, deles já conteria as alterações de nosso, e cada linha inserida por nosso seria identificada como um conflito insert_insert.

Merql::merge() faz a mesclagem no nível das colunas. Para também mesclar dentro dos valores de TEXT e JSON, use ThreeWayMerge com um CellMergeConfig; consulte Mesclagem no nível da célula.

Como lidar com conflitos

Quando os dois lados alteram a mesma coluna para valores diferentes, o merql gera um conflito. Você pode resolver conflitos automaticamente ou inspecioná-los manualmente.

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

Aplicação da mesclagem

Depois de obter um resultado de mesclagem limpo (sem conflitos não resolvidos), aplique-o ao banco de dados passado para Merql::init().

As operações do resultado da mesclagem transformam a base no estado mesclado e incluem as alterações dos dois lados. Merql::apply() executa todas elas; por isso, a aplicação funciona sem problemas em um banco de dados que ainda contém o estado base. Se aplicado a um banco de dados que já contém as alterações de um dos lados, qualquer inserção desse lado encontrará uma linha existente, falhará e reverterá toda a mesclagem. Para incorporar as alterações de um lado a um banco de dados que já contém as alterações do outro (por exemplo, alterações da sandbox para o ambiente ativo), aplique apenas as operações que faltam usando Aplicação protegida.

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

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

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

O aplicador envolve todas as operações em uma transação. Se alguma instrução falhar, toda a mesclagem será revertida e o erro será retornado em errors(). Nesse caso, rowsAffected() ainda conta as instruções executadas antes da falha, embora tenham sido revertidas (#4); verifique primeiro hasErrors().

Simulação

Visualize o SQL que uma mesclagem geraria, sem executá-la.

PHP
use Merql\Apply\DryRun;

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

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

Patch de duas vias

Quando você só tem uma base e um conjunto de alterações (sem modificações simultâneas de “nosso”), use patch(). Isso equivale a uma mesclagem em que nosso permanece inalterado em relação à base.

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

Usando o CLI

Todas essas operações também estão disponíveis na linha de comando. Consulte a ReferênciaCLI para ver todos os detalhes.

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

Em uma mesclagem de três vias, capture cada snapshot do banco de dados que contém aquele estado e, em seguida, faça a mesclagem:

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

Filtragem de tabelas e colunas

Os snapshots podem excluir tabelas (como tabelas de cache ou sessão) e ignorar colunas voláteis (como marcas de data e hora 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']),
]);

Próximas etapas