PHPackages                             merql/merql - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. merql/merql

ActiveLibrary[Database &amp; ORM](/categories/database)

merql/merql
===========

Pure PHP three-way database merge with column-level conflict resolution. Git-style merge semantics applied to MySQL and SQLite tables.

v0.4.0(1mo ago)1206MITPHPPHP ^8.2CI passing

Since Apr 14Pushed 1mo agoCompare

[ Source](https://github.com/inline0/merql)[ Packagist](https://packagist.org/packages/merql/merql)[ Docs](https://github.com/inline0/merql)[ RSS](/packages/merql-merql/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (4)Dependencies (8)Versions (6)Used By (0)

    ![Merql](./docs/public/logo-light.svg)

 Pure PHP three-way database merge with column-level conflict resolution

 [![CI](https://github.com/inline0/merql/actions/workflows/ci.yml/badge.svg)](https://github.com/inline0/merql/actions/workflows/ci.yml) [![Packagist](https://camo.githubusercontent.com/03582330317b0b39891b73b650292b09ec148d1c8acfcc1febf28c28d4086688/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d6572716c2f6d6572716c2e737667)](https://packagist.org/packages/merql/merql) [![license](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](https://github.com/inline0/merql/blob/main/LICENSE)

---

What is Merql?
--------------

[](#what-is-merql)

Merql is a pure PHP three-way database merge engine. It takes three database states (base, ours, theirs), computes changesets, and produces a merged result with conflict detection. Git-style merge semantics applied to MySQL and SQLite tables, at the column and cell level.

**The problem:** when two systems independently modify the same database, reconciling the changes requires understanding what each side added, changed, and deleted relative to a common ancestor. Without a common base, you cannot distinguish "added" from "unchanged."

**Merql solves this** by applying the same three-way merge algorithm that git uses for files, but operating on rows and columns instead of lines:

- Snapshot database state with row fingerprinting for fast change detection
- Compute per-column changesets between any two snapshots
- Three-way merge with column-level conflict resolution
- UI-ready merge plans with stable operation and change group IDs
- Selected operation apply for staging workflows
- Rollback plan generation with drift checks
- Cell-level merge for TEXT (line-by-line via Myers diff) and JSON (key-by-key) columns
- Parameterized SQL generation with FK-aware ordering
- Guarded apply with optimistic live-row preconditions
- Connection adapters for PDO and mysqli, with built-in MySQL and SQLite dialects

Quick Start
-----------

[](#quick-start)

```
composer require merql/merql
```

```
use Merql\Connection;
use Merql\Merql;

// Initialize with a MerQL connection adapter.
Merql::init(Connection::sqlite('/path/to/db.sqlite'));

// Capture database state at key points.
Merql::snapshot('base');
// ... ours makes changes ...
Merql::snapshot('ours');
// ... theirs makes changes ...
Merql::snapshot('theirs');

// Three-way merge.
$result = Merql::merge('base', 'ours', 'theirs');

if ($result->isClean()) {
    Merql::apply($result);
} else {
    foreach ($result->conflicts() as $conflict) {
        echo "{$conflict->table()}.{$conflict->column()}: "
            . "ours={$conflict->oursValue()}, theirs={$conflict->theirsValue()}\n";
    }
}
```

Merge Plans And Rollback
------------------------

[](#merge-plans-and-rollback)

Use merge plans when a host application needs to inspect, stage, or selectively apply database changes before writing to the target database.

```
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Rollback\RollbackPlanBuilder;
use Merql\Snapshot\SnapshotStore;
use Merql\Snapshot\Snapshotter;
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;

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

$connection = Connection::sqlite('/path/to/db.sqlite');
$snapshotter = new Snapshotter($connection, identityRules: $rules);
SnapshotStore::save($snapshotter->capture('base', ['wp_options']));
SnapshotStore::save($snapshotter->capture('ours', ['wp_options']));
SnapshotStore::save($snapshotter->capture('theirs', ['wp_options']));

$plan = Merql::plan('plan-1', 'base', 'ours', 'theirs');

$selection = ChangeGroupSelection::fromIds([
    $plan->changeGroups[0]->id,
]);

$selected = (new SelectedMergeResultFactory())
    ->fromChangeGroupSelection($plan, $selection, SnapshotStore::load('base'));

$rollback = (new RollbackPlanBuilder())->build(
    'rollback-1',
    $plan,
    $selection->toOperationSelection($plan),
    liveBeforeRows: [
        $plan->operations[0]->id => $plan->operations[0]->theirsRow,
    ],
);

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

For prefixed or multi-environment tables, capture physical table names under a canonical name before merging:

```
use Merql\Snapshot\SnapshotStore;
use Merql\Snapshot\Snapshotter;
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;

$connection = Connection::sqlite('/path/to/db.sqlite');
$snapshotter = new Snapshotter($connection, identityRules: new IdentityRuleSet([
    'wp_posts' => IdentityRule::primary(['ID']),
]));

SnapshotStore::save($snapshotter->captureAliased('sandbox', [
    'wp_onumia_abcd_posts' => 'wp_posts',
]));
```

PHP API
-------

[](#php-api)

```
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ConflictPolicy;
use Merql\Merge\ConflictResolver;
use Merql\Merge\ThreeWayMerge;
use Merql\Apply\DryRun;

// Three-way merge with cell-level merge for TEXT and JSON columns
$merge = new ThreeWayMerge(CellMergeConfig::auto());
$result = $merge->merge($base, $ours, $theirs);

// Two-way merge (apply changes onto base, never conflicts)
$result = $merge->patch($base, $changes);

// Resolve conflicts programmatically
$resolved = ConflictResolver::resolve($result, ConflictPolicy::TheirsWins);

// Preview SQL without executing
$sql = DryRun::generate($result);
foreach ($sql as $statement) {
    echo $statement . ";\n";
}
```

How It Works
------------

[](#how-it-works)

```
         Base (common ancestor)
        /                       \
   Ours (our changes)      Theirs (their changes)
        \                       /
         ─────── MERGE ────────
                   │
            Merged result

```

Merql merges at four levels of granularity:

LevelUnitConflict whenTableWhole tableOne side adds, other removesRowRow by PKBoth insert same PKColumnColumn valueBoth change same column to different valuesCellContent within valueBoth change same line (text) or key (JSON)Column-level merge is the key advantage over naive row-level comparison. When both sides change the same row but different columns, merql resolves it cleanly:

```
Base:    { title: "Hello",     content: "Body",    status: "draft"   }
Ours:    { title: "Hello",     content: "Body v2", status: "draft"   }
Theirs:  { title: "New Title", content: "Body",    status: "publish" }
Result:  { title: "New Title", content: "Body v2", status: "publish" }

```

CLI
---

[](#cli)

```
# Set connection (SQLite)
export MERQL_DB_DSN="sqlite:/path/to/db.sqlite"

# Or MySQL
export MERQL_DB_NAME=mydb MERQL_DB_USER=root

# Snapshot, diff, merge
vendor/bin/merql snapshot base
vendor/bin/merql diff base current
vendor/bin/merql merge base ours theirs
vendor/bin/merql merge base ours theirs --dry-run
```

Documentation
-------------

[](#documentation)

The repo includes a dedicated docs app under [`docs/`](docs) that mirrors the same release/docs structure used in sibling projects.

```
cd docs
npm install
npm run dev
```

Topics covered:

- Getting started, CLI reference, and PHP API
- Three-way merge, merge plans, column-level merge, and cell-level merge
- Conflict detection and resolution
- SQL generation, guarded apply, rollback, dry run, and database drivers
- Row identity, identity rules, filters, schema validation, and testing strategy

Testing
-------

[](#testing)

Merql is validated with unit tests, integration tests against real SQLite, and an oracle-style regression corpus.

```
# PHPUnit unit + integration tests
composer test

# Oracle regression corpus
composer test:oracle

# Static analysis
composer analyse

# Coding standards
composer cs

# Full release-grade verification (analyse + cs + test + test:oracle)
composer verify
```

Current local verification baseline:

- `231` PHPUnit tests
- `545` assertions
- oracle regression summary `32/32`

Features
--------

[](#features)

CategoryFeaturesMergethree-way merge, two-way patch, merge plans, selected apply, column-level resolution, cell-level mergeCell MergeTEXT line-by-line (Myers diff via pitmaster), JSON key-by-key, custom mergersConflictsupdate/update, update/delete, delete/update, insert/insert, manual + auto resolveSQLparameterized INSERT/UPDATE/DELETE, FK-aware ordering, guarded apply, dry-run preview, transactionsDatabasesMySQL and SQLite dialects, PDO and mysqli connection adaptersIdentityprimary key, natural key, content hash, composite key support, identity rule registrySnapshotrow fingerprinting, JSON persistence, schema capture, aliased table capture, table/column/row filtersRollbackrollback plan generation, drift checks, inverse operation applyValidationschema mismatch detection, snapshot name validation, path traversal protectionArchitecture
------------

[](#architecture)

```
src/
├── Merql.php                  # Static facade (init, snapshot, diff, merge, apply)
├── Snapshot/                  # Capture database state (fingerprints + data)
├── Diff/                      # Compare two snapshots (insert/update/delete changesets)
├── Merge/                     # Three-way merge with column-level conflict resolution
├── Plan/                      # Merge plans, selections, selected merge results
├── Rollback/                  # Rollback plans, drift checks, inverse apply
├── CellMerge/                 # Cell-level merge (text, JSON, custom)
├── Apply/                     # SQL generation, dry run, FK ordering, applier
├── Database/                  # Connection interface plus PDO and mysqli adapters
├── Driver/                    # Database dialect interface (MySQL, SQLite)
├── Schema/                    # Table schema, validation, primary key resolution
├── Identity/                  # Row identity strategies (PK, natural key, hash)
├── Filter/                    # Table, column, and row filters
├── Connection.php             # Connection adapter builders
└── Exceptions/                # Typed exceptions

```

Requirements
------------

[](#requirements)

- PHP 8.2+
- `ext-pdo` and `ext-pdo_sqlite` for the PDO adapter and SQLite builder
- `ext-mysqli` when using the mysqli adapter

License
-------

[](#license)

MIT

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance91

Actively maintained with recent releases

Popularity18

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~19 days

Total

4

Last Release

43d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/dc81a975082b6843421de211768cfb67d8237bed4c972c9a1eaad46aac14b372?d=identicon)[dnnsjsk](/maintainers/dnnsjsk)

---

Top Contributors

[![dnnsjsk](https://avatars.githubusercontent.com/u/73965001?v=4)](https://github.com/dnnsjsk "dnnsjsk (25 commits)")

---

Tags

phpdiffsnapshotdatabasemysqlsqlitepdomergemysqlithree-way-mergeconflict-resolution

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/merql-merql/health.svg)

```
[![Health](https://phpackages.com/badges/merql-merql/health.svg)](https://phpackages.com/packages/merql-merql)
```

###  Alternatives

[ezsql/ezsql

Advance database access library. Make interacting with a database ridiculously easy. An universal interchangeable CRUD system.

86749.2k](/packages/ezsql-ezsql)[jv2222/ezsql

Advance database access library. Make interacting with a database ridiculously easy. An universal interchangeable CRUD system.

86711.6k2](/packages/jv2222-ezsql)[popphp/pop-db

Pop Db Component for Pop PHP Framework

1816.5k12](/packages/popphp-pop-db)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
