Merql
Advanced

Testing Strategy

merql uses an oracle-driven testing model. Each test scenario defines a known set of inputs (three database states), a deterministic expected result (the oracle), and compares the actual merge output against that expectation. 32 scenarios cover clean merges, conflicts, identity resolution, data types, scale, and edge cases.

# Run the full verification suite.
./bin/verify-all

Oracle-driven testing

There is no existing reference tool that performs three-way database merge, so the oracle is computed from known inputs rather than generated by an external tool. Each scenario has a deterministic expected result that can be verified by logic:

  1. Setup -- create three database states (base, ours, theirs) with known content.
  2. Oracle -- compute the expected merge result from the known inputs.
  3. Actual -- run merql's merge on the three states.
  4. Compare -- the actual result must match the expected result exactly.
Known inputs (base, ours, theirs)
         |                    |
    Deterministic          merql merge
    computation
         |                    |
    Expected result       Actual result
         |                    |
         -----> Compare <-----
                  |
            Pass or Fail

Running tests

Full verification

The verify-all script is the final gate. It runs static analysis, coding standards, PHPUnit tests, and the oracle regression suite:

./bin/verify-all

This must pass before any work is considered complete.

Oracle regression suite

Run all 32 scenarios:

./bin/test-regression

Run scenarios in parallel:

./bin/test-regression --jobs 4

Run a single category:

./bin/test-regression --category clean
./bin/test-regression --category conflict
./bin/test-regression --category identity

Run with minimal output (pass/fail only):

./bin/test-regression --fast

Single scenario

Test one scenario through the full pipeline:

./bin/test-scenario column-level-clean

Individual oracle steps

./bin/oracle column-level-clean    # Compute expected result
./bin/actual column-level-clean    # Run merql, capture output
./bin/compare column-level-clean   # Diff oracle vs actual

Unit tests

composer test:unit     # Isolated component tests
composer test          # Full PHPUnit + oracle matrix

Code quality

composer cs            # PSR-12 coding standards check
composer cs:fix        # Auto-fix coding standards
composer analyse       # PHPStan level 8 static analysis

Scenario categories

clean (8 scenarios)

Clean merges with no conflicts. Both sides may change data, but never the same column of the same row.

ScenarioTests
insert-only-theirsTheirs inserted rows, ours unchanged.
update-only-theirsTheirs updated rows, ours unchanged.
delete-only-theirsTheirs deleted rows, ours unchanged.
insert-only-oursOurs inserted rows, theirs unchanged.
mixed-no-overlapBoth changed different tables or rows.
both-same-changeBoth made identical changes (no conflict).
column-level-cleanBoth changed same row, different columns.
multi-tableChanges across multiple tables.

conflict (6 scenarios)

Scenarios that must produce conflicts.

ScenarioTests
both-update-same-columnBoth changed same column to different values.
update-vs-deleteOne updated, other deleted same row.
delete-vs-updateReverse direction of update-vs-delete.
both-insert-same-pkBoth inserted row with same primary key.
multiple-conflictsSeveral conflicts in one merge.
partial-conflictSome columns conflict, others merge clean.

identity (4 scenarios)

Row identity edge cases.

ScenarioTests
auto-incrementNew rows have different IDs across branches.
natural-keyMatch by unique columns instead of PK.
composite-keyMulti-column primary key.
no-keyTable without primary key (content hash fallback).

types (6 scenarios)

Data type handling across all column types.

ScenarioTests
text-columnsVARCHAR, TEXT, LONGTEXT values.
numeric-columnsINT, DECIMAL, FLOAT values.
date-columnsDATE, DATETIME, TIMESTAMP values.
json-columnsJSON column merge.
blob-columnsBinary data.
null-handlingNULL to value, value to NULL transitions.

scale (4 scenarios)

Performance under load.

ScenarioTests
1k-rows1,000 rows.
10k-rows10,000 rows.
100k-rows100,000 rows.
wide-tableTable with 50+ columns.

edge (4 scenarios)

Edge cases and boundary conditions.

ScenarioTests
empty-changesetNo changes on one or both sides.
schema-mismatchColumn added or removed between snapshots.
encodingUTF-8, emoji, special characters.
large-textVery large TEXT/LONGTEXT values.

Scenario structure

Each scenario is a directory under scenarios/<category>/<name>/ with a scenario.json configuration:

{
    "name": "column-level-clean",
    "category": "clean",
    "description": "Both sides modify same row but different columns",
    "tables": ["test_posts"],
    "expectations": {
        "conflicts": 0,
        "operations": 3,
        "changeset_match": "exact",
        "merge_match": "exact",
        "sql_match": "semantic"
    }
}

For conflict scenarios, the expected conflicts are specified in detail:

{
    "expectations": {
        "conflicts": 1,
        "conflict_details": [
            {
                "table": "test_posts",
                "primary_key": {"id": 42},
                "column": "title",
                "ours_value": "Welcome",
                "theirs_value": "Greetings"
            }
        ]
    }
}

The ScenarioRunner pipeline

ScenarioRunner::run() orchestrates a single scenario:

  1. OracleCapture::compute() builds three snapshots from the scenario's JSON data and runs the merge.
  2. ScenarioComparator::compare() checks the merge result against the expected values in scenario.json.
  3. Returns pass/fail with a list of failure messages.
use Merql\Tests\Oracle\ScenarioRunner;

$result = ScenarioRunner::run($scenario);
// ['name' => 'column-level-clean', 'pass' => true, 'failures' => []]

ScenarioRunner::runAll() runs every scenario in sequence:

$results = ScenarioRunner::runAll($scenarios);

Adding a new scenario

  1. Create a directory: scenarios/<category>/<name>/.
  2. Create scenario.json with the scenario metadata and expectations.
  3. Define the three database states in the JSON configuration: base data, ours mutations, theirs mutations.
  4. Run the scenario: ./bin/test-scenario <name>.
  5. Verify the full suite: ./bin/verify-all.

Code quality standards

merql enforces:

  • PHPStan level 8 -- strict static analysis with no baseline exceptions.
  • PSR-12 -- coding standards checked by phpcs.
  • Oracle regression -- all 32 scenarios must pass.
  • Unit tests -- isolated tests for each component.

The verify-all script runs all four checks in sequence. No partial sign-off is accepted.

On this page