
# SQL Mirror

`SqlMirror` überträgt storh-Sammlungen in SQLite- oder MySQL-Tabellen, damit du Joins, tabellenübergreifende Sortierung, Teilstringsuche und Berichte in SQL ausführen kannst, während die Dateien maßgeblich bleiben. Betrachte es als sekundären Index in Form einer Datenbank: Der Mirror ist abgeleitet, entbehrlich und jederzeit neu erstellbar. `ext-pdo` mit dem Treiber `sqlite` oder `mysql` ist nur erforderlich, wenn du ihn verwendest; das Kernpaket bleibt frei von Abhängigkeiten.

## Einrichtung

```php
use Storh\DocStore;
use Storh\Schema;
use Storh\SqlMirror;

$schema = Schema::collection('pages')
    ->string('slug')->unique()
    ->string('kind')->index()
    ->int('publishedAt')->range();

$pages  = new DocStore($root, 'pages', schema: $schema);
$events = new Storh\SegmentedLog($root, 'events');

$mirror = new SqlMirror(new PDO('sqlite:' . $root . '/mirror.db'), 'app_');
$mirror->collection($pages, 'pages', $schema);
$mirror->collection($events, 'events');

$mirror->install();
$mirror->push();
```

Beide Engines können gespiegelt werden: Sammlungen von `DocStore` und Streams von `SegmentedLog` (übertragen werden nur aktive Datensätze). Die Übergabe eines `Schema` ist optional und unabhängig vom Store: Deklarierte Felder `string`/`int`/`float`/`bool` werden zu typisierten, indizierten Spalten; alles andere bleibt in der `data`-Spalte JSON abfragbar. Felder vom Typ `mixed` können nicht zur Indizierung markiert werden.

## Verbindungen

Der Konstruktor akzeptiert ein `PDO`-Handle (Treiber `sqlite` oder `mysql`, automatisch erkannt), ein `mysqli`-Handle oder eine beliebige Implementierung von `SqlMirrorConnection`:

```php
$mirror = new SqlMirror(new PDO('sqlite:' . $root . '/mirror.db'));
$mirror = new SqlMirror(new PDO('mysql:host=127.0.0.1;dbname=app;charset=utf8mb4', $user, $password));
$mirror = new SqlMirror(new mysqli('127.0.0.1', $user, $password, 'app'), 'wp_storh_');
```

Der mysqli-Pfad funktioniert mit beiden Modi für die Fehlerberichterstattung von mysqli. Daher kann ein vorhandenes Handle einer Host-Anwendung (zum Beispiel WordPresss `$wpdb->dbh`) unverändert wiederverwendet werden. Das Verhalten ist bei allen Treibern gleich; schlägt eine Übertragung fehl, wird die Transaktion der Sammlung bei allen zurückgerollt.

Jede Tabelle erhält `id` (Primärschlüssel), `hash` (steuert inkrementelle Übertragungen), die typisierten Spalten und `data` (den vollständigen Datensatz JSON).

## Synchronisierung

```php
$mirror->push();                    // reconcile: insert, update, delete as needed
$mirror->push('pages');             // reconcile a single collection
$mirror->flush('pages', $ids);      // push or remove just these ids
$mirror->pull();                    // write mirror rows back into the store
$mirror->verify();                  // report missing, stale, and orphaned rows
$mirror->rebuild();                 // clear all rows and push from scratch
$mirror->uninstall();               // drop the mirror tables
```

`push()` vergleicht Datensatz-Hashes mit dem Mirror und schreibt jede Sammlung in einer Transaktion. Ein abgebrochener oder fehlgeschlagener Übertragungsvorgang hinterlässt daher nie einen teilweise angewendeten Batch. Für unveränderte Datensätze genügt ein `SELECT` plus ein Durchlauf durch die Datensätze. Ein regelmäßiges `push()` (zum Beispiel bei einem cron-Takt) hält den Mirror synchron; rufe `flush()` mit den IDs auf, die du während einer Anfrage geschrieben hast, wenn Leser ihre eigenen Änderungen im Mirror sofort sehen müssen.

## Abfragen

Frage den Mirror mit einfachem SQL ab. `table()` gibt den Namen der gespiegelten Tabelle zurück:

```php
$pdo->query(
    'SELECT p.slug, COUNT(*) AS sales
     FROM ' . $mirror->table('purchases') . ' o
     INNER JOIN ' . $mirror->table('products') . ' p ON p.id = o.product_id
     GROUP BY p.slug
     ORDER BY sales DESC'
);
```

## Zurück in Dateien übertragen

`pull()` liest die Zeilen des Mirrors und schreibt sie sortiert nach ID in den registrierten Store. Das deckt zwei Anwendungsfälle ab:

- Wiederherstellung: Die Dateien sind verloren, aber der Mirror ist noch vorhanden. Registriere einen leeren Store unter demselben Sammlungsnamen und übertrage die Daten zurück.
- Befüllen: Zeilen, die externe Tools in die Mirror-Tabelle laden, werden zu storh-Datensätzen. Die Zeilen müssen gültige UUIDv7-IDs und ein Objekt in der Spalte `data` enthalten; beim Zurückübertragen werden typisierte Spalten ignoriert, da `data` den vollständigen Datensatz enthält.

```php
$restored = new Storh\DocStore($root, 'pages');
$mirror   = new SqlMirror($pdo);
$mirror->collection($restored, 'pages');
$mirror->pull();          // ['written' => n, 'unchanged' => m]
```

Zurückübertragene Schreibvorgänge durchlaufen die normale Store-Verarbeitung. Daher gelten Schemaüberprüfung und eindeutige Indizes, und jeder Datensatz wird dauerhaft gespeichert. `pull()` löscht niemals lokale Datensätze und überspringt Datensätze, deren Daten bereits mit der Mirror-Zeile übereinstimmen. Führe nach dem Befüllen mit manuell eingefügten Zeilen einmal `push()` aus: Dadurch werden sie in kanonischer Form neu geschrieben und ihre Hashes stimmen wieder überein.

## Vereinbarungen und Grenzen

- Dateien sind maßgeblich. Der Mirror ist eine schreibgeschützte Ausgabe; manuelle Änderungen daran werden beim nächsten `push()` überschrieben oder gelöscht.
- Eindeutige Spalten im Mirror dienen als zusätzliche Absicherung, erzwingen Eindeutigkeit aber nicht beim Schreiben: Ein Duplikat, das die Dateien zugelassen haben, führt zu einer fehlgeschlagenen und vollständig zurückgerollten Übertragung. Verwende eindeutige storh-Indizes, um Eindeutigkeit beim Schreiben durchzusetzen.
- Der Mirror kann den Dateien um bis zu ein Übertragungsintervall hinterherhinken, außer bei IDs, die du ausdrücklich mit `flush()` synchronisierst.
- Das Spiegeln fügt storh selbst keine Transaktionen hinzu. Atomare Schreibvorgänge über mehrere Datensätze hinweg bleiben Aufgabe einer echten Datenbank als maßgeblichem System.
- Schemaänderungen werden angewendet, indem der Mirror gelöscht und neu eingerichtet wird: `uninstall()`, `install()`, `push()`.
