Merql

PHP API Reference

Full reference for all public classes and methods in merql.

Merql (facade)

Merql\Merql is the static entry point for common operations. It wraps the lower-level classes and manages snapshot persistence automatically.

use Merql\Connection;
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;
use Merql\Merql;

$connection = Connection::sqlite(':memory:');
Merql::init($connection, identityRules: new IdentityRuleSet([
    'options' => IdentityRule::natural(['option_name']),
]));

init(DatabaseConnection $connection, ?Driver $driver = null, ?IdentityRuleSet $identityRules = null): void

Initialize the facade with a database connection. Must be called before snapshot() or apply(). Pass a driver to override auto-detection, or an identity rule set to override per-table row identity during snapshot capture.

snapshot(string $name, array $tables = []): Snapshot

Capture the current database state and persist it to disk. Pass an array of table names to snapshot specific tables, or omit for all tables.

$snapshot = Merql::snapshot('baseline');
$snapshot = Merql::snapshot('baseline', ['posts', 'users']);

diff(string $base, string $current): Changeset

Load two named snapshots and compute the changeset between them. Does not require init() since it works from persisted snapshots.

$changeset = Merql::diff('before', 'after');

merge(string $base, string $ours, string $theirs): MergeResult

Three-way merge of three named snapshots. Returns a result containing clean operations and any conflicts.

$result = Merql::merge('base', 'ours', 'theirs');

patch(string $base, string $changes): MergeResult

Two-way merge: apply changes onto base. Equivalent to merge($base, $base, $changes). Always produces a clean result because there are no concurrent edits.

$result = Merql::patch('base', 'changes');

apply(MergeResult $result): ApplyResult

Execute a clean merge result against the database. Requires init(). Throws ConflictException if unresolved conflicts remain.

$applied = Merql::apply($result);
echo $applied->rowsAffected();

plan(string $id, string $base, string $ours, string $theirs, array $metadata = []): MergePlan

Build a UI-ready merge plan from three named snapshots. The plan includes stable operation IDs, change group IDs, operation context, conflicts, schema mismatches, a summary, metadata, and a plan hash.

$plan = Merql::plan('plan-1', 'base', 'ours', 'theirs', [
    'source' => 'review-ui',
]);

applyGuarded(MergeResult $result, Snapshot $expectedLive): ApplyResult

Apply a clean merge result with optimistic live-row preconditions. Updates and deletes require current live rows to match $expectedLive; inserts require that the target identity does not already exist.

use Merql\Snapshot\SnapshotStore;

$applied = Merql::applyGuarded($selectedResult, SnapshotStore::load('theirs'));

reset(): void

Clear the internal connection and Snapshotter instances. Primarily used in tests to reset state between runs.


Connection

Merql\Connection builds connection adapters with sensible defaults. PDO connections use exceptions on error, associative fetch mode, no emulated prepares, and stringified fetches. The mysqli adapter normalizes fetched values to the same string-or-null shape.

mysql(string $host, string $database, string $username, string $password = '', int $port = 3306, string $charset = 'utf8mb4'): PdoDatabaseConnection

use Merql\Connection;

$connection = Connection::mysql('127.0.0.1', 'my_app', 'root', 'secret');

mysqli(string $host, string $database, string $username, string $password = '', int $port = 3306, string $charset = 'utf8mb4'): MysqliDatabaseConnection

$connection = Connection::mysqli('127.0.0.1', 'my_app', 'root', 'secret');

sqlite(string $path = ':memory:'): PdoDatabaseConnection

Creates a SQLite connection. Enables foreign keys automatically via PRAGMA foreign_keys = ON.

$connection = Connection::sqlite('/var/data/app.sqlite');
$connection = Connection::sqlite(':memory:');

fromDsn(string $dsn, string $username = '', string $password = ''): PdoDatabaseConnection

Connect using any PDO DSN string.

$connection = Connection::fromDsn('mysql:host=localhost;dbname=app', 'root', 'pass');

fromPdo(PDO $pdo): PdoDatabaseConnection

Wrap an existing PDO instance.

fromMysqli(mysqli $mysqli, string $charset = 'utf8mb4'): MysqliDatabaseConnection

Wrap an existing mysqli instance.

Adapter contract

All adapters implement Merql\Database\DatabaseConnection:

interface DatabaseConnection
{
    public function driverName(): string;
    public function execute(string $sql, array $params = []): int;
    public function query(string $sql, array $params = []): array;
    public function scalar(string $sql, array $params = []): ?string;
    public function beginTransaction(): void;
    public function commit(): void;
    public function rollBack(): void;
    public function lastInsertId(): string;
}

execute(), query(), and scalar() accept positional list<scalar|null> parameters for ? placeholders. query() returns list<array<string, scalar|null>>; values are normalized to the same string-or-null shape across PDO and mysqli adapters.


Snapshotter

Merql\Snapshot\Snapshotter captures database state. Unlike the facade, it gives you full control over filters and does not auto-persist.

__construct(DatabaseConnection $connection, ?Driver $driver = null, ?IdentityRuleSet $identityRules = null)

The driver is auto-detected from the connection identity if not provided. Pass an IdentityRuleSet when table-specific row identity should override schema defaults.

capture(string $name, array $tables = [], array $filters = []): Snapshot

Capture current state. Pass table names to limit scope. Pass filter objects to exclude tables, columns, or rows.

use Merql\Snapshot\Snapshotter;
use Merql\Filter\TableFilter;
use Merql\Filter\ColumnFilter;
use Merql\Filter\RowFilter;
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;

$snapshotter = new Snapshotter(
    $connection,
    identityRules: new IdentityRuleSet([
        'wp_options' => IdentityRule::natural(['option_name']),
    ]),
);

// All tables.
$snapshot = $snapshotter->capture('full');

// Specific tables with filters.
$snapshot = $snapshotter->capture('filtered', ['posts', 'users'], [
    TableFilter::exclude(['cache_*', 'sessions']),
    ColumnFilter::ignore(['updated_at', 'created_at']),
    RowFilter::create(fn(string $table, array $row) => $row['status'] !== 'trash'),
]);

captureAliased(string $name, array $physicalToCanonicalTableMap, array $filters = []): Snapshot

Capture physical tables under canonical table names. Use this when the same logical table has different physical names across environments.

$snapshot = $snapshotter->captureAliased('sandbox', [
    'wp_onumia_abcd_posts' => 'wp_posts',
    'wp_onumia_abcd_postmeta' => 'wp_postmeta',
]);

The resulting snapshot stores canonical table names while preserving the physical table schema, configured identity columns, row keys, and filters.

fromData(string $name, array $tableData): Snapshot (static)

Build a snapshot from raw data without a database connection. Useful for testing or when importing data from another source.

use Merql\Snapshot\Snapshotter;
use Merql\Snapshot\TableSnapshotData;
use Merql\Schema\TableSchema;

$schema = new TableSchema('posts', ['id' => 'int', 'title' => 'varchar'], ['id']);

$snapshot = Snapshotter::fromData('test', [
    'posts' => new TableSnapshotData($schema, [
        ['id' => '1', 'title' => 'Hello'],
        ['id' => '2', 'title' => 'World'],
    ], ['id']),
]);

Snapshot

Merql\Snapshot\Snapshot is a readonly value object representing a complete database state.

$snapshot->name;                    // string: snapshot identifier
$snapshot->tables;                  // array<string, TableSnapshot>
$snapshot->tableNames();            // list<string>
$snapshot->hasTable('posts');       // bool
$snapshot->getTable('posts');       // ?TableSnapshot

TableSnapshot

Merql\Snapshot\TableSnapshot holds one table's rows, fingerprints, and schema.

$table = $snapshot->getTable('posts');

$table->schema;                    // TableSchema
$table->identityColumns;           // list<string>: columns used for row identity
$table->fingerprints;              // array<string, string>: row key to hash
$table->rows;                      // array<string, array<string, mixed>>

$table->rowKeys();                 // list<string>
$table->rowCount();                // int
$table->hasRow('42');              // bool
$table->getRow('42');              // ?array<string, mixed>
$table->getFingerprint('42');      // ?string

TableSnapshotData

Merql\Snapshot\TableSnapshotData is input data for Snapshotter::fromData().

use Merql\Snapshot\TableSnapshotData;
use Merql\Schema\TableSchema;

$data = new TableSnapshotData(
    schema: new TableSchema('posts', ['id' => 'int', 'title' => 'varchar'], ['id']),
    rows: [
        ['id' => '1', 'title' => 'First Post'],
    ],
    identityColumns: ['id'],
);

IdentityRule

Merql\Identity\IdentityRule defines the columns used to build row keys.

use Merql\Identity\IdentityRule;

$primary = IdentityRule::primary(['id']);
$natural = IdentityRule::natural(['option_name']);
$composite = IdentityRule::composite(['object_id', 'term_taxonomy_id']);
$content = IdentityRule::content(['name', 'value']);

$key = $natural->key(['option_name' => 'siteurl']);
// "siteurl"

forSchema(TableSchema $schema): IdentityRule (static)

Resolve the default identity rule from schema: primary key, first unique key, then content fallback.

toArray(): array

Serialize the rule as ['type' => string, 'columns' => list<string>].

fromArray(array $data): IdentityRule (static)

Hydrate a rule from serialized data.


IdentityRuleSet

Merql\Identity\IdentityRuleSet stores table-specific identity overrides.

use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;

$rules = new IdentityRuleSet([
    'wp_options' => IdentityRule::natural(['option_name']),
]);

$rule = $rules->ruleFor($schema);

with(string $table, IdentityRule $rule): IdentityRuleSet

Return a new rule set with one table override added.

conflictsFor(string $table, IdentityRule $rule, array $rows): array

Return IdentityConflict entries when multiple rows produce the same key for a rule.

toArray(): array

Serialize table rules as a JSON-safe array.

fromArray(array $data): IdentityRuleSet (static)

Hydrate a rule set from serialized data.


SnapshotStore

Merql\Snapshot\SnapshotStore persists and loads snapshots as JSON files.

setDirectory(string $directory): void

Set the storage directory. Default is .merql/snapshots.

save(Snapshot $snapshot): void

Write a snapshot to disk.

load(string $name): Snapshot

Load a snapshot by name. Throws SnapshotException if not found.

exists(string $name): bool

Check whether a snapshot file exists.

delete(string $name): void

Remove a snapshot file.


Differ

Merql\Diff\Differ compares two snapshots and produces a changeset.

diff(Snapshot $base, Snapshot $current): Changeset

use Merql\Diff\Differ;

$differ = new Differ();
$changeset = $differ->diff($baseSnapshot, $currentSnapshot);

valuesEqual(mixed $a, mixed $b): bool (static)

Compare two values using string coercion. NULL is treated as a distinct value: NULL === NULL is true, NULL === '' is false.


Changeset

Merql\Diff\Changeset is a readonly collection of row-level operations.

$changeset->inserts();             // list<RowInsert>
$changeset->updates();             // list<RowUpdate>
$changeset->deletes();             // list<RowDelete>
$changeset->isEmpty();             // bool
$changeset->count();               // int: total operations
$changeset->forTable('posts');     // Changeset: filtered to one table

RowInsert, RowUpdate, RowDelete

Readonly value objects representing individual row changes.

RowInsert

$insert->table;                    // string
$insert->rowKey;                   // string
$insert->values;                   // array<string, mixed>

RowUpdate

$update->table;                    // string
$update->rowKey;                   // string
$update->columnDiffs;              // list<ColumnDiff>
$update->fullRow;                  // array<string, mixed>: complete current row
$update->columnDiffMap();          // array<string, ColumnDiff>: keyed by column name

RowDelete

$delete->table;                    // string
$delete->rowKey;                   // string
$delete->oldValues;                // array<string, mixed>: values before deletion

ColumnDiff

Merql\Diff\ColumnDiff represents a single column change within a row update.

$diff->column;                     // string
$diff->oldValue;                   // mixed
$diff->newValue;                   // mixed

ThreeWayMerge

Merql\Merge\ThreeWayMerge is the core merge algorithm.

__construct(?CellMergeConfig $cellMergeConfig = null)

Pass a CellMergeConfig to enable cell-level merging for TEXT and JSON columns.

use Merql\Merge\ThreeWayMerge;
use Merql\CellMerge\CellMergeConfig;

// Default: column-level merge only.
$merge = new ThreeWayMerge();

// With cell-level merge for TEXT and JSON columns.
$merge = new ThreeWayMerge(CellMergeConfig::auto());

merge(Snapshot $base, Snapshot $ours, Snapshot $theirs): MergeResult

Perform a three-way merge. Computes changesets internally (base to ours, base to theirs) and merges them.

$result = $merge->merge($baseSnapshot, $oursSnapshot, $theirsSnapshot);

patch(Snapshot $base, Snapshot $changes): MergeResult

Two-way merge shortcut. Equivalent to merge($base, $base, $changes).

schemaMismatches(): array

Returns SchemaException[] for any schema differences detected during the last merge (columns added or removed between snapshots).


MergeResult

Merql\Merge\MergeResult is a readonly value object containing the merge outcome.

$result->isClean();                // bool: no conflicts
$result->operations();             // list<MergeOperation>
$result->conflicts();              // list<Conflict>
$result->operationCount();         // int
$result->conflictCount();          // int
$result->baseSnapshot();           // ?Snapshot
$result->schemaMismatches();       // list<SchemaException>
$result->hasSchemaMismatches();    // bool

MergePlan

Merql\Plan\MergePlan is a serializable review model for a merge.

$plan->id;                        // string
$plan->baseSnapshot;              // string
$plan->oursSnapshot;              // string
$plan->theirsSnapshot;            // string
$plan->summary;                   // MergePlanSummary
$plan->changeGroups;              // list<MergePlanChangeGroup>
$plan->operations;                // list<MergePlanOperation>
$plan->conflicts;                 // list<MergePlanConflict>
$plan->schemaMismatches;          // list<string>
$plan->hash;                      // string
$plan->metadata;                  // array<string, mixed>

Use MergePlanSerializer::toJson() and ::fromJson() for stable JSON roundtrips.

Selections

ChangeGroupSelection stores staged change group IDs. Convert it to an OperationSelection when building selected merge results or rollback plans.

use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;

$selection = ChangeGroupSelection::fromIds([$groupId]);
$operationSelection = $selection->toOperationSelection($plan);

$selected = (new SelectedMergeResultFactory())
    ->fromOperationSelection($plan, $operationSelection);

RollbackPlan

Merql\Rollback\RollbackPlan stores inverse operations and drift expectations for a selected apply.

use Merql\Rollback\RollbackPlanBuilder;
use Merql\Rollback\RollbackPlanSerializer;

$rollback = (new RollbackPlanBuilder())->build(
    'rollback-1',
    $plan,
    $operationSelection,
    $liveBeforeRows,
);

$json = RollbackPlanSerializer::toJson($rollback);

Use RollbackDrift before applying rollback, then RollbackPlanApplier to run the inverse operations.


MergeOperation

Merql\Merge\MergeOperation is a single resolved operation from the merge.

$op->type;                         // string: 'insert' | 'update' | 'delete'
$op->table;                        // string
$op->rowKey;                       // string
$op->values;                       // array<string, mixed>
$op->source;                       // string: 'ours' | 'theirs' | 'merged'

Type constants:

MergeOperation::TYPE_INSERT;       // 'insert'
MergeOperation::TYPE_UPDATE;       // 'update'
MergeOperation::TYPE_DELETE;       // 'delete'

Conflict

Merql\Merge\Conflict represents an unresolved merge conflict.

$conflict->table();                // string
$conflict->rowKey();               // string
$conflict->type();                 // string: 'update_update' | 'update_delete' | 'delete_update' | 'insert_insert'
$conflict->column();               // ?string (null for structural conflicts)
$conflict->oursValue();            // mixed
$conflict->theirsValue();          // mixed
$conflict->baseValue();            // mixed
$conflict->primaryKey(['id']);     // array<string, string>: decoded from rowKey

Conflict types

TypeMeaning
update_updateBoth sides changed the same column to different values
update_deleteOurs updated a row that theirs deleted
delete_updateOurs deleted a row that theirs updated
insert_insertBoth sides inserted a row with the same primary key

ConflictResolver

Merql\Merge\ConflictResolver applies a resolution policy to all conflicts in a merge result.

resolve(MergeResult $result, ConflictPolicy $policy): MergeResult (static)

Returns a new MergeResult with conflicts resolved according to the policy.

use Merql\Merge\ConflictResolver;
use Merql\Merge\ConflictPolicy;

// Accept all "theirs" values when conflicts arise.
$resolved = ConflictResolver::resolve($result, ConflictPolicy::TheirsWins);

// Accept all "ours" values.
$resolved = ConflictResolver::resolve($result, ConflictPolicy::OursWins);

// Return unchanged (for manual inspection).
$resolved = ConflictResolver::resolve($result, ConflictPolicy::Manual);

ConflictPolicy

Merql\Merge\ConflictPolicy is a backed enum with three cases.

use Merql\Merge\ConflictPolicy;

ConflictPolicy::OursWins;          // Resolve all conflicts in favor of "ours"
ConflictPolicy::TheirsWins;        // Resolve all conflicts in favor of "theirs"
ConflictPolicy::Manual;            // Leave conflicts unresolved for manual handling

ColumnMerge

Merql\Merge\ColumnMerge performs per-column merge logic for a single row. Used internally by ThreeWayMerge.

merge(string $table, string $rowKey, array $base, array $ours, array $theirs, ?CellMergeConfig $cellMergeConfig = null, array $columnTypes = []): array (static)

Returns ['values' => array, 'conflicts' => list<Conflict>].

use Merql\Merge\ColumnMerge;

$result = ColumnMerge::merge(
    'posts',
    '42',
    ['title' => 'Hello', 'status' => 'draft'],     // base
    ['title' => 'Hello', 'status' => 'published'],  // ours
    ['title' => 'New Title', 'status' => 'draft'],   // theirs
);

// $result['values'] = ['title' => 'New Title', 'status' => 'published']
// $result['conflicts'] = [] (clean merge)

Applier

Merql\Apply\Applier executes a merge result against a database.

__construct(DatabaseConnection $connection, ?Driver $driver = null)

apply(MergeResult $result, ?Snapshot $base = null): ApplyResult

Executes all operations in a single transaction. Rolls back on any error. Throws ConflictException if the result has unresolved conflicts.

use Merql\Apply\Applier;

$applier = new Applier($connection);
$applied = $applier->apply($result);

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

Foreign key ordering is handled automatically: inserts are sorted in dependency order, deletes in reverse dependency order.


ApplyResult

Merql\Apply\ApplyResult is a readonly value object returned by the applier.

$applied->rowsAffected();         // int
$applied->errors();               // list<string>
$applied->hasErrors();            // bool

DryRun

Merql\Apply\DryRun generates human-readable SQL without executing anything.

generate(MergeResult $result, ?Snapshot $base = null, array $fkDependencies = [], ?Driver $driver = null): array (static)

Returns a list of SQL strings with values inlined (for display purposes only).

use Merql\Apply\DryRun;

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

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

// Output:
// INSERT INTO `posts` (`id`, `title`) VALUES ('10', 'New Post');
// UPDATE `posts` SET `title` = 'Updated' WHERE `id` = '5';
// DELETE FROM `comments` WHERE `id` = '3';

SqlGenerator

Merql\Apply\SqlGenerator generates parameterized SQL statements. Used internally by both Applier and DryRun.

generate(MergeResult $result, ?Snapshot $base = null, array $fkDependencies = [], ?Driver $driver = null): array (static)

Returns list<array{sql: string, params: list<scalar|null>}>.

use Merql\Apply\SqlGenerator;

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

foreach ($statements as $stmt) {
    $connection->execute($stmt['sql'], $stmt['params']);
}

CellMergeConfig

Merql\CellMerge\CellMergeConfig configures cell-level merge strategies for specific columns or column types.

auto(): self (static)

Convenience factory that assigns TextCellMerger to TEXT/LONGTEXT columns and JsonCellMerger to JSON columns.

use Merql\CellMerge\CellMergeConfig;

$config = CellMergeConfig::auto();

forColumn(string $column, CellMerger $merger): self

Assign a merger to a specific column. Use table.column for qualified names or just column for all tables.

use Merql\CellMerge\CellMergeConfig;
use Merql\CellMerge\TextCellMerger;

$config = (new CellMergeConfig())
    ->forColumn('posts.content', new TextCellMerger())
    ->forColumn('description', new TextCellMerger());

forType(string $typePattern, CellMerger $merger): self

Assign a merger to all columns matching a type pattern (case-insensitive substring match).

use Merql\CellMerge\CellMergeConfig;
use Merql\CellMerge\JsonCellMerger;

$config = (new CellMergeConfig())
    ->forType('json', new JsonCellMerger());

getMerger(string $table, string $column, string $columnType = ''): CellMerger

Look up the merger for a given table, column, and type. Priority: qualified column name, unqualified column name, type pattern, default (opaque).


CellMerger (interface)

Merql\CellMerge\CellMerger is the interface for cell-level merge strategies.

use Merql\CellMerge\CellMerger;
use Merql\CellMerge\CellMergeResult;

interface CellMerger
{
    public function merge(mixed $base, mixed $ours, mixed $theirs): CellMergeResult;
}

Three built-in implementations are provided.

TextCellMerger

Merql\CellMerge\TextCellMerger merges multi-line text using a three-way line-by-line diff (the same algorithm git uses for file content). When both sides edit different lines, the merge is clean. When both sides edit the same line differently, it reports a conflict.

use Merql\CellMerge\TextCellMerger;

$merger = new TextCellMerger();
$result = $merger->merge(
    "line 1\nline 2\nline 3",
    "line 1\nline 2 modified\nline 3",
    "line 1\nline 2\nline 3 modified",
);
// $result->clean === true
// $result->value === "line 1\nline 2 modified\nline 3 modified"

JsonCellMerger

Merql\CellMerge\JsonCellMerger merges JSON values by comparing top-level keys independently. Each key is merged using three-way logic. Nested objects are compared as opaque JSON strings (not recursively merged).

use Merql\CellMerge\JsonCellMerger;

$merger = new JsonCellMerger();
$result = $merger->merge(
    '{"a": 1, "b": 2}',
    '{"a": 10, "b": 2}',        // changed "a"
    '{"a": 1, "b": 20}',         // changed "b"
);
// $result->clean === true
// $result->value === '{"a":10,"b":20}'

OpaqueCellMerger

Merql\CellMerge\OpaqueCellMerger is the default. It treats values as opaque strings and always reports a conflict when they differ.


CellMergeResult

Merql\CellMerge\CellMergeResult is the return type of all cell mergers.

$result->clean;                    // bool: true if merge succeeded
$result->value;                    // mixed: merged value (or ours on conflict)
$result->conflicts;                // int: number of conflicts

// Factory methods.
CellMergeResult::resolved($value);           // clean merge
CellMergeResult::conflict($oursValue, $n);   // conflict with count

TableSchema

Merql\Schema\TableSchema is a readonly representation of table structure.

use Merql\Schema\TableSchema;

$schema = new TableSchema(
    name: 'posts',
    columns: ['id' => 'int', 'title' => 'varchar(255)', 'content' => 'text'],
    primaryKey: ['id'],
    uniqueKeys: [['slug']],
);

$schema->name;                     // string
$schema->columns;                  // array<string, string>: name to type
$schema->primaryKey;               // list<string>
$schema->uniqueKeys;               // list<list<string>>
$schema->hasPrimaryKey();          // bool
$schema->hasUniqueKeys();          // bool
$schema->columnNames();            // list<string>

Driver system

merql uses a pluggable driver system to abstract database-specific operations. Drivers are auto-detected from the connection driver identity.

Driver (interface)

Merql\Driver\Driver defines the contract all database drivers implement.

interface Driver
{
    public function quoteIdentifier(string $name): string;
    public function listTables(DatabaseConnection $connection): array;
    public function readSchema(DatabaseConnection $connection, string $table): TableSchema;
    public function readForeignKeys(DatabaseConnection $connection): array;
    public function selectAll(string $table): string;
}

Built-in implementations: MysqlDriver and SqliteDriver.

DriverFactory

Merql\Driver\DriverFactory auto-detects the driver from a connection.

use Merql\Driver\DriverFactory;

$driver = DriverFactory::create($connection);

Register a custom driver for other databases:

DriverFactory::register('pgsql', MyPostgresDriver::class);

Filters

TableFilter

Merql\Filter\TableFilter includes or excludes tables using glob patterns.

use Merql\Filter\TableFilter;

$filter = TableFilter::exclude(['cache_*', 'sessions', 'temp_*']);
$filter = TableFilter::include(['posts', 'users', 'comments']);

ColumnFilter

Merql\Filter\ColumnFilter removes specific columns from snapshots.

use Merql\Filter\ColumnFilter;

$filter = ColumnFilter::ignore(['updated_at', 'created_at', 'modified_date']);

RowFilter

Merql\Filter\RowFilter filters rows using a predicate function.

use Merql\Filter\RowFilter;

$filter = RowFilter::create(function (string $table, array $row): bool {
    // Exclude trashed posts.
    if ($table === 'posts' && ($row['status'] ?? '') === 'trash') {
        return false;
    }
    return true;
});

Exceptions

SnapshotException

Merql\Exceptions\SnapshotException is thrown when a snapshot cannot be found or has an invalid name.

SnapshotException::notFound('baseline', '/path/to/baseline.json');

MergeException

Merql\Exceptions\MergeException is thrown when a required snapshot is missing for a merge.

MergeException::snapshotRequired('base');

ConflictException

Merql\Exceptions\ConflictException is thrown when attempting to apply a merge result that has unresolved conflicts.

ConflictException::unresolved(3);
// Message: "Cannot apply merge with 3 unresolved conflict(s)"

SchemaException

Merql\Exceptions\SchemaException is thrown (or collected) when schemas differ between snapshots.

SchemaException::mismatch('posts', 'column "tags" exists in current but not in base');

Schema mismatches do not stop the merge. They are collected and available via $result->schemaMismatches() for inspection.

On this page

PHP API ReferenceMerql (facade)init(DatabaseConnection $connection, ?Driver $driver = null, ?IdentityRuleSet $identityRules = null): voidsnapshot(string $name, array $tables = []): Snapshotdiff(string $base, string $current): Changesetmerge(string $base, string $ours, string $theirs): MergeResultpatch(string $base, string $changes): MergeResultapply(MergeResult $result): ApplyResultplan(string $id, string $base, string $ours, string $theirs, array $metadata = []): MergePlanapplyGuarded(MergeResult $result, Snapshot $expectedLive): ApplyResultreset(): voidConnectionmysql(string $host, string $database, string $username, string $password = '', int $port = 3306, string $charset = 'utf8mb4'): PdoDatabaseConnectionmysqli(string $host, string $database, string $username, string $password = '', int $port = 3306, string $charset = 'utf8mb4'): MysqliDatabaseConnectionsqlite(string $path = ':memory:'): PdoDatabaseConnectionfromDsn(string $dsn, string $username = '', string $password = ''): PdoDatabaseConnectionfromPdo(PDO $pdo): PdoDatabaseConnectionfromMysqli(mysqli $mysqli, string $charset = 'utf8mb4'): MysqliDatabaseConnectionAdapter contractSnapshotter__construct(DatabaseConnection $connection, ?Driver $driver = null, ?IdentityRuleSet $identityRules = null)capture(string $name, array $tables = [], array $filters = []): SnapshotcaptureAliased(string $name, array $physicalToCanonicalTableMap, array $filters = []): SnapshotfromData(string $name, array $tableData): Snapshot (static)SnapshotTableSnapshotTableSnapshotDataIdentityRuleforSchema(TableSchema $schema): IdentityRule (static)toArray(): arrayfromArray(array $data): IdentityRule (static)IdentityRuleSetwith(string $table, IdentityRule $rule): IdentityRuleSetconflictsFor(string $table, IdentityRule $rule, array $rows): arraytoArray(): arrayfromArray(array $data): IdentityRuleSet (static)SnapshotStoresetDirectory(string $directory): voidsave(Snapshot $snapshot): voidload(string $name): Snapshotexists(string $name): booldelete(string $name): voidDifferdiff(Snapshot $base, Snapshot $current): ChangesetvaluesEqual(mixed $a, mixed $b): bool (static)ChangesetRowInsert, RowUpdate, RowDeleteRowInsertRowUpdateRowDeleteColumnDiffThreeWayMerge__construct(?CellMergeConfig $cellMergeConfig = null)merge(Snapshot $base, Snapshot $ours, Snapshot $theirs): MergeResultpatch(Snapshot $base, Snapshot $changes): MergeResultschemaMismatches(): arrayMergeResultMergePlanSelectionsRollbackPlanMergeOperationConflictConflict typesConflictResolverresolve(MergeResult $result, ConflictPolicy $policy): MergeResult (static)ConflictPolicyColumnMergemerge(string $table, string $rowKey, array $base, array $ours, array $theirs, ?CellMergeConfig $cellMergeConfig = null, array $columnTypes = []): array (static)Applier__construct(DatabaseConnection $connection, ?Driver $driver = null)apply(MergeResult $result, ?Snapshot $base = null): ApplyResultApplyResultDryRungenerate(MergeResult $result, ?Snapshot $base = null, array $fkDependencies = [], ?Driver $driver = null): array (static)SqlGeneratorgenerate(MergeResult $result, ?Snapshot $base = null, array $fkDependencies = [], ?Driver $driver = null): array (static)CellMergeConfigauto(): self (static)forColumn(string $column, CellMerger $merger): selfforType(string $typePattern, CellMerger $merger): selfgetMerger(string $table, string $column, string $columnType = ''): CellMergerCellMerger (interface)TextCellMergerJsonCellMergerOpaqueCellMergerCellMergeResultTableSchemaDriver systemDriver (interface)DriverFactoryFiltersTableFilterColumnFilterRowFilterExceptionsSnapshotExceptionMergeExceptionConflictExceptionSchemaException