
# SQL Mirror

`SqlMirror` přenáší kolekce storh do tabulek SQLite nebo MySQL, abyste mohli v SQL provádět spojování, řazení napříč tabulkami, vyhledávání podřetězců a vytvářet přehledy, zatímco soubory zůstávají v kanonickém formátu. Představte si jej jako sekundární index, který je zároveň databází: zrcadlo je odvozené, lze je zahodit a kdykoli znovu sestavit. `ext-pdo` s ovladačem `sqlite` nebo `mysql` je potřeba pouze při jeho použití; hlavní balíček zůstává bez závislostí.

## Nastavení

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

Zrcadlit lze obě úložiště: kolekce `DocStore` a streamy `SegmentedLog` (přenášejí se pouze aktivní záznamy). Předání objektu `Schema` je volitelné a nezávislé na úložišti: deklarovaná pole `string`/`int`/`float`/`bool` se stanou typovanými a indexovanými sloupci; vše ostatní zůstane dotazovatelné ve sloupci `data` JSON. Pole `mixed` nelze označit k indexování.

## Připojení

Konstruktor přijímá objekt `PDO` (ovladač `sqlite` nebo `mysql`, rozpoznaný automaticky), objekt `mysqli` nebo libovolnou implementaci `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_');
```

Cesta mysqli funguje s oběma režimy hlášení chyb mysqli, takže lze beze změn znovu použít existující objekt z hostitelské aplikace (například `$wpdb->dbh` od WordPress). Chování je u všech ovladačů stejné; při selhání přenosu se transakce kolekce u všech vrátí zpět.

Každá tabulka obsahuje `id` (primární klíč), `hash` (slouží k přírůstkovým přenosům), typované sloupce a `data` (celý záznam ve formátu JSON).

## Synchronizace

```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()` porovnává hash hodnoty záznamů se zrcadlem a zapisuje každou kolekci v rámci jedné transakce, takže pád nebo selhání přenosu nikdy nezanechá dávku jen částečně použitou. Nezměněné záznamy vyžadují jeden `SELECT` a procházení záznamů. Pravidelný `push()` (například při každém taktu cron) udržuje zrcadlo synchronizované; když čtenáři potřebují, aby se jejich zápisy projevily i při čtení ze zrcadla, zavolejte `flush()` s ID záznamů zapsaných během požadavku.

## Dotazování

V zrcadle lze dotazovat pomocí běžného SQL. `table()` vrací název zrcadlené tabulky:

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

## Načtení zpět do souborů

`pull()` čte řádky zrcadla a zapisuje je do registrovaného úložiště v pořadí podle ID. Podporuje dva způsoby použití:

- Obnovení: soubory jsou pryč, ale zrcadlo zůstalo. Zaregistrujte nové úložiště pod stejným názvem kolekce a načtěte data zpět.
- Naplnění: z řádků načtených do tabulky zrcadla externími nástroji se stanou záznamy storh. Řádky musí obsahovat platná ID UUIDv7 a objekt ve sloupci `data`; typované sloupce se při zpětném načítání ignorují, protože `data` obsahuje celý záznam.

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

Zpětně načtené zápisy procházejí běžnými mechanismy úložiště, takže se použije ověření schématu i jedinečné indexy a každý záznam se zapíše trvale. `pull()` nikdy nemaže místní záznamy a záznamy, jejichž data již odpovídají řádku zrcadla, přeskočí. Po ručním vložení řádků při naplňování spusťte jednou `push()`: řádky se přepíší do kanonické podoby, aby se jejich hash hodnoty sesouhlasily.

## Zásady a limity

- Soubory jsou v kanonickém formátu. Zrcadlo poskytuje výstup pouze pro čtení; vše, co do něj ručně zapíšete, bude při příštím `push()` přepsáno nebo odstraněno.
- Jedinečné sloupce v zrcadle slouží jako pojistka, nevynucují jedinečnost při zápisu: duplicita povolená v souborech se projeví jako neúspěšný a zcela vrácený přenos. K vynucování jedinečnosti při zápisu použijte jedinečné indexy storh.
- Zrcadlo může za soubory zaostávat až o jeden interval přenosu, s výjimkou ID, jejichž přenos výslovně `flush()`.
- Zrcadlení nepřidává transakce do samotného storh. Atomické zápisy zahrnující více záznamů nadále vyžadují skutečnou databázi jako hlavní zdroj záznamů.
- Změny schématu se použijí odstraněním a opětovnou instalací zrcadla: `uninstall()`, `install()`, `push()`.
