
# Référence PHP API

Référence complète de toutes les classes et méthodes publiques de merql.

## Merql (façade)

`Merql\Merql` est le point d'entrée statique pour les opérations courantes. Il encapsule les classes de niveau inférieur et gère automatiquement la persistance des instantanés.

```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`

Initialisez la façade avec une connexion à la base de données. Doit être appelé avant
`snapshot()`, `apply()` ou `applyGuarded()` ; rappelez cette méthode pour changer de base de données,
par exemple pour créer des instantanés de ours et theirs à partir de deux copies différentes. En l'absence de
`$driver`, la méthode appelle `DriverFactory::create()`, qui lève une exception pour les moteurs autres que
MySQL et SQLite, sauf si un pilote est enregistré. Fournissez un pilote pour remplacer la détection automatique, ou un ensemble de règles d'identité pour remplacer l'identité des lignes par table lors de la capture d'instantanés.

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

Capturez l'état actuel de la base de données et enregistrez-le sur disque. Passez un tableau de noms de tables pour créer un instantané de certaines tables, ou omettez-le pour inclure toutes les tables.

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

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

Chargez deux instantanés nommés et calculez les modifications entre eux. `init()` n'est pas nécessaire, car cette opération utilise les instantanés persistés.

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

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

Fusion à trois voies de trois instantanés nommés. Renvoie un résultat contenant les opérations sans conflit et les éventuels conflits. La fusion s'effectue au niveau des colonnes : la façade construit `ThreeWayMerge` sans `CellMergeConfig`, la fusion au niveau des cellules n'est donc pas appliquée. Utilisez directement [`ThreeWayMerge`](#threewaymerge) pour cela.

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

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

Fusion à deux voies : appliquez les modifications à la base. Équivalent à `merge($base, $base, $changes)`. Produit toujours un résultat sans conflit, car il n'y a pas de modifications concurrentes. Au niveau des colonnes uniquement, comme `merge()`.

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

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

Exécutez un résultat de fusion sans conflit sur la base de données transmise à `init()`. Nécessite `init()`. Lève `ConflictException` si des conflits non résolus subsistent.

Toutes les opérations du résultat sont exécutées. Le résultat complet de `merge()` ne s'applique donc correctement qu'à une base de données qui contient toujours l'état de base. Pour une base de données qui contient déjà l'un des côtés, appliquez une sélection des opérations qui lui manquent ; voir [Application protégée](https://merql.inline0.dev/fr/docs/apply/guarded).

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

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

Créez un plan de fusion compatible avec UI à partir de trois instantanés nommés. Le plan comprend des ID d'opération stables, des ID de groupes de modifications, le contexte des opérations, les conflits, les différences de schéma, un résumé, des métadonnées et un hachage du plan. La fusion s'effectue au niveau des colonnes, comme dans `merge()` ; transmettez un résultat fusionné au niveau des cellules à `MergePlanBuilder::build()` pour effectuer une fusion à ce niveau.

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

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

Appliquez un résultat de fusion sans conflit avec des préconditions optimistes sur les lignes en temps réel. Pour les mises à jour et les suppressions, les lignes actives doivent correspondre à `$expectedLive` ; pour les insertions, l'identité cible ne doit pas déjà exister. `$expectedLive` est un instantané de la base de données cible (theirs, lorsque ours est promu dans la base à partir de laquelle theirs a été créé). Toute instruction qui ne modifie aucune ligne annule la transaction.

```php
use Merql\Snapshot\SnapshotStore;

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

### `reset(): void`

Efface les instances internes de connexion et de Snapshotter. Sert principalement à réinitialiser l'état entre les exécutions de tests.

---

## Connexion

`Merql\Connection` construit des adaptateurs de connexion avec des valeurs par défaut adaptées. Les connexions PDO utilisent les exceptions en cas d'erreur, le mode de récupération associatif, les préparations non émulées et la récupération des valeurs sous forme de chaînes. L'adaptateur mysqli normalise les valeurs récupérées pour obtenir la même forme chaîne ou null.

### `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`

Crée une connexion SQLite. Active automatiquement les clés étrangères via `PRAGMA foreign_keys = ON`.

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

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

Connectez-vous à l'aide de n'importe quelle chaîne DSN PDO. merql fournit des pilotes uniquement pour `mysql` et `sqlite` ; les autres DSN permettent d'ouvrir une connexion, mais `Merql::init()`, `Snapshotter` et les composants d'application lèvent `Unsupported database driver`, sauf si un pilote est enregistré avec `DriverFactory::register()`.

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

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

Encapsule une instance PDO existante.

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

Encapsule une instance mysqli existante.

### Contrat des adaptateurs

Tous les adaptateurs implémentent `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()` et `scalar()` acceptent des paramètres positionnels `list<scalar|null>`
pour les espaces réservés `?`. `query()` renvoie
`list<array<string, scalar|null>>` ; les valeurs sont normalisées sous la même forme chaîne ou null
avec les adaptateurs PDO et mysqli.

---

## Snapshotter

`Merql\Snapshot\Snapshotter` capture l'état de la base de données. Contrairement à la façade, il vous donne un contrôle total sur les filtres et n'enregistre pas automatiquement les instantanés.

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

Si aucun pilote n'est fourni, il est détecté automatiquement à partir de l'identité de la connexion. Fournissez un
`IdentityRuleSet` si une identité de ligne propre à une table doit remplacer les
valeurs par défaut du schéma.

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

Capturez l'état actuel. Indiquez des noms de tables pour limiter la portée. Passez des objets de filtre pour exclure des tables, des colonnes ou des lignes.

```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`

Capturez les tables physiques en leur attribuant des noms de table canoniques. Utilisez cette méthode lorsque la même table logique porte des noms physiques différents selon les environnements.

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

L'instantané obtenu enregistre les noms de tables canoniques tout en conservant le schéma des tables physiques, les colonnes d'identité configurées, les clés des lignes et les filtres.

### `fromData(string $name, array $tableData): Snapshot` (statique)

Créez un instantané à partir de données brutes, sans connexion à une base de données. Utile pour les tests ou l'importation de données depuis une autre source.

```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']),
]);
```

---

## Instantané

`Merql\Snapshot\Snapshot` est un objet valeur en lecture seule qui représente un état complet de base de données.

```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` contient les lignes, les empreintes numériques et le schéma d'une table.

```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` représente les données d'entrée de `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` définit les colonnes utilisées pour construire les clés des lignes.

```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` (statique)

Résout la règle d'identité par défaut à partir du schéma : clé primaire, première clé unique,
puis repli sur le contenu.

### `toArray(): array`

Sérialise la règle en `['type' => string, 'columns' => list<string>]`.

### `fromArray(array $data): IdentityRule` (statique)

Hydrate une règle à partir de données sérialisées.

---

## IdentityRuleSet

`Merql\Identity\IdentityRuleSet` stocke les remplacements de règles d'identité propres aux tables.

```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`

Renvoie un nouvel ensemble de règles auquel une substitution pour une table a été ajoutée.

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

Renvoie les entrées `IdentityConflict` lorsque plusieurs lignes produisent la même clé pour une
règle.

### `toArray(): array`

Sérialise les règles de table sous forme de tableau compatible avec JSON.

### `fromArray(array $data): IdentityRuleSet` (statique)

Hydrate un ensemble de règles à partir de données sérialisées.

---

## SnapshotStore

`Merql\Snapshot\SnapshotStore` enregistre et charge les instantanés sous forme de fichiers JSON.

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

Définit le répertoire de stockage. La valeur par défaut est `.merql/snapshots`.

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

Écrit un instantané sur le disque.

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

Charge un instantané par nom. Lève `SnapshotException` s'il est introuvable.

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

Vérifie si un fichier d'instantané existe.

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

Supprime un fichier d'instantané.

---

## Comparateur

`Merql\Diff\Differ` compare deux instantanés et produit un ensemble de modifications.

### `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` (statique)

Compare deux valeurs après coercition en chaînes. NULL est traité comme une valeur distincte : `NULL === NULL` est vrai, `NULL === ''` est faux.

---

## Ensemble de modifications

`Merql\Diff\Changeset` est une collection en lecture seule d'opérations au niveau des lignes.

```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

Objets valeur en lecture seule représentant des modifications individuelles de lignes.

### 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` représente une modification de colonne unique au sein d'une mise à jour de ligne.

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

---

## ThreeWayMerge

`Merql\Merge\ThreeWayMerge` est l'algorithme central de fusion.

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

Fournissez un `CellMergeConfig` pour activer la fusion au niveau des cellules pour les colonnes TEXT et 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`

Effectue une fusion à trois voies. Calcule les ensembles de modifications en interne (base vers ours, base vers theirs), puis les fusionne.

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

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

Raccourci pour une fusion à deux voies. Équivalent à `merge($base, $base, $changes)`.

### `schemaMismatches(): array`

Renvoie `SchemaException[]` pour toute différence de schéma détectée lors de la dernière fusion (colonnes ajoutées ou supprimées entre les instantanés).

---

## MergeResult

`Merql\Merge\MergeResult` est un objet valeur en lecture seule contenant le résultat de la fusion.

```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` est un modèle sérialisable permettant d'examiner une fusion.

```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>
```

Utilisez `MergePlanSerializer::toJson()` et `::fromJson()` pour des allers-retours JSON stables.

## Sélections

`ChangeGroupSelection` stocke les ID des groupes de modifications préparés. Convertissez-le en
`OperationSelection` lors de la création de résultats de fusion sélectionnés ou de plans de restauration.

```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` stocke les opérations inverses et les attentes de dérive pour une application sélectionnée.

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

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

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

Utilisez `RollbackDrift` avant d'appliquer le retour arrière, puis `RollbackPlanApplier` pour exécuter les opérations inverses.

---

## MergeOperation

`Merql\Merge\MergeOperation` est une opération unique résolue issue de la fusion.

```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 type :

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

---

## Conflit

`Merql\Merge\Conflict` représente un conflit de fusion non résolu.

```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
```

### Types de conflit

| Type | Signification |
|---|---|
| `update_update` | Les deux côtés ont modifié la même colonne avec des valeurs différentes |
| `update_delete` | Notre côté a mis à jour une ligne que leur côté a supprimée |
| `delete_update` | Notre côté a supprimé une ligne que leur côté a mise à jour |
| `insert_insert` | Les deux côtés ont inséré une ligne avec la même clé primaire |

---

## ConflictResolver

`Merql\Merge\ConflictResolver` applique une règle de résolution à tous les conflits d'un résultat de fusion.

### `resolve(MergeResult $result, ConflictPolicy $policy): MergeResult` (statique)

Renvoie un nouveau `MergeResult` dont les conflits sont résolus selon la règle.

```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` est une énumération adossée comportant trois cas.

```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` applique la logique de fusion colonne par colonne pour une seule ligne. Utilisé en interne par `ThreeWayMerge`.

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

Renvoie `['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)
```

---

## Appliqueur

`Merql\Apply\Applier` exécute un résultat de fusion sur une base de données.

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

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

Exécute toutes les opérations dans une seule transaction. Annule la transaction en cas d'erreur et la signale dans `ApplyResult::errors()` au lieu de lever une exception. Lève `ConflictException` si le résultat comporte des conflits non résolus.

```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());
}
```

L'ordre des clés étrangères est géré automatiquement : les insertions sont triées selon l'ordre des dépendances, et les suppressions dans l'ordre inverse.

---

## ApplyResult

`Merql\Apply\ApplyResult` est un objet valeur en lecture seule renvoyé par l'appliqueur.

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

Après une application annulée, `rowsAffected()` compte toujours les instructions exécutées avant l'échec ([#4](https://github.com/inline0/merql/issues/4)). Vérifiez `hasErrors()` avant de vous fier à ce décompte.

---

## DryRun

`Merql\Apply\DryRun` génère des SQL lisibles sans rien exécuter.

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

Renvoie une liste de chaînes SQL avec les valeurs insérées (à des fins d'affichage uniquement).

```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` génère des instructions SQL paramétrées. Utilisé en interne par `Applier` et `DryRun`.

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

Renvoie `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` configure des stratégies de fusion au niveau des cellules pour des colonnes ou types de colonnes spécifiques.

### `auto(): self` (statique)

Fabrique pratique qui attribue `TextCellMerger` aux colonnes TEXT/LONGTEXT et `JsonCellMerger` aux colonnes JSON.

```php
use Merql\CellMerge\CellMergeConfig;

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

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

Attribue un fusionneur à une colonne spécifique. Utilisez `table.column` pour les noms qualifiés ou simplement `column` pour toutes les tables.

```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`

Attribue un fusionneur à toutes les colonnes correspondant à un modèle de type (correspondance de sous-chaîne sans distinction de casse).

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

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

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

Recherche le fusionneur pour une table, une colonne et un type donnés. Priorité : nom de colonne qualifié, nom de colonne non qualifié, modèle de type, valeur par défaut (opaque).

---

## CellMerger (interface)

`Merql\CellMerge\CellMerger` est l'interface des stratégies de fusion au niveau des cellules.

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

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

Trois implémentations intégrées sont fournies.

### TextCellMerger

`Merql\CellMerge\TextCellMerger` fusionne le texte multiligne à l’aide d’une comparaison ligne par ligne à trois voies (le même algorithme que git utilise pour le contenu des fichiers). Lorsque les deux côtés modifient des lignes différentes, la fusion est propre. Lorsqu’ils modifient différemment la même ligne, un conflit est signalé.

```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` fusionne les valeurs JSON en comparant indépendamment les clés de premier niveau. Chaque clé est fusionnée selon une logique à trois voies. Les objets imbriqués sont comparés comme des chaînes JSON opaques (sans fusion récursive).

```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` est la valeur par défaut. Il traite les valeurs comme des chaînes opaques et signale toujours un conflit lorsqu'elles diffèrent.

---

## CellMergeResult

`Merql\CellMerge\CellMergeResult` est le type de retour de tous les fusionneurs de cellules.

```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` est une représentation en lecture seule de la structure d'une table.

```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>
```

---

## Système de pilotes

merql utilise un système de pilotes extensible pour abstraire les opérations propres à chaque base de données. Les pilotes sont détectés automatiquement à partir de l'identité du pilote de connexion.

### Driver (interface)

`Merql\Driver\Driver` définit le contrat mis en œuvre par tous les pilotes de base de données.

```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;
}
```

Implémentations intégrées : `MysqlDriver` et `SqliteDriver`.

### DriverFactory

`Merql\Driver\DriverFactory` détecte automatiquement le pilote à partir d'une connexion.

```php
use Merql\Driver\DriverFactory;

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

Enregistrez un pilote personnalisé pour d'autres bases de données :

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

---

## Filtres

### TableFilter

`Merql\Filter\TableFilter` inclut ou exclut des tables à l'aide de modèles glob.

```php
use Merql\Filter\TableFilter;

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

### ColumnFilter

`Merql\Filter\ColumnFilter` retire des instantanés des colonnes spécifiques.

```php
use Merql\Filter\ColumnFilter;

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

### RowFilter

`Merql\Filter\RowFilter` filtre les lignes à l'aide d'une fonction prédicat.

```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;
});
```

---

## Exceptions

### SnapshotException

`Merql\Exceptions\SnapshotException` est levée lorsqu'un instantané est introuvable ou porte un nom invalide.

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

### MergeException

`Merql\Exceptions\MergeException` est levée lorsqu'un instantané requis pour une fusion est manquant.

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

### ConflictException

`Merql\Exceptions\ConflictException` est levée lorsqu'on tente d'appliquer un résultat de fusion comportant des conflits non résolus.

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

### SchemaException

`Merql\Exceptions\SchemaException` est levée (ou recueillie) lorsque les schémas diffèrent entre les instantanés.

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

Les divergences de schéma n'interrompent pas la fusion. Elles sont recueillies et consultables via `$result->schemaMismatches()`.
