Cell-Level Merge

View as Markdown

Column-level merge detects when both sides changed the same column. Cell-level merge goes deeper, merging the content inside that column. Two people editing different lines of a TEXT field, or different keys of a JSON object, can merge cleanly.

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

$config = CellMergeConfig::auto();
$merge = new ThreeWayMerge($config);
$result = $merge->merge($base, $ours, $theirs);

CellMergeConfig

CellMergeConfig maps columns or column types to cell merger implementations. The auto() factory provides sensible defaults:

PHP
use Merql\CellMerge\CellMergeConfig;

// Auto: TEXT/LONGTEXT columns use TextCellMerger, JSON columns use JsonCellMerger.
$config = CellMergeConfig::auto();

// Custom: assign mergers to specific columns.
$config = (new CellMergeConfig())
    ->forColumn('posts.content', new TextCellMerger())
    ->forColumn('settings', new JsonCellMerger())
    ->forType('json', new JsonCellMerger());

Merger resolution priority:

  1. Qualified column name (table.column).
  2. Unqualified column name (column).
  3. Column type pattern match (case-insensitive str_contains).
  4. Default: OpaqueCellMerger (always reports a conflict when values differ).

TextCellMerger

Merges multi-line text using Myers diff, the same algorithm git uses for file content. Each line is treated as a unit, and changes to different lines merge cleanly.

PHP
use Merql\CellMerge\TextCellMerger;

$merger = new TextCellMerger();

Example: clean text merge

Code
Base:    "line one\nline two\nline three"
Ours:    "LINE ONE\nline two\nline three"    // changed line 1
Theirs:  "line one\nline two\nLINE THREE"    // changed line 3

Different lines were edited. The merge produces "LINE ONE\nline two\nLINE THREE" with no conflict.

Example: text conflict

Code
Base:    "line one\nline two"
Ours:    "changed by us\nline two"     // changed line 1
Theirs:  "changed by them\nline two"   // also changed line 1

Both sides changed the same line to different values. The merger returns a conflict result. The merged content includes conflict markers similar to git.

How it works

TextCellMerger delegates to pitmaster's ThreeWayMerge::merge(), which computes a Myers diff between base and each side, then combines the two edit scripts. When edits overlap on the same line region, it reports a conflict.

JsonCellMerger

Merges JSON objects by comparing top-level keys independently. Each key is merged using the standard three-way rules.

PHP
use Merql\CellMerge\JsonCellMerger;

$merger = new JsonCellMerger();

Example: clean JSON merge

JSON
// Base
{"name": "Alice", "role": "editor", "active": true}

// Ours: changed "role"
{"name": "Alice", "role": "admin", "active": true}

// Theirs: changed "name"
{"name": "Bob", "role": "editor", "active": true}

Different keys were changed. The merged result is:

JSON
{"name": "Bob", "role": "admin", "active": true}

Example: JSON conflict

JSON
// Base
{"color": "red"}

// Ours
{"color": "blue"}

// Theirs
{"color": "green"}

Both sides changed the color key to different values. This is a conflict. The merger defaults to ours and reports one conflict.

Key-level rules

Base Ours Theirs Result
Key unchanged Key unchanged Accept base
Key unchanged Key changed Accept theirs
Key changed Key unchanged Accept ours
Key changed (same) Key changed (same) Accept (agree)
Key changed (different) Key changed (different) Conflict (default: ours)
Key exists Key exists Key removed Accept removal
Key exists Key removed Key exists Accept removal
Key missing Key added Key missing Accept addition
Key missing Key missing Key added Accept addition

Nested objects are compared as opaque JSON strings, not recursively. If a nested object differs, the entire value of that key is compared, not its sub-keys.

Requirements

All three values must be valid JSON objects or arrays. If any value is not valid JSON, or is a JSON scalar (string, number, boolean), the merger falls back to a conflict.

OpaqueCellMerger

The default merger when no cell-level strategy is configured. It treats the column value as an opaque blob and always reports a conflict when both sides differ.

PHP
use Merql\CellMerge\OpaqueCellMerger;

$merger = new OpaqueCellMerger();
$result = $merger->merge($base, $ours, $theirs);
// $result->clean is always false when $ours !== $theirs

CellMergeResult

All cell mergers return a CellMergeResult:

PHP
$result = $merger->merge($base, $ours, $theirs);

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

Factory methods:

PHP
CellMergeResult::resolved($value);           // clean merge
CellMergeResult::conflict($oursValue, $n);   // n conflicts, defaults to ours

Custom cell mergers

Implement the CellMerger interface to add your own merge strategy:

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

class YamlCellMerger implements CellMerger
{
    public function merge(mixed $base, mixed $ours, mixed $theirs): CellMergeResult
    {
        // Parse YAML, merge keys, return result.
        $merged = $this->mergeYaml($base, $ours, $theirs);

        if ($merged['clean']) {
            return CellMergeResult::resolved($merged['value']);
        }

        return CellMergeResult::conflict($ours);
    }
}

$config = (new CellMergeConfig())
    ->forColumn('config_data', new YamlCellMerger());