
# SQL-Generierung

`SqlGenerator` wandelt ein `MergeResult` in parametrisierte SQL-Anweisungen um. Alle Werte werden als Parameter gebunden und niemals in die SQL-Zeichenfolge eingefügt.

```php
use Merql\Apply\SqlGenerator;

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

foreach ($statements as $stmt) {
    echo $stmt['sql'] . "\n";
    // INSERT INTO `posts` (`id`, `title`, `status`) VALUES (?, ?, ?)
    print_r($stmt['params']);
    // [42, 'New Title', 'publish']
}
```

Verwende `generateGuarded()`, wenn ein geprüfter Zusammenführungsplan nur angewendet werden soll,
wenn Live-Zeilen noch einem erwarteten Snapshot entsprechen.

```php
use Merql\Apply\SqlGenerator;
use Merql\Snapshot\SnapshotStore;

$statements = SqlGenerator::generateGuarded(
    $selectedResult,
    SnapshotStore::load('theirs'),
);
```

## Anweisungsformat

Jede Anweisung ist ein assoziatives Array mit zwei Schlüsseln:

```php
[
    'sql'    => 'INSERT INTO `posts` (`id`, `title`) VALUES (?, ?)',
    'params' => [42, 'New Title'],
]
```

Bezeichner (Tabellen- und Spaltennamen) werden mit der Maskierungsweise des Datenbanktreibers in Anführungszeichen gesetzt. MySQL verwendet Backticks, SQLite doppelte Anführungszeichen. Ohne Treiber werden standardmäßig Backticks verwendet.

## Reihenfolge der Operationen

Operationen werden nach Typ gruppiert und in dieser Reihenfolge ausgeführt:

1. **INSERTs** zuerst -- neue Zeilen müssen vorhanden sein, bevor Aktualisierungen auf sie verweisen können.
2. **UPDATEs** danach.
3. **DELETEs** zuletzt -- Zeilen müssen zuerst von Verweisen befreit werden.

```php
$statements = SqlGenerator::generate($result);
// All INSERTs, then all UPDATEs, then all DELETEs.
```

## Reihenfolge der Fremdschlüssel

Wenn Fremdschlüsselabhängigkeiten angegeben sind, werden die Operationen innerhalb jeder Gruppe topologisch sortiert. Bei Einfügungen und Aktualisierungen werden übergeordnete Tabellen vor untergeordneten verarbeitet. Bei Löschungen werden untergeordnete Tabellen vor übergeordneten verarbeitet.

```php
$fkDependencies = [
    'comments' => ['posts'],        // comments references posts
    'post_meta' => ['posts'],       // post_meta references posts
];

$statements = SqlGenerator::generate($result, fkDependencies: $fkDependencies);
// INSERTs: posts first, then comments and post_meta
// DELETEs: comments and post_meta first, then posts
```

`ForeignKeyResolver` führt eine topologische Sortierung des Abhängigkeitsgraphen durch. Zirkuläre Abhängigkeiten werden erkannt und aufgelöst, damit keine Endlosschleifen entstehen.

### Automatische Erkennung von Fremdschlüsseln

`Applier` liest Fremdschlüsselbeziehungen automatisch aus der Datenbank aus:

```php
use Merql\Apply\Applier;

$applier = new Applier($connection);
$applied = $applier->apply($result);
// FK dependencies are read from the database via the driver.
```

## Beispiele für generierte SQL

### INSERT

```sql
INSERT INTO `posts` (`id`, `title`, `content`, `status`) VALUES (?, ?, ?, ?)
-- params: [42, 'New Post', 'Body text', 'draft']
```

Alle Spalten aus den Werten der Operation werden einbezogen.

### UPDATE

```sql
UPDATE `posts` SET `title` = ?, `status` = ? WHERE `id` = ?
-- params: ['New Title', 'publish', 42]
```

Identitätsspalten (Primärschlüssel) werden aus der SET-Klausel ausgeschlossen und in der WHERE-Klausel verwendet. Die Werte für die WHERE-Klausel stammen aus dem dekodierten Zeilenschlüssel.

### DELETE

```sql
DELETE FROM `posts` WHERE `id` = ?
-- params: [42]
```

In der WHERE-Klausel erscheinen nur Identitätsspalten.

### Abgesichertes UPDATE

Abgesicherte Aktualisierungen enthalten sowohl Identitätsklauseln als auch Klauseln für die erwartete Live-Zeile.

```sql
UPDATE `posts` SET `title` = ? WHERE `id` = ? AND `status` = ?
-- params: ['New Title', 42, 'draft']
```

Wenn die Zeile nicht mehr den erwarteten `status`-Wert hat, betrifft die Anweisung null Zeilen
und Guarded Apply rollt die Transaktion zurück.

### Abgesichertes INSERT

Abgesicherte Einfügungen verwenden `NOT EXISTS`, um zu verhindern, dass eine Identität eingefügt wird,
die seit der Planerstellung hinzugekommen ist.

```sql
INSERT INTO `posts` (`id`, `title`) SELECT ?, ? WHERE NOT EXISTS (
  SELECT 1 FROM `posts` WHERE `id` = ?
)
-- params: [42, 'New Post', 42]
```

## NULL-Behandlung

NULL-Werte werden als PHP `null` im Parameter-Array übergeben. Der Verbindungsadapter übernimmt die Bindung:

```php
$statements = SqlGenerator::generate($result);
// params might contain: [42, null, 'draft']
// The adapter binds null as SQL NULL
```

In der Probelaufausgabe werden NULL-Werte als Literalzeichenfolge `NULL` ohne Anführungszeichen dargestellt.

## Erkennung von Identitätsspalten

`SqlGenerator` ermittelt anhand des `TableSnapshot::identityColumns` im Basis-Snapshot, welche Spalten die Zeilenidentität bilden. Diese Spalten werden in WHERE-Klauseln von UPDATE- und DELETE-Anweisungen verwendet und bei UPDATEs aus SET-Klauseln ausgeschlossen.

Wenn kein Basis-Snapshot verfügbar ist, wird ersatzweise die erste Spalte im Werte-Array verwendet. Ist das Array leer, wird `id` angenommen.

## Treiber übergeben

Um die Maskierung von Bezeichnern zu steuern, übergib eine Instanz von `Driver`:

```php
use Merql\Driver\SqliteDriver;

$statements = SqlGenerator::generate(
    $result,
    driver: new SqliteDriver(),
);
// Uses double-quote quoting: "posts", "title"
```
