
# SQL-spegel

`SqlMirror` skickar storh-samlingar till tabeller i SQLite eller MySQL så att du kan
köra kopplingar, sortering över flera tabeller, delsträngssökning och rapportering i SQL medan
filerna förblir kanoniska. Se det som ett sekundärt index som råkar vara
en databas: spegeln härleds, kan kastas bort och kan byggas om när som helst.
`ext-pdo` med `sqlite`- eller `mysql`-drivrutinen krävs bara när du använder
funktionen; kärnpaketet är fortfarande fritt från beroenden.

## Konfiguration

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

Båda motorerna kan speglas: `DocStore`-samlingar och `SegmentedLog`-strömmar (endast aktiva poster skickas). Det är valfritt att skicka en `Schema` och oberoende av lagret: deklarerade fält av typen `string`/`int`/`float`/`bool` blir typade, indexerade kolumner; allt annat förblir sökbart i kolumnen `data`JSON. Fält av typen `mixed` kan inte flaggas för indexering.

## Anslutningar

Konstruktorn accepterar ett `PDO`-handtag (drivrutinen `sqlite` eller `mysql` identifieras automatiskt), ett `mysqli`-handtag eller valfri implementation av `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_');
```

Sökvägen för mysqli fungerar med båda lägena för felrapportering i mysqli, så ett befintligt handtag från ett värdprogram (till exempel WordPresss `$wpdb->dbh`) kan återanvändas som det är. Beteendet är identiskt mellan drivrutinerna; misslyckade skickningar återställer samlingens transaktion i alla drivrutiner.

Varje tabell får `id` (primärnyckel), `hash` (driver inkrementella skickningar), de typade kolumnerna och `data` (hela postens 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()` jämför hashvärden för posterna med spegeln och skriver varje samling i
en transaktion, så en kraschad eller misslyckad skickning lämnar aldrig en delvis tillämpad
batch. Oförändrade poster kostar en `SELECT` plus en genomgång av posterna. En återkommande
`push()` (till exempel från ett cron-tick) håller spegeln uppdaterad; anropa
`flush()` med id:na du skrev under en begäran när läsare behöver
kunna läsa sina egna skrivningar från spegeln.

## Frågor

Fråga spegeln med vanlig SQL. `table()` returnerar namnet på den speglade tabellen:

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

## Hämta tillbaka till filer

`pull()` läser spegelrader och skriver dem till det registrerade lagret i id-ordning. Den stöder två arbetsflöden:

- Återställning: filerna är borta, men spegeln finns kvar. Registrera ett nytt lager under samma samlingsnamn och hämta posterna.
- Förifyllning: rader som externa verktyg har läst in i spegeltabellen blir storh-poster. Raderna måste innehålla giltiga id:n av typen UUIDv7 och ett objekt i kolumnen `data`; typade kolumner ignoreras vid hämtning eftersom `data` är hela posten.

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

Hämtade skrivningar går genom lagrets vanliga mekanismer, så schemavalidering och unika index tillämpas och varje post skrivs beständigt. `pull()` tar aldrig bort lokala poster, och poster vars data redan matchar spegelraden hoppas över. När du har förifyllt med manuellt infogade rader kör du `push()` en gång: det skriver om raderna i kanonisk form så att deras hashvärden stämmer överens.

## Avtal och gränser

- Filerna är kanoniska. Spegeln är skrivskyddade utdata; allt som skrivs dit manuellt skrivs över eller tas bort vid nästa `push()`.
- Unika kolumner i spegeln fungerar som skyddsnät, inte som kontroll vid skrivning: en dubblett som filerna tillät leder till en misslyckad skickning som återställs helt. Använd storh unika index för kontroll vid skrivning.
- Spegeln kan ligga upp till ett skickningsintervall efter filerna, förutom för id:n som du uttryckligen `flush()`.
- Spegling lägger inte till transaktioner i själva storh. Atomära skrivningar över flera poster kräver fortfarande en riktig databas som källsystem.
- Schemaändringar tillämpas genom att spegeln tas bort och installeras om:
  `uninstall()`, `install()`, `push()`.
