
# SQL-spejl

`SqlMirror` flytter storh-samlinger til SQLite- eller MySQL-tabeller, så du kan
køre joins, sortere på tværs af tabeller, søge efter delstrenge og lave rapporter i SQL, mens
filerne forbliver kanoniske. Tænk på det som et sekundært indeks, der tilfældigvis er en database: spejlet er afledt, kan kasseres og kan genopbygges når som helst.
`ext-pdo` med driveren `sqlite` eller `mysql` kræves kun, når du bruger
funktionen; kernepakken forbliver uden afhængigheder.

## Opsætning

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

Begge motorer kan spejles: `DocStore`-samlinger og `SegmentedLog`-streams (kun aktive poster flyttes). Det er valgfrit at angive en `Schema` og
uafhængigt af lageret: erklærede felter af typen `string`/`int`/`float`/`bool` bliver typede, indekserede kolonner; alt andet kan fortsat forespørges i `data`-kolonnen JSON. Felter af typen `mixed` kan ikke markeres til indeksering.

## Forbindelser

Konstruktøren accepterer et `PDO`-håndtag (driveren `sqlite` eller `mysql`, registreres
automatisk), et `mysqli`-håndtag eller enhver implementering af `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_');
```

mysqli-stien fungerer med begge mysqli-fejlrapporteringsindstillinger, så et eksisterende
håndtag fra et værtsprogram (for eksempel WordPresss `$wpdb->dbh`) kan
genbruges direkte. Adfærden er identisk på tværs af drivere; fejl ved overførsel ruller
samlingens transaktion tilbage på dem alle.

Hver tabel får `id` (primærnøgle), `hash` (styrer trinvise overførsler), de
typede kolonner og `data` (hele posten i JSON).

## Synkronisering

```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()` sammenligner posternes hashes med spejlet og skriver hver samling i
én transaktion, så en afbrudt eller mislykket overførsel aldrig efterlader en delvist anvendt
batch. Uændrede poster koster én `SELECT` plus en gennemgang af posterne. En periodisk
`push()` (for eksempel fra et cron-interval) holder spejlet synkroniseret; kald
`flush()` med id'erne, du skrev under en forespørgsel, når læsere skal kunne
læse deres egne ændringer fra spejlet.

## Forespørgsler

Lav forespørgsler mod spejlet med almindelig SQL. `table()` returnerer det spejlede tabelnavn:

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

## Tilbageførsel til filer

`pull()` læser rækker fra spejlet og skriver dem til det registrerede lager i id-
rækkefølge. Det understøtter to forløb:

- Gendannelse: filerne er væk, men spejlet findes stadig. Registrer et nyt lager
  med samme samlingsnavn, og hent rækkerne tilbage.
- Klargøring: rækker, der indlæses i spejltabellen med eksterne værktøjer, bliver til storh-
  poster. Rækkerne skal indeholde gyldige UUIDv7-id'er og et objekt i kolonnen `data`;
  typede kolonner ignoreres ved hentning, fordi `data` er hele
  posten.

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

Skrivninger fra hentede rækker går gennem lagerets normale funktioner, så skemavalidering og
unikke indekser gælder, og hver post skrives holdbart. `pull()` sletter aldrig
lokale poster, og poster, hvis data allerede svarer til spejlrækken, springes over. Efter klargøring af manuelt indsatte rækker skal du køre `push()` én gang: det omskriver
rækkerne i kanonisk form, så deres hashes stemmer overens.

## Aftale og begrænsninger

- Filerne er kanoniske. Spejlet er skrivebeskyttet output; alt, der skrives til det
  manuelt, overskrives eller slettes ved næste `push()`.
- Unikke kolonner i spejlet fungerer som et sikkerhedsnet, ikke som håndhævelse ved skrivning: en
  dublet, som filerne tillod, giver en mislykket (og fuldt tilbagerullet)
  overførsel. Brug unikke storh-indekser for at håndhæve unikhed ved skrivning.
- Spejlet kan være op til ét overførselsinterval bag filerne, bortset fra de id'er, du
  udtrykkeligt `flush()`.
- Spejling tilføjer ikke transaktioner til storh. Atomiske skrivninger på tværs af flere poster
  kræver fortsat en rigtig database som autoritativt datasæt.
- Skemaændringer anvendes ved at slette og geninstallere spejlet:
  `uninstall()`, `install()`, `push()`.
