
# Fusión de tres vías

Una fusión de tres vías compara dos conjuntos de cambios con un ancestro común y los combina en un único resultado. merql aplica este algoritmo a filas de bases de datos en lugar de a líneas de archivos.

```php
use Merql\Merge\ThreeWayMerge;

$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);

echo $result->operationCount(); // resolved operations
echo $result->conflictCount();  // unresolved conflicts
```

## Los tres estados

Cada fusión requiere exactamente tres instantáneas:

- **Base** -- el estado de la base de datos en el momento en que se separaron ambas ramas.
- **Nuestra** -- una rama después de sus cambios, por ejemplo, una copia de pruebas que se editó.
- **Ajena** -- la otra rama después de sus cambios, por ejemplo, la base de datos activa que siguió ejecutándose.

Nuestra y ajena deben capturarse cada una de la base de datos que contiene esa rama. Las reglas de fusión tratan ambos lados por igual; los nombres determinan el `source` de cada operación, cómo se nombran los tipos de conflicto (`update_delete` significa que nuestra rama actualizó y la ajena eliminó) y qué lado favorecen `ConflictPolicy::OursWins` y `TheirsWins`.

```
         Base (snapshot at fork time)
        /                              \
   Ours (our changes)                   Theirs (their changes)
        \                              /
         --------- MERGE ----------
                     |
              Merged result
              + conflicts (if any)
```

Sin una base común, no se puede distinguir entre «añadido» y «sin cambios». La comparación de dos vías muestra que el lado A tiene un valor y el lado B no, pero no permite determinar si A lo añadió o B lo eliminó. La base responde a esa pregunta.

## Cómo se calculan los conjuntos de cambios

`Differ` compara cada instantánea con la base para producir dos conjuntos de cambios. Cada conjunto contiene tres tipos de operaciones:

| Base | Actual | Operación |
|---|---|---|
| La fila existe | La fila existe, con la misma huella | Sin cambios |
| La fila existe | La fila existe, con una huella distinta | **UPDATE** (diferencias por columna) |
| La fila existe | Falta la fila | **DELETE** |
| Falta la fila | La fila existe | **INSERT** |

Las huellas (SHA-256 hashes del contenido normalizado de la fila) ofrecen una vía rápida. Si dos huellas coinciden, la fila no ha cambiado y no hace falta comparar las columnas.

```php
use Merql\Diff\Differ;

$differ = new Differ();
$oursChangeset = $differ->diff($base, $ours);
$theirsChangeset = $differ->diff($base, $theirs);
```

## Reglas de fusión

A continuación, los dos conjuntos de cambios se fusionan operación por operación. Esta tabla define el resultado de cada combinación:

| Nuestra | Ajena | Resultado |
|---|---|---|
| Sin cambios | Sin cambios | Mantener la base |
| Sin cambios | Actualizada | Aceptar la ajena |
| Actualizada | Sin cambios | Aceptar la nuestra |
| Actualizada (mismo valor) | Actualizada (mismo valor) | Aceptar (ambas coinciden) |
| Actualizada (valor distinto) | Actualizada (valor distinto) | **Conflicto** |
| Sin cambios | Eliminada | Aceptar la eliminación |
| Eliminada | Sin cambios | Aceptar la eliminación |
| Eliminada | Eliminada | Aceptar la eliminación (ambas coinciden) |
| Actualizada | Eliminada | **Conflicto** |
| Eliminada | Actualizada | **Conflicto** |
| Insertada (solo nuestra) | -- | Aceptar la inserción |
| -- | Insertada (solo la ajena) | Aceptar la inserción |
| Insertada (misma PK) | Insertada (misma PK) | **Conflicto** |

Cuando ambos lados actualizan la misma fila, merql no declara inmediatamente un conflicto. Desciende al nivel de columna y compara cada columna por separado. Consulte [Fusión a nivel de columna](https://merql.inline0.dev/es/docs/merge/column-level) para obtener más detalles.

## Uso de la fachada

La fachada `Merql` opera con instantáneas con nombre que se han guardado en disco. Llame a `Merql::init()` con la base de datos que contiene cada estado antes de crear su instantánea:

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

$live    = Connection::sqlite('/data/live.sqlite');
$sandbox = Connection::sqlite('/data/sandbox.sqlite');

Merql::init($live);
Merql::snapshot('base');      // before the sandbox is copied from live

// ... copy live.sqlite to sandbox.sqlite, then both databases change ...

Merql::init($sandbox);
Merql::snapshot('ours');
Merql::init($live);
Merql::snapshot('theirs');

$result = Merql::merge('base', 'ours', 'theirs');
```

No capture las tres instantáneas de una sola base de datos en secuencia. La instantánea ajena incluiría también los cambios de la nuestra, por lo que cada fila insertada por nuestra rama aparecería como insertada en ambos lados y se notificaría como un conflicto `insert_insert`.

`Merql::merge()`, `Merql::patch()` y `Merql::plan()` crean un `ThreeWayMerge` sin un `CellMergeConfig`, por lo que solo fusionan a nivel de columna. Para fusionar a nivel de celda, cargue las instantáneas y use directamente la clase, como se muestra a continuación.

## Uso directo de las clases

Para tener más control, cree las instancias de los componentes usted mismo. `Snapshotter::capture()` no guarda nada, así que conserve las instantáneas en memoria o guárdelas con `SnapshotStore::save()`:

```php
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Snapshot\Snapshotter;
use Merql\Snapshot\SnapshotStore;

$base   = (new Snapshotter($live))->capture('base');
// ... copy live to the sandbox, then both databases change ...
$ours   = (new Snapshotter($sandbox))->capture('ours');
$theirs = (new Snapshotter($live))->capture('theirs');

$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);

// Snapshots saved earlier, merged with cell-level merge for TEXT and JSON columns.
$merge = new ThreeWayMerge(CellMergeConfig::auto());
$result = $merge->merge(
    SnapshotStore::load('base'),
    SnapshotStore::load('ours'),
    SnapshotStore::load('theirs'),
);
```

## El atajo de patch

Cuando solo un lado tiene cambios (sin modificaciones simultáneas), use `patch()`. Es una fusión de dos vías en la que la nuestra es igual a la base, por lo que no puede haber conflictos:

```php
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.
```

A través de la fachada:

```php
$result = Merql::patch('base', 'changes');
```

`patch()` equivale a `merge($base, $base, $changes)`. Como la nuestra nunca se desvió de la base, todos los cambios de la ajena se aceptan sin conflictos.

## Detección de discrepancias de esquema

Si se añadieron o eliminaron columnas de una tabla entre las instantáneas, la fusión continúa, pero registra las discrepancias. No se comparan los tipos de columna modificados, y una columna renombrada aparece como una columna eliminada y otra añadida; consulte [Validación del esquema](https://merql.inline0.dev/es/docs/advanced/schema).

```php
$result = $merge->merge($base, $ours, $theirs);

if ($result->hasSchemaMismatches()) {
    foreach ($result->schemaMismatches() as $mismatch) {
        echo $mismatch->getMessage() . "\n";
    }
}
```

merql no migra esquemas automáticamente. Los cambios de esquema son responsabilidad de quien llama. La fusión los informa para que pueda decidir cómo proceder.

## El objeto MergeResult

El objeto del resultado contiene todo lo que necesita:

```php
$result->isClean();            // true if no conflicts
$result->operations();         // list of MergeOperation objects
$result->conflicts();          // list of Conflict objects
$result->operationCount();     // number of resolved operations
$result->conflictCount();      // number of unresolved conflicts
$result->hasSchemaMismatches(); // true if schemas differ
$result->baseSnapshot();       // the base snapshot used for the merge
```

Cada `MergeOperation` tiene un tipo (`insert`, `update` o `delete`), un nombre de tabla, una clave de fila, los valores de las columnas y un origen (`ours`, `theirs` o `merged`). `merged` marca una fila que ambos lados actualizaron (combinada columna por columna) o que ambos lados eliminaron.

## Dónde se aplica un resultado de fusión

Un resultado de fusión describe cómo convertir la base en el estado fusionado. Sus operaciones incluyen los cambios de ambos lados: una inserción de nuestra rama y una inserción de la ajena aparecen como inserciones.

- `Merql::apply()` (o `Applier`) ejecuta todas las operaciones en la base de datos conectada. Se aplica correctamente a una base de datos que aún contiene la base, como una copia recién creada de ese estado. Si se aplica a una base de datos que ya contiene los cambios de una rama, falla al insertar una fila existente y se revierte la transacción.
- Para incorporar los cambios de nuestra rama en una base de datos que ya contiene los de la ajena, por ejemplo, para llevar las ediciones de pruebas a producción, aplique solo las operaciones que faltan en esa base de datos: las que tienen el origen `ours` y las actualizaciones `merged`. Use un [plan de fusión](https://merql.inline0.dev/es/docs/merge/plans) para seleccionarlas y [Aplicación protegida](https://merql.inline0.dev/es/docs/apply/guarded) para escribirlas con condiciones previas sobre las filas activas.
