Primii pași
Acest ghid vă prezintă instalarea, conectarea la o bază de date, capturarea instantaneelor, calcularea diferențelor și efectuarea unei îmbinări în trei direcții.
Instalare
composer require merql/merql
merql necesită PHP 8.2+ cu ext-mbstring și ext-zlib (ambele necesare pentru
pitmaster/pitmaster). Aveți nevoie și de extensia pentru adaptorul folosit: ext-pdo cu ext-pdo_sqlite sau ext-pdo_mysql pentru adaptorul PDO, ori ext-mysqli pentru adaptorul mysqli. Composer doar le sugerează, așadar verificați dacă extensia necesară este instalată.
Pachetul instalează CLI ca vendor/bin/merql. Folosiți merql 0.4.1 sau o versiune ulterioară pentru CLI; în versiunile anterioare, binarul nu putea găsi autoloaderul când merql era instalat ca dependență.
Conectarea la o bază de date
merql funcționează printr-o interfață simplă de adaptor pentru conexiuni. Sunt incluse adaptoare pentru PDO și mysqli, precum și drivere de dialect pentru MySQL și 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() deschide orice DSN acceptat de PDO, însă merql include drivere de dialect doar pentru MySQL și SQLite. Cu un alt motor, precum pgsql:, conexiunea se deschide, dar Merql::init() aruncă Unsupported database driver: pgsql până când înregistrați un driver pentru acesta. Consultați Drivere de baze de date.
Capturarea unui instantaneu
Un instantaneu înregistrează fiecare rând din fiecare tabel (sau dintr-un subset de tabele) sub formă de amprente și date. Amprentele permit detectarea rapidă a modificărilor; datele complete ale rândurilor sunt disponibile când este nevoie de o comparație.
use Merql\Merql;
Merql::init($connection);
// Snapshot all tables.
$snapshot = Merql::snapshot('baseline');
// Snapshot specific tables.
$snapshot = Merql::snapshot('baseline', ['posts', 'comments', 'users']);
Instantaneele sunt salvate implicit ca fișiere JSON în .merql/snapshots/. Puteți schimba această locație:
use Merql\Snapshot\SnapshotStore;
SnapshotStore::setDirectory('/var/data/merql-snapshots');
Calcularea unei comparații
O comparație analizează două instantanee și returnează un set de modificări cu inserări, actualizări și ștergeri. Ambele instantanee trebuie să existe deja; aici current este un al doilea instantaneu, capturat după modificarea bazei de date.
$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";
}
}
Îmbinare în trei direcții
Operația principală. Preia trei instantanee cu nume (bază, versiunea noastră, versiunea celorlalți) și îmbină cele două seturi de modificări.
Baza este starea de la care au pornit ambele copii. Versiunile noastră și a celorlalți sunt cele două copii după ce au fost modificate independent, așadar fiecare trebuie capturată din baza de date care o conține. De exemplu, când un mediu sandbox a fost copiat dintr-o bază de date activă:
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";
Nu capturați toate cele trei instantanee succesiv dintr-o singură bază de date. În acest caz, versiunea celorlalți ar conține deja modificările noastre, iar fiecare rând inserat de noi ar apărea ca un conflict insert_insert.
Merql::merge() face îmbinarea la nivel de coloană. Pentru a îmbina și în interiorul valorilor TEXT și JSON, folosiți ThreeWayMerge cu un CellMergeConfig; consultați Îmbinare la nivel de celulă.
Gestionarea conflictelor
Când ambele părți modifică aceeași coloană cu valori diferite, merql semnalează un conflict. Puteți rezolva conflictele automat sau le puteți inspecta manual.
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";
}
}
Aplicarea îmbinării
După ce obțineți un rezultat al îmbinării curat (fără conflicte nerezolvate), aplicați-l bazei de date transmise către Merql::init().
Operațiile din rezultatul îmbinării transformă baza în starea îmbinată și includ modificările ambelor părți. Merql::apply() le execută pe toate, așadar aplicarea reușește pe o bază de date care încă păstrează starea de bază. Dacă le aplicați unei baze de date care conține deja modificările uneia dintre părți, orice inserare a acelei părți întâlnește un rând existent, eșuează și anulează întreaga îmbinare. Pentru a transfera modificările unei părți într-o bază de date care o conține deja pe cealaltă parte (de exemplu, modificările din sandbox în baza activă), aplicați doar operațiile care îi lipsesc folosind Aplicare condiționată.
$applied = Merql::apply($result);
echo "Rows affected: {$applied->rowsAffected()}\n";
if ($applied->hasErrors()) {
foreach ($applied->errors() as $error) {
echo "Error: {$error}\n";
}
}
Executorul încadrează toate operațiile într-o tranzacție. Dacă o instrucțiune eșuează, întreaga îmbinare este anulată, iar eroarea este returnată în errors(). În acest caz, rowsAffected() numără în continuare instrucțiunile executate înainte de eșec, chiar dacă au fost anulate (#4); verificați mai întâi hasErrors().
Simulare
Previzualizați SQL pe care le-ar genera o îmbinare, fără a le executa.
use Merql\Apply\DryRun;
$statements = DryRun::generate($result);
foreach ($statements as $sql) {
echo "{$sql};\n";
}
Patch în două direcții
Când aveți doar o bază și un set de modificări (fără modificări concurente din partea noastră), folosiți patch(). Este echivalent cu o îmbinare în care versiunea noastră nu diferă de bază.
$result = Merql::patch('base', 'changes');
// Always clean: no concurrent edits means no conflicts.
Merql::apply($result);
Folosirea CLI
Toate aceste operații sunt disponibile și din linia de comandă. Consultați CLI Referință pentru detalii complete.
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
Pentru o îmbinare în trei direcții, capturați fiecare instantaneu din baza de date care conține starea respectivă, apoi efectuați îmbinarea:
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
Filtrarea tabelelor și coloanelor
Instantaneele pot exclude tabele (precum cele pentru cache sau sesiuni) și pot ignora coloane volatile (precum marcajele temporale 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']),
]);
Pașii următori
- CLI Referință pentru întreaga interfață de linie de comandă.
- PHP API Referință pentru documentația detaliată a fiecărei clase și metode.