
# PHP API-Referenz

Vollständige Referenz aller öffentlichen Klassen und Methoden in merql.

## Merql (Fassade)

`Merql\Merql` ist der statische Einstiegspunkt für gängige Vorgänge. Die Klasse kapselt die nachgeordneten Klassen und verwaltet die dauerhafte Speicherung von Snapshots automatisch.

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

Initialisiere die Fassade mit einer Datenbankverbindung. Dies muss vor `snapshot()`, `apply()` oder `applyGuarded()` erfolgen. Rufe die Methode erneut auf, um die Datenbank zu wechseln, zum Beispiel, um unsere und ihre Snapshots aus zwei verschiedenen Kopien zu erstellen. Ohne `$driver` ruft die Methode `DriverFactory::create()` auf. Für andere Engines als MySQL und SQLite wird eine Ausnahme ausgelöst, sofern kein Treiber registriert ist. Übergib einen Treiber, um die automatische Erkennung zu überschreiben, oder einen Identitätsregelsatz, um die Zeilenidentität pro Tabelle während der Snapshot-Erfassung festzulegen.

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

Erfasse den aktuellen Datenbankzustand und speichere ihn dauerhaft auf dem Datenträger. Übergib ein Array mit Tabellennamen, um bestimmte Tabellen als Snapshot zu sichern, oder lass es weg, um alle Tabellen einzubeziehen.

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

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

Lade zwei benannte Snapshots und berechne die Änderungen zwischen ihnen. `init()` ist nicht erforderlich, da mit dauerhaft gespeicherten Snapshots gearbeitet wird.

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

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

Dreiwege-Merge von drei benannten Snapshots. Gibt ein Ergebnis mit konfliktfreien Vorgängen und etwaigen Konflikten zurück. Der Merge erfolgt auf Spaltenebene: Die Fassade erstellt `ThreeWayMerge` ohne `CellMergeConfig`, daher wird kein Merge auf Zellenebene angewendet. Verwende dafür direkt [`ThreeWayMerge`](#threewaymerge).

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

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

Zweiwege-Merge: Änderungen auf die Basis anwenden. Entspricht `merge($base, $base, $changes)`. Liefert immer ein konfliktfreies Ergebnis, da es keine gleichzeitigen Bearbeitungen gibt. Wie bei `merge()` nur auf Spaltenebene.

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

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

Wende ein konfliktfreies Merge-Ergebnis auf die Datenbank an, die an `init()` übergeben wurde. `init()` ist erforderlich. Löst `ConflictException` aus, wenn ungelöste Konflikte bestehen bleiben.

Jeder Vorgang im Ergebnis wird ausgeführt. Daher lässt sich das vollständige Ergebnis von `merge()` nur dann problemlos anwenden, wenn die Datenbank noch den Basiszustand enthält. Wenn die Datenbank bereits einen der beiden Zweige enthält, wende nur die Vorgänge an, die dort fehlen; siehe [Abgesicherte Anwendung](https://merql.inline0.dev/de/docs/apply/guarded).

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

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

Erstelle aus drei benannten Snapshots einen für UI geeigneten Merge-Plan. Der Plan enthält stabile Vorgangs-IDs, Änderungsgruppen-IDs, Vorgangskontext, Konflikte, Schemaabweichungen, eine Zusammenfassung, Metadaten und einen Plan-Hash. Wie bei `merge()` erfolgt der Merge auf Spaltenebene. Übergib für einen Merge auf Zellenebene ein zellweise zusammengeführtes Ergebnis an `MergePlanBuilder::build()`.

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

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

Wende ein konfliktfreies Merge-Ergebnis mit optimistischen Vorbedingungen für Live-Zeilen an. Vor Aktualisierungen und Löschungen müssen die aktuellen Live-Zeilen mit `$expectedLive` übereinstimmen. Vor dem Einfügen darf die Zielidentität noch nicht existieren. `$expectedLive` ist ein Snapshot der Zieldatenbank (sie, wenn unsere Änderungen in die Datenbank übernommen werden, aus der ihr Snapshot stammt). Wenn eine Anweisung keine Zeilen betrifft, wird die Transaktion zurückgerollt.

```php
use Merql\Snapshot\SnapshotStore;

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

### `reset(): void`

Setze die internen Verbindungs- und Snapshotter-Instanzen zurück. Wird hauptsächlich in Tests verwendet, um den Zustand zwischen Durchläufen zurückzusetzen.

---

## Verbindung

`Merql\Connection` erstellt Verbindungsadapter mit sinnvollen Standardeinstellungen. Bei PDO-Verbindungen werden Ausnahmen bei Fehlern, assoziativer Abrufmodus, keine emulierten vorbereiteten Anweisungen und Stringkonvertierung beim Abruf verwendet. Der mysqli-Adapter normalisiert abgerufene Werte auf dieselbe Zeichenfolge-oder-null-Form.

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

Erstellt eine SQLite-Verbindung. Aktiviert Fremdschlüssel automatisch über `PRAGMA foreign_keys = ON`.

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

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

Stelle eine Verbindung mit einer beliebigen PDO-DSN-Zeichenfolge her. merql enthält nur Treiber für `mysql` und `sqlite`. Andere DSNs stellen zwar eine Verbindung her, aber `Merql::init()`, `Snapshotter` und die Applier lösen `Unsupported database driver` aus, sofern kein Treiber mit `DriverFactory::register()` registriert ist.

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

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

Umschließt eine vorhandene PDO-Instanz.

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

Umschließt eine vorhandene mysqli-Instanz.

### Adaptervertrag

Alle Adapter implementieren `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()` und `scalar()` akzeptieren positionale `list<scalar|null>`-Parameter für Platzhalter `?`. `query()` gibt `list<array<string, scalar|null>>` zurück. Werte werden bei PDO- und mysqli-Adaptern auf dieselbe Zeichenfolge-oder-null-Form normalisiert.

---

## Snapshotter

`Merql\Snapshot\Snapshotter` erfasst den Datenbankzustand. Anders als die Fassade bietet es dir die volle Kontrolle über Filter und speichert Snapshots nicht automatisch dauerhaft.

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

Wenn kein Treiber angegeben ist, wird er anhand der Verbindungsidentität automatisch erkannt. Übergib einen `IdentityRuleSet`, wenn eine tabellenspezifische Zeilenidentität die Schema-Standardeinstellungen überschreiben soll.

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

Erfasse den aktuellen Zustand. Übergib Tabellennamen, um den Umfang einzugrenzen. Übergib Filterobjekte, um Tabellen, Spalten oder Zeilen auszuschließen.

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

Erfasse physische Tabellen unter kanonischen Tabellennamen. Verwende dies, wenn dieselbe logische Tabelle in verschiedenen Umgebungen unterschiedliche physische Namen hat.

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

Der resultierende Snapshot speichert kanonische Tabellennamen und bewahrt zugleich das physische Tabellenschema, konfigurierte Identitätsspalten, Zeilenschlüssel und Filter.

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

Erstellt ohne Datenbankverbindung einen Snapshot aus Rohdaten. Nützlich für Tests oder beim Importieren von Daten aus einer anderen Quelle.

```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` ist ein schreibgeschütztes Wertobjekt, das einen vollständigen Datenbankzustand darstellt.

```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` enthält die Zeilen, Fingerprints und das Schema einer Tabelle.

```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` sind Eingabedaten für `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` definiert die Spalten, aus denen Zeilenschlüssel erstellt werden.

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

Löst die standardmäßige Identitätsregel anhand des Schemas auf: Primärschlüssel, erster eindeutiger Schlüssel, dann Rückgriff auf den Inhalt.

### `toArray(): array`

Serialisiert die Regel als `['type' => string, 'columns' => list<string>]`.

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

Stellt eine Regel aus serialisierten Daten wieder her.

---

## IdentityRuleSet

`Merql\Identity\IdentityRuleSet` speichert tabellenspezifische Identitätsüberschreibungen.

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

Gibt einen neuen Regelsatz mit einer hinzugefügten Tabellenüberschreibung zurück.

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

Gibt `IdentityConflict`-Einträge zurück, wenn mehrere Zeilen für eine Regel denselben Schlüssel ergeben.

### `toArray(): array`

Serialisiert Tabellenregeln als JSON-sicheres Array.

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

Stellt einen Regelsatz aus serialisierten Daten wieder her.

---

## SnapshotStore

`Merql\Snapshot\SnapshotStore` speichert und lädt Snapshots als JSON-Dateien.

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

Legt das Speicherverzeichnis fest. Standard ist `.merql/snapshots`.

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

Schreibt einen Snapshot auf die Festplatte.

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

Lädt einen Snapshot anhand seines Namens. Wirft `SnapshotException`, wenn er nicht gefunden wird.

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

Prüft, ob eine Snapshot-Datei existiert.

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

Entfernt eine Snapshot-Datei.

---

## Differ

`Merql\Diff\Differ` vergleicht zwei Snapshots und erstellt einen Änderungssatz.

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

Vergleicht zwei Werte nach Umwandlung in Zeichenfolgen. NULL gilt als eigenständiger Wert: `NULL === NULL` ist true, `NULL === ''` ist false.

---

## Changeset

`Merql\Diff\Changeset` ist eine schreibgeschützte Sammlung von Operationen auf Zeilenebene.

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

Schreibgeschützte Wertobjekte, die einzelne Zeilenänderungen darstellen.

### 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` stellt eine einzelne Spaltenänderung innerhalb einer Zeilenaktualisierung dar.

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

---

## ThreeWayMerge

`Merql\Merge\ThreeWayMerge` ist der zentrale Merge-Algorithmus.

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

Übergib eine `CellMergeConfig`, um das zellenweise Zusammenführen für TEXT- und JSON-Spalten zu aktivieren.

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

Führt drei Wege zusammen. Berechnet intern Änderungssätze (Basis zu unseren Änderungen, Basis zu deren Änderungen) und führt sie zusammen.

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

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

Kurzform für einen Zwei-Wege-Merge. Entspricht `merge($base, $base, $changes)`.

### `schemaMismatches(): array`

Gibt `SchemaException[]` für alle Schemaunterschiede zurück, die beim letzten Merge erkannt wurden (zwischen Snapshots hinzugefügte oder entfernte Spalten).

---

## MergeResult

`Merql\Merge\MergeResult` ist ein schreibgeschütztes Wertobjekt mit dem Ergebnis des Merges.

```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` ist ein serialisierbares Prüfmodell für einen Merge.

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

Verwende `MergePlanSerializer::toJson()` und `::fromJson()` für stabile JSON-Roundtrips.

## Selections

`ChangeGroupSelection` speichert IDs vorgemerkter Änderungsgruppen. Wandle es in ein `OperationSelection` um, wenn du ausgewählte Merge-Ergebnisse oder Rollback-Pläne erstellst.

```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` speichert inverse Operationen und Erwartungen an Abweichungen für eine ausgewählte Anwendung.

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

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

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

Verwende `RollbackDrift` vor dem Anwenden des Rollbacks und anschließend `RollbackPlanApplier`, um die inversen Operationen auszuführen.

---

## MergeOperation

`Merql\Merge\MergeOperation` ist eine einzelne aufgelöste Operation des Merges.

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

Typkonstanten:

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

---

## Conflict

`Merql\Merge\Conflict` stellt einen ungelösten Merge-Konflikt dar.

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

### Konflikttypen

| Typ | Bedeutung |
|---|---|
| `update_update` | Beide Seiten haben dieselbe Spalte auf unterschiedliche Werte geändert |
| `update_delete` | Unsere Seite hat eine Zeile aktualisiert, die deren Seite gelöscht hat |
| `delete_update` | Unsere Seite hat eine Zeile gelöscht, die deren Seite aktualisiert hat |
| `insert_insert` | Beide Seiten haben eine Zeile mit demselben Primärschlüssel eingefügt |

---

## ConflictResolver

`Merql\Merge\ConflictResolver` wendet eine Auflösungsrichtlinie auf alle Konflikte in einem Merge-Ergebnis an.

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

Gibt ein neues `MergeResult` zurück, in dem Konflikte gemäß der Richtlinie aufgelöst wurden.

```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` ist ein Backed Enum mit drei Fällen.

```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` führt die Merge-Logik für einzelne Spalten einer Zeile aus. Wird intern von `ThreeWayMerge` verwendet.

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

Gibt `['values' => array, 'conflicts' => list<Conflict>]` zurück.

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

---

## Applier

`Merql\Apply\Applier` wendet ein Merge-Ergebnis auf eine Datenbank an.

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

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

Führt alle Operationen in einer Transaktion aus. Bei einem Fehler wird ein Rollback ausgeführt und der Fehler in `ApplyResult::errors()` gemeldet, statt eine Exception auszulösen. Wirft `ConflictException`, wenn das Ergebnis ungelöste Konflikte enthält.

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

Die Reihenfolge von Fremdschlüsseln wird automatisch berücksichtigt: Einfügungen werden nach Abhängigkeiten sortiert, Löschungen in umgekehrter Abhängigkeitsreihenfolge.

---

## ApplyResult

`Merql\Apply\ApplyResult` ist ein schreibgeschütztes Wertobjekt, das vom Applier zurückgegeben wird.

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

Nach einem zurückgerollten Apply zählt `rowsAffected()` weiterhin die Anweisungen, die vor dem Fehler ausgeführt wurden ([#4](https://github.com/inline0/merql/issues/4)). Prüfe `hasErrors()`, bevor du dich auf die Anzahl verlässt.

---

## DryRun

`Merql\Apply\DryRun` erstellt menschenlesbare SQL, ohne etwas auszuführen.

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

Gibt eine Liste von SQL-Zeichenfolgen zurück, in denen Werte eingefügt sind (nur zur Anzeige).

```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` erstellt parametrisierte SQL-Anweisungen. Wird intern von `Applier` und `DryRun` verwendet.

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

Gibt `list<array{sql: string, params: list<scalar|null>}>` zurück.

```php
use Merql\Apply\SqlGenerator;

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

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

---

## CellMergeConfig

`Merql\CellMerge\CellMergeConfig` konfiguriert zellenweise Merge-Strategien für bestimmte Spalten oder Spaltentypen.

### `auto(): self` (statisch)

Praktische Factory, die TEXT/LONGTEXT-Spalten `TextCellMerger` und JSON-Spalten `JsonCellMerger` zuweist.

```php
use Merql\CellMerge\CellMergeConfig;

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

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

Weist einer bestimmten Spalte einen Merger zu. Verwende `table.column` für qualifizierte Namen oder einfach `column` für alle Tabellen.

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

Weist allen Spalten, die einem Typsuchmuster entsprechen, einen Merger zu (Groß-/Kleinschreibung wird bei der Teilzeichenfolgensuche ignoriert).

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

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

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

Sucht den Merger für eine Tabelle, Spalte und einen Typ. Priorität: qualifizierter Spaltenname, nicht qualifizierter Spaltenname, Typsuchmuster, Standard (opaque).

---

## CellMerger (Schnittstelle)

`Merql\CellMerge\CellMerger` ist die Schnittstelle für zellenweise Merge-Strategien.

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

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

Drei integrierte Implementierungen sind enthalten.

### TextCellMerger

`Merql\CellMerge\TextCellMerger` führt mehrzeiligen Text mit einem dreifachen zeilenweisen Diff zusammen (demselben Algorithmus, den git für Dateiinhalt verwendet). Bearbeiten beide Seiten unterschiedliche Zeilen, gelingt der Merge ohne Konflikt. Bearbeiten beide Seiten dieselbe Zeile unterschiedlich, wird ein Konflikt gemeldet.

```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` führt JSON-Werte zusammen, indem Schlüssel der obersten Ebene unabhängig voneinander verglichen werden. Jeder Schlüssel wird mit einer Drei-Wege-Logik zusammengeführt. Verschachtelte Objekte werden als undurchsichtige JSON-Zeichenfolgen verglichen (ohne rekursives Zusammenführen).

```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` ist die Standardeinstellung. Behandelt Werte als undurchsichtige Zeichenfolgen und meldet bei Unterschieden immer einen Konflikt.

---

## CellMergeResult

`Merql\CellMerge\CellMergeResult` ist der Rückgabetyp aller Cell Merger.

```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` ist eine schreibgeschützte Darstellung der Tabellenstruktur.

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

---

## Treibersystem

merql verwendet ein erweiterbares Treibersystem, um datenbankspezifische Operationen zu abstrahieren. Treiber werden anhand der Verbindungs-Treiberkennung automatisch erkannt.

### Driver (Schnittstelle)

`Merql\Driver\Driver` definiert den Vertrag, den alle Datenbanktreiber implementieren.

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

Integrierte Implementierungen: `MysqlDriver` und `SqliteDriver`.

### DriverFactory

`Merql\Driver\DriverFactory` erkennt den Treiber anhand einer Verbindung automatisch.

```php
use Merql\Driver\DriverFactory;

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

Registriere einen benutzerdefinierten Treiber für andere Datenbanken:

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

---

## Filter

### TableFilter

`Merql\Filter\TableFilter` schließt Tabellen anhand von Glob-Mustern ein oder aus.

```php
use Merql\Filter\TableFilter;

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

### ColumnFilter

`Merql\Filter\ColumnFilter` entfernt bestimmte Spalten aus Snapshots.

```php
use Merql\Filter\ColumnFilter;

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

### RowFilter

`Merql\Filter\RowFilter` filtert Zeilen mit einer Prädikatfunktion.

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

---

## Ausnahmen

### SnapshotException

`Merql\Exceptions\SnapshotException` wird ausgelöst, wenn ein Snapshot nicht gefunden wird oder einen ungültigen Namen hat.

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

### MergeException

`Merql\Exceptions\MergeException` wird ausgelöst, wenn für einen Merge ein erforderlicher Snapshot fehlt.

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

### ConflictException

`Merql\Exceptions\ConflictException` wird ausgelöst, wenn versucht wird, ein Merge-Ergebnis mit ungelösten Konflikten anzuwenden.

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

### SchemaException

`Merql\Exceptions\SchemaException` wird ausgelöst (oder erfasst), wenn sich die Schemas von Snapshots unterscheiden.

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

Schemaabweichungen verhindern den Merge nicht. Sie werden erfasst und können über `$result->schemaMismatches()` geprüft werden.
