
# Zrkadlo SQL

`SqlMirror` presúva kolekcie storh do tabuliek SQLite alebo MySQL, aby ste mohli
v SQL spúšťať spájanie tabuliek, zoradenie naprieč tabuľkami, vyhľadávanie podreťazcov a vytvárať reporty, pričom súbory zostávajú v kanonickom formáte. Predstavte si ho ako sekundárny index, ktorý je zároveň databázou: zrkadlo je odvodené, možno ho zahodiť a kedykoľvek znova vytvoriť.
`ext-pdo` s ovládačom `sqlite` alebo `mysql` je potrebné iba pri jeho použití; základný balík zostáva bez závislostí.

## Nastavenie

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

Možno zrkadliť oba moduly: kolekcie `DocStore` aj streamy `SegmentedLog` (presúvajú sa iba živé záznamy). Odovzdanie objektu `Schema` je voliteľné a nezávislé od úložiska: deklarované polia `string`/`int`/`float`/`bool` sa zmenia na typované indexované stĺpce; všetko ostatné zostáva vyhľadateľné v stĺpci `data` JSON. Polia `mixed` nemožno označiť príznakom indexovania.

## Pripojenia

Konštruktor prijíma objekt pripojenia `PDO` (ovládač `sqlite` alebo `mysql`, rozpoznaný automaticky), objekt pripojenia `mysqli` alebo ľubovoľnú implementáciu `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 pri oboch režimoch hlásenia chýb mysqli, takže možno opätovne použiť existujúci objekt pripojenia z hostiteľskej aplikácie, napríklad WordPress `$wpdb->dbh`, bez úprav. Správanie je vo všetkých ovládačoch rovnaké; pri zlyhaní presunu sa transakcia kolekcie v každom z nich vráti späť.

Každá tabuľka obsahuje `id` (primárny kľúč), `hash` (riadi priebežné presuny), typované stĺpce a `data` (úplný záznam JSON).

## Synchronizácia

```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áva kontrolné súčty záznamov so zrkadlom a zapisuje každú kolekciu v rámci jednej transakcie, takže po páde alebo zlyhaní presunu nikdy nezostane dávka spracovaná iba čiastočne. Spracovanie nezmenených záznamov stojí jedno `SELECT` a prechod cez záznamy. Pravidelný `push()` (napríklad pri každom cykle cron) udržiava zrkadlo aktuálne; ak čitatelia potrebujú vidieť vlastné zápisy v zrkadle, zavolajte `flush()` s ID, ktoré ste zapísali počas požiadavky.

## Dotazovanie

Dotazujte zrkadlo pomocou obyčajného SQL. `table()` vráti názov zrkadlenej tabuľky:

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

## Prenos späť do súborov

`pull()` číta riadky zrkadla a v poradí podľa ID ich zapisuje do registrovaného úložiska. Podporuje dva postupy:

- Obnovenie: súbory sú preč, ale zrkadlo zostalo. Zaregistrujte nové úložisko
  s rovnakým názvom kolekcie a načítajte údaje.
- Naplnenie: riadky vložené externými nástrojmi do tabuľky zrkadla sa zmenia na záznamy storh. Riadky musia obsahovať platné identifikátory UUIDv7 a objekt v stĺpci `data`; typované stĺpce sa pri načítaní ignorujú, pretože `data` predstavuje celý záznam.

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

Zápisy načítané zo zrkadla prechádzajú bežným mechanizmom úložiska, takže sa uplatňuje validácia schémy aj jedinečné indexy a každý záznam sa zapisuje trvalo. `pull()` nikdy neodstraňuje lokálne záznamy a záznamy, ktorých údaje sa už zhodujú s riadkom v zrkadle, preskočí. Po vložení riadkov naplnených ručne raz spustite `push()`: prepíše tieto riadky v kanonickom tvare, aby sa ich hashe zosúladili.

## Zmluva a obmedzenia

- Súbory sú kanonické. Zrkadlo je výstup určený len na čítanie; všetko, čo sa doň ručne zapíše, prepíše alebo odstráni nasledujúce `push()`.
- Jedinečné stĺpce v zrkadle slúžia ako poistka, nie ako vynucovanie pri zápise: duplicita povolená súbormi sa prejaví ako neúspešné (a úplne vrátené) odoslanie. Na vynucovanie pri zápise použite jedinečné indexy storh.
- Zrkadlo zaostáva za súbormi až o jeden interval odosielania, s výnimkou identifikátorov, ktoré výslovne `flush()`.
- Zrkadlenie nepridáva transakcie do samotného storh. Atómové zápisy viacerých záznamov zostávajú úlohou pre skutočnú databázu, ktorá slúži ako zdroj pravdy.
- Zmeny schémy sa uplatňujú odstránením a opätovnou inštaláciou zrkadla: `uninstall()`, `install()`, `push()`.
