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:
- Setup -- create three database states (base, ours, theirs) with known content.
- Oracle -- compute the expected merge result from the known inputs.
- Actual -- run merql's merge on the three states.
- 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 10 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.
| Scenario | Tests |
|---|---|
insert-only-theirs |
Theirs inserted rows, ours unchanged. |
update-only-theirs |
Theirs updated rows, ours unchanged. |
delete-only-theirs |
Theirs deleted rows, ours unchanged. |
insert-only-ours |
Ours inserted rows, theirs unchanged. |
mixed-no-overlap |
Both changed different tables or rows. |
both-same-change |
Both made identical changes (no conflict). |
column-level-clean |
Both changed same row, different columns. |
multi-table |
Changes across multiple tables. |
conflict (6 scenarios)
Scenarios that must produce conflicts.
| Scenario | Tests |
|---|---|
both-update-same-column |
Both changed same column to different values. |
update-vs-delete |
One updated, other deleted same row. |
delete-vs-update |
Reverse direction of update-vs-delete. |
both-insert-same-pk |
Both inserted row with same primary key. |
multiple-conflicts |
Several conflicts in one merge. |
partial-conflict |
Some columns conflict, others merge clean. |
identity (4 scenarios)
Row identity edge cases.
| Scenario | Tests |
|---|---|
auto-increment |
New rows have different IDs across branches. |
natural-key |
Match by unique columns instead of PK. |
composite-key |
Multi-column primary key. |
no-key |
Table without primary key (content hash fallback). |
types (6 scenarios)
Data type handling across all column types.
| Scenario | Tests |
|---|---|
text-columns |
VARCHAR, TEXT, LONGTEXT values. |
numeric-columns |
INT, DECIMAL, FLOAT values. |
date-columns |
DATE, DATETIME, TIMESTAMP values. |
json-columns |
JSON column merge. |
blob-columns |
Binary data. |
null-handling |
NULL to value, value to NULL transitions. |
scale (4 scenarios)
Performance under load.
| Scenario | Tests |
|---|---|
1k-rows |
1,000 rows. |
10k-rows |
10,000 rows. |
100k-rows |
100,000 rows. |
wide-table |
Table with 50+ columns. |
edge (4 scenarios)
Edge cases and boundary conditions.
| Scenario | Tests |
|---|---|
empty-changeset |
No changes on one or both sides. |
schema-mismatch |
Column added or removed between snapshots. |
encoding |
UTF-8, emoji, special characters. |
large-text |
Very 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:
OracleCapture::compute()builds three snapshots from the scenario's JSON data and runs the merge.ScenarioComparator::compare()checks the merge result against the expected values inscenario.json.- 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
- Create a directory:
scenarios/<category>/<name>/. - Create
scenario.jsonwith the scenario metadata and expectations. - Define the three database states in the JSON configuration: base data, ours mutations, theirs mutations.
- Run the scenario:
./bin/test-scenario <name>. - Verify the full suite:
./bin/verify-all.
Code quality standards
merql enforces:
- PHPStan level 10 -- 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.