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-allOracle-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 FailRunning 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-allThis must pass before any work is considered complete.
Oracle regression suite
Run all 32 scenarios:
./bin/test-regressionRun scenarios in parallel:
./bin/test-regression --jobs 4Run a single category:
./bin/test-regression --category clean
./bin/test-regression --category conflict
./bin/test-regression --category identityRun with minimal output (pass/fail only):
./bin/test-regression --fastSingle scenario
Test one scenario through the full pipeline:
./bin/test-scenario column-level-cleanIndividual 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 actualUnit tests
composer test:unit # Isolated component tests
composer test # Full PHPUnit + oracle matrixCode quality
composer cs # PSR-12 coding standards check
composer cs:fix # Auto-fix coding standards
composer analyse # PHPStan level 8 static analysisScenario 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 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.