
# SQL Mirror

`SqlMirror` pusht storh-collecties naar tabellen in SQLite of MySQL, zodat u joins, sortering tussen tabellen, zoeken in subreeksen en rapportages kunt uitvoeren in SQL, terwijl de bestanden de canonieke bron blijven. Zie het als een secundaire index die toevallig een database is: de mirror is afgeleid, wegwerpbaar en op elk moment opnieuw op te bouwen. `ext-pdo` met de driver `sqlite` of `mysql` is alleen nodig wanneer u de mirror gebruikt; het kernpakket blijft vrij van afhankelijkheden.

## Instellen

```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 kunnen worden gespiegeld: `DocStore`-collecties en `SegmentedLog`-streams (alleen actieve records worden gepusht). Een `Schema` meegeven is optioneel en staat los van de store: gedeclareerde velden van het type `string`/`int`/`float`/`bool` worden getypeerde, geïndexeerde kolommen; al het andere blijft doorzoekbaar in de kolom `data` JSON. Velden van het type `mixed` kunnen niet worden gemarkeerd voor indexering.

## Verbindingen

De constructor accepteert een `PDO`-handle (driver `sqlite` of `mysql`, automatisch gedetecteerd), een `mysqli`-handle of elke implementatie van `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_');
```

Het mysqli-pad werkt met beide foutmeldingsmodi van mysqli, dus een bestaande handle van een hosttoepassing (bijvoorbeeld `$wpdb->dbh` van WordPress) kan zo worden hergebruikt. Het gedrag is voor alle drivers hetzelfde; mislukte pushbewerkingen rollen de transactie van de collectie bij elke driver terug.

Elke tabel krijgt `id` (primaire sleutel), `hash` (stuurt incrementele pushes aan), de getypeerde kolommen en `data` (de volledige record-JSON).

## Synchroniseren

```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()` vergelijkt hashes van records met de mirror en schrijft elke collectie in één transactie, zodat een gecrashte of mislukte push nooit een gedeeltelijk toegepaste batch achterlaat. Voor ongewijzigde records is één `SELECT` nodig, plus een scan van de records. Een periodieke `push()` (bijvoorbeeld vanuit een cron-tick) houdt de mirror synchroon; roep `flush()` aan met de ID's die u tijdens een request hebt geschreven wanneer lezers de zojuist geschreven gegevens meteen in de mirror moeten kunnen lezen.

## Query's uitvoeren

Voer query's op de mirror uit met gewone SQL. `table()` retourneert de naam van de gespiegelde tabel:

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

## Terugzetten naar bestanden

`pull()` leest rijen uit de mirror en schrijft ze in ID-volgorde naar de geregistreerde store. Dit ondersteunt twee scenario's:

- Herstellen: de bestanden zijn weg, maar de mirror is er nog. Registreer een lege store onder dezelfde collectienaam en haal de gegevens terug.
- Gegevens initialiseren: rijen die met externe tools in de mirrortabel zijn geladen, worden storh-records. De rijen moeten geldige UUIDv7-ID's en een object in de kolom `data` bevatten; getypeerde kolommen worden bij het terugzetten genegeerd, omdat `data` de volledige record is.

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

Teruggezette schrijfbewerkingen lopen via de normale storeverwerking. Daardoor zijn schemavalidatie en unieke indexen van toepassing en wordt elk record duurzaam opgeslagen. `pull()` verwijdert nooit lokale records en slaat records over waarvan de gegevens al overeenkomen met de rij in de mirror. Voer na het initialiseren met handmatig ingevoegde rijen één keer `push()` uit: die herschrijft de rijen in canonieke vorm, zodat de hashes overeenkomen.

## Afspraken en beperkingen

- Bestanden zijn canoniek. De mirror is alleen-lezen uitvoer; alles wat er handmatig in wordt geschreven, wordt overschreven of verwijderd door de volgende `push()`.
- Unieke kolommen in de mirror zijn een vangnet, geen controle tijdens het schrijven: een duplicaat dat in de bestanden was toegestaan, leidt tot een mislukte push die volledig wordt teruggedraaid. Gebruik unieke indexen in storh om duplicaten tijdens het schrijven te voorkomen.
- De mirror loopt maximaal één pushinterval achter op de bestanden, behalve voor ID's die u expliciet `flush()`.
- Mirroring voegt zelf geen transacties toe aan storh. Atomair schrijven van meerdere records blijft een taak voor een echte database die als bronsysteem dient.
- Schemawijzigingen worden toegepast door de mirror te verwijderen en opnieuw te installeren: `uninstall()`, `install()`, `push()`.
