
# Referencia de PHP API

Referencia completa de todas las clases y métodos públicos de merql.

## Merql (fachada)

`Merql\Merql` es el punto de entrada estático para las operaciones comunes. Envuelve las clases de nivel inferior y gestiona automáticamente la persistencia de las instantáneas.

```php
use Merql\Connection;
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;
use Merql\Merql;

$connection = Connection::sqlite(':memory:');
Merql::init($connection, identityRules: new IdentityRuleSet([
    'options' => IdentityRule::natural(['option_name']),
]));
```

### `init(DatabaseConnection $connection, ?Driver $driver = null, ?IdentityRuleSet $identityRules = null): void`

Inicializa la fachada con una conexión a la base de datos. Debe llamarse antes de
`snapshot()`, `apply()` o `applyGuarded()`; vuelve a llamarla para cambiar de base de datos,
por ejemplo, para crear instantáneas de ours y theirs desde dos copias distintas. Si no se proporciona
`$driver`, llama a `DriverFactory::create()`, que genera una excepción para motores distintos de
MySQL y SQLite, salvo que se registre un controlador. Pasa un controlador para anular la detección automática, o un conjunto de reglas de identidad para anular la identidad de filas por tabla durante la captura de instantáneas.

### `snapshot(string $name, array $tables = []): Snapshot`

Captura el estado actual de la base de datos y lo guarda en disco. Pasa una matriz de nombres de tablas para crear instantáneas de tablas concretas, o no pases nada para incluirlas todas.

```php
$snapshot = Merql::snapshot('baseline');
$snapshot = Merql::snapshot('baseline', ['posts', 'users']);
```

### `diff(string $base, string $current): Changeset`

Carga dos instantáneas con nombre y calcula las diferencias entre ellas. No requiere `init()` porque trabaja con instantáneas guardadas.

```php
$changeset = Merql::diff('before', 'after');
```

### `merge(string $base, string $ours, string $theirs): MergeResult`

Fusión de tres vías de tres instantáneas con nombre. Devuelve un resultado con operaciones sin conflictos y los conflictos que haya. Fusiona a nivel de columna: la fachada construye `ThreeWayMerge` sin `CellMergeConfig`, por lo que no se aplica la fusión a nivel de celda. Para ello, usa directamente [`ThreeWayMerge`](#threewaymerge).

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

### `patch(string $base, string $changes): MergeResult`

Fusión de dos vías: aplica los cambios sobre la base. Equivale a `merge($base, $base, $changes)`. Siempre genera un resultado sin conflictos porque no hay ediciones simultáneas. Solo funciona a nivel de columna, como `merge()`.

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

### `apply(MergeResult $result): ApplyResult`

Ejecuta un resultado de fusión sin conflictos en la base de datos pasada a `init()`. Requiere `init()`. Genera `ConflictException` si quedan conflictos sin resolver.

Se ejecutan todas las operaciones del resultado, así que el resultado completo de `merge()` solo se aplica sin problemas a una base de datos que aún conserva el estado base. Para una base de datos que ya contiene uno de los lados, aplica una selección de las operaciones que le faltan; consulta [Aplicación protegida](https://merql.inline0.dev/es/docs/apply/guarded).

```php
$applied = Merql::apply($result);
echo $applied->rowsAffected();
```

### `plan(string $id, string $base, string $ours, string $theirs, array $metadata = []): MergePlan`

Crea un plan de fusión listo para UI a partir de tres instantáneas con nombre. El plan incluye identificadores estables de operación, identificadores de grupos de cambios, contexto de las operaciones, conflictos, discrepancias del esquema, un resumen, metadatos y un hash del plan. La fusión es a nivel de columna, como en `merge()`; pasa un resultado fusionado a nivel de celda a `MergePlanBuilder::build()` para una fusión a nivel de celda.

```php
$plan = Merql::plan('plan-1', 'base', 'ours', 'theirs', [
    'source' => 'review-ui',
]);
```

### `applyGuarded(MergeResult $result, Snapshot $expectedLive): ApplyResult`

Aplica un resultado de fusión sin conflictos con condiciones previas optimistas para las filas activas. Las actualizaciones y eliminaciones requieren que las filas activas actuales coincidan con `$expectedLive`; las inserciones requieren que la identidad de destino aún no exista. `$expectedLive` es una instantánea de la base de datos de destino (theirs, cuando se promociona ours a la base de datos de la que se tomó theirs). Cualquier instrucción que no afecte ninguna fila revierte la transacción.

```php
use Merql\Snapshot\SnapshotStore;

$applied = Merql::applyGuarded($selectedResult, SnapshotStore::load('theirs'));
```

### `reset(): void`

Borra las instancias internas de conexión y Snapshotter. Se usa principalmente en pruebas para restablecer el estado entre ejecuciones.

---

## Conexión

`Merql\Connection` crea adaptadores de conexión con valores predeterminados razonables. Las conexiones PDO usan excepciones en caso de error, modo de captura asociativa, consultas preparadas no emuladas y valores capturados convertidos a cadenas. El adaptador mysqli normaliza los valores capturados al mismo formato de cadena o nulo.

### `mysql(string $host, string $database, string $username, string $password = '', int $port = 3306, string $charset = 'utf8mb4'): PdoDatabaseConnection`

```php
use Merql\Connection;

$connection = Connection::mysql('127.0.0.1', 'my_app', 'root', 'secret');
```

### `mysqli(string $host, string $database, string $username, string $password = '', int $port = 3306, string $charset = 'utf8mb4'): MysqliDatabaseConnection`

```php
$connection = Connection::mysqli('127.0.0.1', 'my_app', 'root', 'secret');
```

### `sqlite(string $path = ':memory:'): PdoDatabaseConnection`

Crea una conexión SQLite. Habilita automáticamente las claves externas mediante `PRAGMA foreign_keys = ON`.

```php
$connection = Connection::sqlite('/var/data/app.sqlite');
$connection = Connection::sqlite(':memory:');
```

### `fromDsn(string $dsn, string $username = '', string $password = ''): PdoDatabaseConnection`

Conéctate usando cualquier cadena DSN de PDO. merql incluye controladores solo para `mysql` y `sqlite`; otros DSN abren una conexión, pero `Merql::init()`, `Snapshotter` y los aplicadores generan `Unsupported database driver` salvo que se registre un controlador con `DriverFactory::register()`.

```php
$connection = Connection::fromDsn('mysql:host=localhost;dbname=app', 'root', 'pass');
```

### `fromPdo(PDO $pdo): PdoDatabaseConnection`

Envuelve una instancia PDO existente.

### `fromMysqli(mysqli $mysqli, string $charset = 'utf8mb4'): MysqliDatabaseConnection`

Envuelve una instancia mysqli existente.

### Contrato del adaptador

Todos los adaptadores implementan `Merql\Database\DatabaseConnection`:

```php
interface DatabaseConnection
{
    public function driverName(): string;
    public function execute(string $sql, array $params = []): int;
    public function query(string $sql, array $params = []): array;
    public function scalar(string $sql, array $params = []): ?string;
    public function beginTransaction(): void;
    public function commit(): void;
    public function rollBack(): void;
    public function lastInsertId(): string;
}
```

`execute()`, `query()` y `scalar()` aceptan parámetros posicionales `list<scalar|null>`
para los marcadores de posición `?`. `query()` devuelve
`list<array<string, scalar|null>>`; los valores se normalizan al mismo formato de cadena o nulo en los adaptadores PDO y mysqli.

---

## Snapshotter

`Merql\Snapshot\Snapshotter` captura el estado de la base de datos. A diferencia de la fachada, te da control total sobre los filtros y no guarda automáticamente las instantáneas.

### `__construct(DatabaseConnection $connection, ?Driver $driver = null, ?IdentityRuleSet $identityRules = null)`

El controlador se detecta automáticamente a partir de la identidad de la conexión si no se proporciona. Pasa un
`IdentityRuleSet` cuando la identidad de fila específica de una tabla debe anular los valores predeterminados del esquema.

### `capture(string $name, array $tables = [], array $filters = []): Snapshot`

Captura el estado actual. Pasa nombres de tablas para limitar el alcance. Pasa objetos de filtro para excluir tablas, columnas o filas.

```php
use Merql\Snapshot\Snapshotter;
use Merql\Filter\TableFilter;
use Merql\Filter\ColumnFilter;
use Merql\Filter\RowFilter;
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;

$snapshotter = new Snapshotter(
    $connection,
    identityRules: new IdentityRuleSet([
        'wp_options' => IdentityRule::natural(['option_name']),
    ]),
);

// All tables.
$snapshot = $snapshotter->capture('full');

// Specific tables with filters.
$snapshot = $snapshotter->capture('filtered', ['posts', 'users'], [
    TableFilter::exclude(['cache_*', 'sessions']),
    ColumnFilter::ignore(['updated_at', 'created_at']),
    RowFilter::create(fn(string $table, array $row) => $row['status'] !== 'trash'),
]);
```

### `captureAliased(string $name, array $physicalToCanonicalTableMap, array $filters = []): Snapshot`

Captura tablas físicas con nombres de tabla canónicos. Usa esta opción cuando una misma
 tabla lógica tenga nombres físicos distintos entre entornos.

```php
$snapshot = $snapshotter->captureAliased('sandbox', [
    'wp_onumia_abcd_posts' => 'wp_posts',
    'wp_onumia_abcd_postmeta' => 'wp_postmeta',
]);
```

La instantánea resultante guarda los nombres canónicos de las tablas y conserva el
esquema físico de la tabla, las columnas de identidad configuradas, las claves de fila y los filtros.

### `fromData(string $name, array $tableData): Snapshot` (estático)

Crea una instantánea a partir de datos sin procesar, sin conexión a la base de datos. Es útil para probar o importar datos de otra fuente.

```php
use Merql\Snapshot\Snapshotter;
use Merql\Snapshot\TableSnapshotData;
use Merql\Schema\TableSchema;

$schema = new TableSchema('posts', ['id' => 'int', 'title' => 'varchar'], ['id']);

$snapshot = Snapshotter::fromData('test', [
    'posts' => new TableSnapshotData($schema, [
        ['id' => '1', 'title' => 'Hello'],
        ['id' => '2', 'title' => 'World'],
    ], ['id']),
]);
```

---

## Snapshot

`Merql\Snapshot\Snapshot` es un objeto de valor de solo lectura que representa un estado completo de la base de datos.

```php
$snapshot->name;                    // string: snapshot identifier
$snapshot->tables;                  // array<string, TableSnapshot>
$snapshot->tableNames();            // list<string>
$snapshot->hasTable('posts');       // bool
$snapshot->getTable('posts');       // ?TableSnapshot
```

---

## TableSnapshot

`Merql\Snapshot\TableSnapshot` contiene las filas, las huellas digitales y el esquema de una tabla.

```php
$table = $snapshot->getTable('posts');

$table->schema;                    // TableSchema
$table->identityColumns;           // list<string>: columns used for row identity
$table->fingerprints;              // array<string, string>: row key to hash
$table->rows;                      // array<string, array<string, mixed>>

$table->rowKeys();                 // list<string>
$table->rowCount();                // int
$table->hasRow('42');              // bool
$table->getRow('42');              // ?array<string, mixed>
$table->getFingerprint('42');      // ?string
```

---

## TableSnapshotData

`Merql\Snapshot\TableSnapshotData` son los datos de entrada para `Snapshotter::fromData()`.

```php
use Merql\Snapshot\TableSnapshotData;
use Merql\Schema\TableSchema;

$data = new TableSnapshotData(
    schema: new TableSchema('posts', ['id' => 'int', 'title' => 'varchar'], ['id']),
    rows: [
        ['id' => '1', 'title' => 'First Post'],
    ],
    identityColumns: ['id'],
);
```

---

## IdentityRule

`Merql\Identity\IdentityRule` define las columnas utilizadas para crear claves de fila.

```php
use Merql\Identity\IdentityRule;

$primary = IdentityRule::primary(['id']);
$natural = IdentityRule::natural(['option_name']);
$composite = IdentityRule::composite(['object_id', 'term_taxonomy_id']);
$content = IdentityRule::content(['name', 'value']);

$key = $natural->key(['option_name' => 'siteurl']);
// "siteurl"
```

### `forSchema(TableSchema $schema): IdentityRule` (estático)

Resuelve la regla de identidad predeterminada a partir del esquema: clave principal, primera clave única y, por último, alternativa basada en el contenido.

### `toArray(): array`

Serializa la regla como `['type' => string, 'columns' => list<string>]`.

### `fromArray(array $data): IdentityRule` (estático)

Restaura una regla a partir de datos serializados.

---

## IdentityRuleSet

`Merql\Identity\IdentityRuleSet` guarda anulaciones de identidad específicas de cada tabla.

```php
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;

$rules = new IdentityRuleSet([
    'wp_options' => IdentityRule::natural(['option_name']),
]);

$rule = $rules->ruleFor($schema);
```

### `with(string $table, IdentityRule $rule): IdentityRuleSet`

Devuelve un conjunto de reglas nuevo con una anulación para una tabla.

### `conflictsFor(string $table, IdentityRule $rule, array $rows): array`

Devuelve las entradas de `IdentityConflict` cuando varias filas generan la misma clave para una regla.

### `toArray(): array`

Serializa las reglas de las tablas como una matriz segura para JSON.

### `fromArray(array $data): IdentityRuleSet` (estático)

Restaura un conjunto de reglas a partir de datos serializados.

---

## SnapshotStore

`Merql\Snapshot\SnapshotStore` guarda y carga instantáneas como archivos JSON.

### `setDirectory(string $directory): void`

Establece el directorio de almacenamiento. El valor predeterminado es `.merql/snapshots`.

### `save(Snapshot $snapshot): void`

Escribe una instantánea en disco.

### `load(string $name): Snapshot`

Carga una instantánea por nombre. Genera `SnapshotException` si no se encuentra.

### `exists(string $name): bool`

Comprueba si existe un archivo de instantánea.

### `delete(string $name): void`

Elimina un archivo de instantánea.

---

## Differ

`Merql\Diff\Differ` compara dos instantáneas y genera un conjunto de cambios.

### `diff(Snapshot $base, Snapshot $current): Changeset`

```php
use Merql\Diff\Differ;

$differ = new Differ();
$changeset = $differ->diff($baseSnapshot, $currentSnapshot);
```

### `valuesEqual(mixed $a, mixed $b): bool` (estático)

Compara dos valores convirtiéndolos a cadenas. NULL se trata como un valor distinto: `NULL === NULL` es verdadero, `NULL === ''` es falso.

---

## Changeset

`Merql\Diff\Changeset` es una colección de solo lectura de operaciones a nivel de fila.

```php
$changeset->inserts();             // list<RowInsert>
$changeset->updates();             // list<RowUpdate>
$changeset->deletes();             // list<RowDelete>
$changeset->isEmpty();             // bool
$changeset->count();               // int: total operations
$changeset->forTable('posts');     // Changeset: filtered to one table
```

---

## RowInsert, RowUpdate, RowDelete

Objetos de valor de solo lectura que representan cambios individuales en filas.

### RowInsert

```php
$insert->table;                    // string
$insert->rowKey;                   // string
$insert->values;                   // array<string, mixed>
```

### RowUpdate

```php
$update->table;                    // string
$update->rowKey;                   // string
$update->columnDiffs;              // list<ColumnDiff>
$update->fullRow;                  // array<string, mixed>: complete current row
$update->columnDiffMap();          // array<string, ColumnDiff>: keyed by column name
```

### RowDelete

```php
$delete->table;                    // string
$delete->rowKey;                   // string
$delete->oldValues;                // array<string, mixed>: values before deletion
```

---

## ColumnDiff

`Merql\Diff\ColumnDiff` representa un cambio de columna individual dentro de una actualización de fila.

```php
$diff->column;                     // string
$diff->oldValue;                   // mixed
$diff->newValue;                   // mixed
```

---

## ThreeWayMerge

`Merql\Merge\ThreeWayMerge` es el algoritmo central de fusión.

### `__construct(?CellMergeConfig $cellMergeConfig = null)`

Pasa un `CellMergeConfig` para habilitar la fusión a nivel de celda en columnas TEXT y JSON.

```php
use Merql\Merge\ThreeWayMerge;
use Merql\CellMerge\CellMergeConfig;

// Default: column-level merge only.
$merge = new ThreeWayMerge();

// With cell-level merge for TEXT and JSON columns.
$merge = new ThreeWayMerge(CellMergeConfig::auto());
```

### `merge(Snapshot $base, Snapshot $ours, Snapshot $theirs): MergeResult`

Realiza una fusión de tres vías. Calcula internamente conjuntos de cambios (de base a ours y de base a theirs) y los fusiona.

```php
$result = $merge->merge($baseSnapshot, $oursSnapshot, $theirsSnapshot);
```

### `patch(Snapshot $base, Snapshot $changes): MergeResult`

Atajo para la fusión de dos vías. Equivale a `merge($base, $base, $changes)`.

### `schemaMismatches(): array`

Devuelve `SchemaException[]` por cada diferencia de esquema detectada durante la última fusión (columnas añadidas o eliminadas entre instantáneas).

---

## MergeResult

`Merql\Merge\MergeResult` es un objeto de valor de solo lectura que contiene el resultado de la fusión.

```php
$result->isClean();                // bool: no conflicts
$result->operations();             // list<MergeOperation>
$result->conflicts();              // list<Conflict>
$result->operationCount();         // int
$result->conflictCount();          // int
$result->baseSnapshot();           // ?Snapshot
$result->schemaMismatches();       // list<SchemaException>
$result->hasSchemaMismatches();    // bool
```

---

## MergePlan

`Merql\Plan\MergePlan` es un modelo serializable para revisar una fusión.

```php
$plan->id;                        // string
$plan->baseSnapshot;              // string
$plan->oursSnapshot;              // string
$plan->theirsSnapshot;            // string
$plan->summary;                   // MergePlanSummary
$plan->changeGroups;              // list<MergePlanChangeGroup>
$plan->operations;                // list<MergePlanOperation>
$plan->conflicts;                 // list<MergePlanConflict>
$plan->schemaMismatches;          // list<string>
$plan->hash;                      // string
$plan->metadata;                  // array<string, mixed>
```

Usa `MergePlanSerializer::toJson()` y `::fromJson()` para realizar viajes de ida y vuelta estables de JSON.

## Selecciones

`ChangeGroupSelection` guarda los ID de los grupos de cambios preparados. Conviértelo en un `OperationSelection` al crear resultados de fusión seleccionados o planes de reversión.

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

$selection = ChangeGroupSelection::fromIds([$groupId]);
$operationSelection = $selection->toOperationSelection($plan);

$selected = (new SelectedMergeResultFactory())
    ->fromOperationSelection($plan, $operationSelection);
```

## RollbackPlan

`Merql\Rollback\RollbackPlan` guarda operaciones inversas y expectativas de desviación para una aplicación seleccionada.

```php
use Merql\Rollback\RollbackPlanBuilder;
use Merql\Rollback\RollbackPlanSerializer;

$rollback = (new RollbackPlanBuilder())->build(
    'rollback-1',
    $plan,
    $operationSelection,
    $liveBeforeRows,
);

$json = RollbackPlanSerializer::toJson($rollback);
```

Usa `RollbackDrift` antes de aplicar la reversión y, luego, `RollbackPlanApplier` para ejecutar las operaciones inversas.

---

## MergeOperation

`Merql\Merge\MergeOperation` es una única operación resuelta de la fusión.

```php
$op->type;                         // string: 'insert' | 'update' | 'delete'
$op->table;                        // string
$op->rowKey;                       // string
$op->values;                       // array<string, mixed>
$op->source;                       // string: 'ours' | 'theirs' | 'merged'
```

Constantes de tipo:

```php
MergeOperation::TYPE_INSERT;       // 'insert'
MergeOperation::TYPE_UPDATE;       // 'update'
MergeOperation::TYPE_DELETE;       // 'delete'
```

---

## Conflicto

`Merql\Merge\Conflict` representa un conflicto de fusión sin resolver.

```php
$conflict->table();                // string
$conflict->rowKey();               // string
$conflict->type();                 // string: 'update_update' | 'update_delete' | 'delete_update' | 'insert_insert'
$conflict->column();               // ?string (null for structural conflicts)
$conflict->oursValue();            // mixed
$conflict->theirsValue();          // mixed
$conflict->baseValue();            // mixed
$conflict->primaryKey(['id']);     // array<string, string>: decoded from rowKey
```

### Tipos de conflicto

| Tipo | Significado |
|---|---|
| `update_update` | Ambos lados cambiaron la misma columna a valores distintos |
| `update_delete` | Ours actualizó una fila que theirs eliminó |
| `delete_update` | Ours eliminó una fila que theirs actualizó |
| `insert_insert` | Ambos lados insertaron una fila con la misma clave principal |

---

## ConflictResolver

`Merql\Merge\ConflictResolver` aplica una política de resolución a todos los conflictos de un resultado de fusión.

### `resolve(MergeResult $result, ConflictPolicy $policy): MergeResult` (estático)

Devuelve un nuevo `MergeResult` con los conflictos resueltos según la política.

```php
use Merql\Merge\ConflictResolver;
use Merql\Merge\ConflictPolicy;

// Accept all "theirs" values when conflicts arise.
$resolved = ConflictResolver::resolve($result, ConflictPolicy::TheirsWins);

// Accept all "ours" values.
$resolved = ConflictResolver::resolve($result, ConflictPolicy::OursWins);

// Return unchanged (for manual inspection).
$resolved = ConflictResolver::resolve($result, ConflictPolicy::Manual);
```

---

## ConflictPolicy

`Merql\Merge\ConflictPolicy` es un enum respaldado con tres casos.

```php
use Merql\Merge\ConflictPolicy;

ConflictPolicy::OursWins;          // Resolve all conflicts in favor of "ours"
ConflictPolicy::TheirsWins;        // Resolve all conflicts in favor of "theirs"
ConflictPolicy::Manual;            // Leave conflicts unresolved for manual handling
```

---

## ColumnMerge

`Merql\Merge\ColumnMerge` realiza la lógica de combinación por columna para una sola fila. `ThreeWayMerge` lo usa internamente.

### `merge(string $table, string $rowKey, array $base, array $ours, array $theirs, ?CellMergeConfig $cellMergeConfig = null, array $columnTypes = []): array` (estático)

Devuelve `['values' => array, 'conflicts' => list<Conflict>]`.

```php
use Merql\Merge\ColumnMerge;

$result = ColumnMerge::merge(
    'posts',
    '42',
    ['title' => 'Hello', 'status' => 'draft'],     // base
    ['title' => 'Hello', 'status' => 'published'],  // ours
    ['title' => 'New Title', 'status' => 'draft'],   // theirs
);

// $result['values'] = ['title' => 'New Title', 'status' => 'published']
// $result['conflicts'] = [] (clean merge)
```

---

## Aplicador

`Merql\Apply\Applier` ejecuta un resultado de combinación en una base de datos.

### `__construct(DatabaseConnection $connection, ?Driver $driver = null)`

### `apply(MergeResult $result, ?Snapshot $base = null): ApplyResult`

Ejecuta todas las operaciones en una sola transacción. Revierte la transacción ante cualquier error y lo informa en `ApplyResult::errors()` en lugar de lanzarlo. Lanza `ConflictException` si el resultado tiene conflictos sin resolver.

```php
use Merql\Apply\Applier;

$applier = new Applier($connection);
$applied = $applier->apply($result);

echo "Rows affected: {$applied->rowsAffected()}\n";
if ($applied->hasErrors()) {
    print_r($applied->errors());
}
```

El orden de las claves externas se gestiona automáticamente: las inserciones se ordenan según las dependencias y las eliminaciones en orden inverso.

---

## ApplyResult

`Merql\Apply\ApplyResult` es un objeto de valor de solo lectura que devuelve el aplicador.

```php
$applied->rowsAffected();         // int
$applied->errors();               // list<string>
$applied->hasErrors();            // bool
```

Tras revertir una aplicación, `rowsAffected()` sigue contando las instrucciones ejecutadas antes del error ([#4](https://github.com/inline0/merql/issues/4)). Compruebe `hasErrors()` antes de confiar en el recuento.

---

## DryRun

`Merql\Apply\DryRun` genera instrucciones SQL legibles sin ejecutar nada.

### `generate(MergeResult $result, ?Snapshot $base = null, array $fkDependencies = [], ?Driver $driver = null): array` (estático)

Devuelve una lista de cadenas SQL con los valores insertados (solo para mostrarlos).

```php
use Merql\Apply\DryRun;

$statements = DryRun::generate($result);

foreach ($statements as $sql) {
    echo "{$sql};\n";
}

// Output:
// INSERT INTO `posts` (`id`, `title`) VALUES ('10', 'New Post');
// UPDATE `posts` SET `title` = 'Updated' WHERE `id` = '5';
// DELETE FROM `comments` WHERE `id` = '3';
```

---

## SqlGenerator

`Merql\Apply\SqlGenerator` genera instrucciones SQL parametrizadas. `Applier` y `DryRun` las usan internamente.

### `generate(MergeResult $result, ?Snapshot $base = null, array $fkDependencies = [], ?Driver $driver = null): array` (estático)

Devuelve `list<array{sql: string, params: list<scalar|null>}>`.

```php
use Merql\Apply\SqlGenerator;

$statements = SqlGenerator::generate($result);

foreach ($statements as $stmt) {
    $connection->execute($stmt['sql'], $stmt['params']);
}
```

---

## CellMergeConfig

`Merql\CellMerge\CellMergeConfig` configura estrategias de combinación a nivel de celda para columnas o tipos de columna específicos.

### `auto(): self` (estático)

Fábrica de conveniencia que asigna `TextCellMerger` a las columnas TEXT/LONGTEXT y `JsonCellMerger` a las columnas JSON.

```php
use Merql\CellMerge\CellMergeConfig;

$config = CellMergeConfig::auto();
```

### `forColumn(string $column, CellMerger $merger): self`

Asigne un combinador a una columna específica. Use `table.column` para nombres cualificados o solo `column` para todas las tablas.

```php
use Merql\CellMerge\CellMergeConfig;
use Merql\CellMerge\TextCellMerger;

$config = (new CellMergeConfig())
    ->forColumn('posts.content', new TextCellMerger())
    ->forColumn('description', new TextCellMerger());
```

### `forType(string $typePattern, CellMerger $merger): self`

Asigne un combinador a todas las columnas que coincidan con un patrón de tipo (coincidencia de subcadena sin distinguir mayúsculas y minúsculas).

```php
use Merql\CellMerge\CellMergeConfig;
use Merql\CellMerge\JsonCellMerger;

$config = (new CellMergeConfig())
    ->forType('json', new JsonCellMerger());
```

### `getMerger(string $table, string $column, string $columnType = ''): CellMerger`

Busca el combinador para una tabla, columna y tipo determinados. Prioridad: nombre de columna cualificado, nombre de columna no cualificado, patrón de tipo, predeterminado (opaco).

---

## CellMerger (interfaz)

`Merql\CellMerge\CellMerger` es la interfaz para las estrategias de combinación a nivel de celda.

```php
use Merql\CellMerge\CellMerger;
use Merql\CellMerge\CellMergeResult;

interface CellMerger
{
    public function merge(mixed $base, mixed $ours, mixed $theirs): CellMergeResult;
}
```

Se proporcionan tres implementaciones integradas.

### TextCellMerger

`Merql\CellMerge\TextCellMerger` combina texto multilínea mediante una comparación de diferencias de tres vías, línea por línea (el mismo algoritmo que usa git para el contenido de archivos). Si ambos lados editan líneas distintas, la combinación se realiza sin conflictos. Si ambos editan la misma línea de forma diferente, informa de un conflicto.

```php
use Merql\CellMerge\TextCellMerger;

$merger = new TextCellMerger();
$result = $merger->merge(
    "line 1\nline 2\nline 3",
    "line 1\nline 2 modified\nline 3",
    "line 1\nline 2\nline 3 modified",
);
// $result->clean === true
// $result->value === "line 1\nline 2 modified\nline 3 modified"
```

### JsonCellMerger

`Merql\CellMerge\JsonCellMerger` combina valores JSON comparando las claves de nivel superior por separado. Cada clave se combina mediante lógica de tres vías. Los objetos anidados se comparan como cadenas JSON opacas (no se combinan de forma recursiva).

```php
use Merql\CellMerge\JsonCellMerger;

$merger = new JsonCellMerger();
$result = $merger->merge(
    '{"a": 1, "b": 2}',
    '{"a": 10, "b": 2}',        // changed "a"
    '{"a": 1, "b": 20}',         // changed "b"
);
// $result->clean === true
// $result->value === '{"a":10,"b":20}'
```

### OpaqueCellMerger

`Merql\CellMerge\OpaqueCellMerger` es el valor predeterminado. Trata los valores como cadenas opacas y siempre informa de un conflicto cuando difieren.

---

## CellMergeResult

`Merql\CellMerge\CellMergeResult` es el tipo de retorno de todos los combinadores de celdas.

```php
$result->clean;                    // bool: true if merge succeeded
$result->value;                    // mixed: merged value (or ours on conflict)
$result->conflicts;                // int: number of conflicts

// Factory methods.
CellMergeResult::resolved($value);           // clean merge
CellMergeResult::conflict($oursValue, $n);   // conflict with count
```

---

## TableSchema

`Merql\Schema\TableSchema` es una representación de solo lectura de la estructura de una tabla.

```php
use Merql\Schema\TableSchema;

$schema = new TableSchema(
    name: 'posts',
    columns: ['id' => 'int', 'title' => 'varchar(255)', 'content' => 'text'],
    primaryKey: ['id'],
    uniqueKeys: [['slug']],
);

$schema->name;                     // string
$schema->columns;                  // array<string, string>: name to type
$schema->primaryKey;               // list<string>
$schema->uniqueKeys;               // list<list<string>>
$schema->hasPrimaryKey();          // bool
$schema->hasUniqueKeys();          // bool
$schema->columnNames();            // list<string>
```

---

## Sistema de controladores

merql usa un sistema de controladores conectables para abstraer las operaciones específicas de cada base de datos. Los controladores se detectan automáticamente a partir de la identidad del controlador de conexión.

### Driver (interfaz)

`Merql\Driver\Driver` define el contrato que implementan todos los controladores de bases de datos.

```php
interface Driver
{
    public function quoteIdentifier(string $name): string;
    public function listTables(DatabaseConnection $connection): array;
    public function readSchema(DatabaseConnection $connection, string $table): TableSchema;
    public function readForeignKeys(DatabaseConnection $connection): array;
    public function selectAll(string $table): string;
}
```

Implementaciones integradas: `MysqlDriver` y `SqliteDriver`.

### DriverFactory

`Merql\Driver\DriverFactory` detecta automáticamente el controlador a partir de una conexión.

```php
use Merql\Driver\DriverFactory;

$driver = DriverFactory::create($connection);
```

Registre un controlador personalizado para otras bases de datos:

```php
DriverFactory::register('pgsql', MyPostgresDriver::class);
```

---

## Filtros

### TableFilter

`Merql\Filter\TableFilter` incluye o excluye tablas mediante patrones glob.

```php
use Merql\Filter\TableFilter;

$filter = TableFilter::exclude(['cache_*', 'sessions', 'temp_*']);
$filter = TableFilter::include(['posts', 'users', 'comments']);
```

### ColumnFilter

`Merql\Filter\ColumnFilter` elimina columnas específicas de las instantáneas.

```php
use Merql\Filter\ColumnFilter;

$filter = ColumnFilter::ignore(['updated_at', 'created_at', 'modified_date']);
```

### RowFilter

`Merql\Filter\RowFilter` filtra filas mediante una función de predicado.

```php
use Merql\Filter\RowFilter;

$filter = RowFilter::create(function (string $table, array $row): bool {
    // Exclude trashed posts.
    if ($table === 'posts' && ($row['status'] ?? '') === 'trash') {
        return false;
    }
    return true;
});
```

---

## Excepciones

### SnapshotException

`Merql\Exceptions\SnapshotException` se lanza cuando no se encuentra una instantánea o su nombre no es válido.

```php
SnapshotException::notFound('baseline', '/path/to/baseline.json');
```

### MergeException

`Merql\Exceptions\MergeException` se lanza cuando falta una instantánea necesaria para una combinación.

```php
MergeException::snapshotRequired('base');
```

### ConflictException

`Merql\Exceptions\ConflictException` se lanza al intentar aplicar un resultado de combinación que tiene conflictos sin resolver.

```php
ConflictException::unresolved(3);
// Message: "Cannot apply merge with 3 unresolved conflict(s)"
```

### SchemaException

`Merql\Exceptions\SchemaException` se lanza (o se recopila) cuando los esquemas difieren entre instantáneas.

```php
SchemaException::mismatch('posts', 'column "tags" exists in current but not in base');
```

Las discrepancias de esquema no detienen la combinación. Se recopilan y están disponibles en `$result->schemaMismatches()` para su inspección.
