
# Planes de combinación

Un `MergeResult` es el resultado ejecutable de una combinación. Un `MergePlan` es la forma inspeccionable de ese resultado. Úselo cuando una aplicación necesite mostrar, preparar, serializar o aplicar selectivamente cambios en la base de datos antes de escribir en la base de datos de destino.

```php
use Merql\Merql;

$plan = Merql::plan(
    id: 'plan-2026-06-06',
    base: 'base',
    ours: 'ours',
    theirs: 'theirs',
    metadata: ['sandbox_id' => 'ow_123'],
);
```

`Merql::plan()` carga las tres instantáneas nombradas y las combina a nivel de columna, como `Merql::merge()`. Para planificar una combinación que use combinación a nivel de celda en columnas TEXT y JSON, cree el resultado usted mismo y páselo a `MergePlanBuilder`:

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

$base = SnapshotStore::load('base');
$ours = SnapshotStore::load('ours');
$theirs = SnapshotStore::load('theirs');

$result = (new ThreeWayMerge(CellMergeConfig::auto()))->merge($base, $ours, $theirs);
$plan = (new MergePlanBuilder())->build('plan-2026-06-06', $base, $ours, $theirs, $result);
```

El plan contiene:

- los nombres de las instantáneas usadas para crearlo,
- un resumen de operaciones, conflictos, discrepancias de esquema, tablas y grupos,
- identificadores de operación estables,
- identificadores de grupo de cambios estables,
- columnas de identidad de filas y valores de identidad,
- valores de fila de base, nuestro lado, el suyo y el resultado para cada operación,
- metadatos opcionales de la aplicación anfitriona,
- un hash del plan,
- una versión del esquema.

## Identificadores estables

Los identificadores de operación son deterministas para el mismo contenido de operación. Se derivan del nombre de la tabla, la clave de fila, el tipo de operación, el origen de la operación, las columnas modificadas y los valores resultantes.

Los identificadores de grupo de cambios son deterministas para el mismo contenido del grupo. El generador predeterminado crea un grupo de operación por cada operación. Las aplicaciones anfitrionas pueden crear grupos de dominio más amplios a partir de la lista de operaciones y serializarlos con la misma estructura DTO.

```php
foreach ($plan->changeGroups as $group) {
    echo $group->id . ': ' . implode(', ', $group->operationIds);
}
```

## Estructura de la operación

Cada `MergePlanOperation` conserva contexto suficiente para su revisión y reversión.

```php
$operation = $plan->operations[0];

$operation->id;              // stable operation ID
$operation->type;            // insert, update, delete
$operation->table;           // canonical table name
$operation->rowKey;          // encoded identity key
$operation->identityColumns; // columns used for identity
$operation->identityValues;  // decoded identity values
$operation->source;          // ours, theirs, or merged
$operation->baseRow;         // row in base snapshot, or null
$operation->oursRow;         // row in ours snapshot, or null
$operation->theirsRow;       // row in theirs snapshot, or null
$operation->resultValues;    // values the merge would write
$operation->changedColumns;  // changed result columns
```

## Selecciones

Use `ChangeGroupSelection` cuando UI prepare grupos. Conviértalo en un `OperationSelection` al aplicar cambios o generar artefactos de reversión.

```php
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

$selection = ChangeGroupSelection::fromIds([
    $plan->changeGroups[0]->id,
]);

$operationSelection = $selection->toOperationSelection($plan);

$selectedResult = (new SelectedMergeResultFactory())->fromOperationSelection(
    $plan,
    $operationSelection,
    SnapshotStore::load($plan->baseSnapshot),
);
```

El resultado seleccionado es un `MergeResult` limpio que solo contiene las operaciones seleccionadas. Se puede pasar a `Merql::apply()` o `Merql::applyGuarded()`.

## Seleccionar lo que le falta al destino

Un plan contiene las operaciones de ambas partes, porque el resultado de la combinación transforma la base en el estado combinado. Cuando el destino ya contiene los cambios de una parte, seleccione solo las operaciones que aún no tiene. Para un destino que contiene los cambios de su lado, como una base de datos live que recibe las ediciones de una copia sandbox, seleccione todas las operaciones con origen `ours`, más las actualizaciones `merged`, que combinan los cambios de columnas de ambas partes:

```php
use Merql\Plan\OperationSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

// Rows with an unresolved conflict stay out of the selection.
$conflicted = [];
foreach ($plan->conflicts as $conflict) {
    $conflicted[$conflict->table . "\0" . $conflict->rowKey] = true;
}

$ids = [];
foreach ($plan->operations as $operation) {
    $missing = $operation->source === 'ours'
        || ($operation->source === 'merged' && $operation->type === 'update');

    if ($missing && !isset($conflicted[$operation->table . "\0" . $operation->rowKey])) {
        $ids[] = $operation->id;
    }
}

$selected = (new SelectedMergeResultFactory())->fromOperationSelection(
    $plan,
    OperationSelection::fromIds($ids),
    SnapshotStore::load($plan->baseSnapshot),
);
```

Las operaciones con origen `theirs` y las eliminaciones `merged` (filas eliminadas por ambas partes) ya están presentes en ese destino.

Un plan creado a partir de una combinación con conflictos sigue enumerando una operación por cada fila en conflicto que ambas partes actualizaron. Sus valores de resultado conservan el valor de nuestro lado en la columna en conflicto, y `SelectedMergeResultFactory` devuelve un resultado limpio para cualquier selección. En la actualidad, `conflictState` de la operación y del grupo de cambios indican `clean` incluso en ese caso ([#9](https://github.com/inline0/merql/issues/9)), así que compruebe `$plan->conflicts` usted mismo como se indicó antes, o resuelva los conflictos con `ConflictResolver` y cree el plan a partir del resultado resuelto con `MergePlanBuilder`. Consulte [Aplicación protegida](https://merql.inline0.dev/es/docs/apply/guarded) para escribir la selección con condiciones previas sobre las filas activas.

## Serialización

Los planes y las selecciones de fusión tienen una serialización JSON estable. Guarde el plan serializado si un paso de aplicación posterior debe usar exactamente el conjunto de operaciones revisado.

```php
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\MergePlanSerializer;

$json = MergePlanSerializer::toJson($plan);
$restored = MergePlanSerializer::fromJson($json);

$selectionJson = json_encode($selection->toArray(), JSON_THROW_ON_ERROR);
$selection = ChangeGroupSelection::fromArray(
    json_decode($selectionJson, true, flags: JSON_THROW_ON_ERROR),
);
```

## Instantáneas con alias

Los planes de fusión usan nombres de tabla canónicos. Si los nombres físicos de las tablas difieren entre entornos, capture las tablas con el mismo nombre canónico antes de planificar.

```php
use Merql\Snapshot\SnapshotStore;
use Merql\Snapshot\Snapshotter;

$snapshotter = new Snapshotter($connection);

SnapshotStore::save($snapshotter->captureAliased('live', [
    'wp_posts' => 'wp_posts',
]));

SnapshotStore::save($snapshotter->captureAliased('sandbox', [
    'wp_onumia_abc_posts' => 'wp_posts',
]));
```

El esquema almacenado en la instantánea usa el nombre canónico de la tabla y conserva las columnas, claves, columnas de identidad, claves de fila y filtros de la tabla física.
