
# SQL zrcalo

`SqlMirror` prenese zbirke storh v tabele SQLite ali MySQL, da lahko v SQL izvajate združevanja, razvrščanje med tabelami, iskanje podnizov in pripravo poročil, datoteke pa ostanejo kanonične. Predstavljajte si ga kot sekundarni indeks v obliki podatkovne zbirke: zrcalo je izpeljano, ga lahko zavržete in kadar koli znova zgradite. `ext-pdo` z gonilnikom `sqlite` ali `mysql` potrebujete samo, ko ga uporabljate; osnovni paket ostane brez odvisnosti.

## Nastavitev

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

Zrcalite lahko oba pogona: zbirke `DocStore` in tokove `SegmentedLog` (prenesejo se samo aktivni zapisi). Posredovanje `Schema` je izbirno in neodvisno od shrambe: deklarirana polja `string`/`int`/`float`/`bool` postanejo tipizirani, indeksirani stolpci; vse drugo ostane poizvedljivo v stolpcu `data`JSON. Polj `mixed` ni mogoče označiti za indeksiranje.

## Povezave

Konstruktor sprejme ročico `PDO` (gonilnik `sqlite` ali `mysql`, samodejno zaznan), ročico `mysqli` ali katero koli implementacijo `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_');
```

Pot mysqli deluje v obeh načinih poročanja o napakah mysqli, zato lahko obstoječo ročico iz gostiteljske aplikacije (na primer `$wpdb->dbh` v WordPress) ponovno uporabite brez sprememb. Delovanje je pri vseh gonilnikih enako; neuspešni prenosi pri vseh razveljavijo transakcijo zbirke.

Vsaka tabela dobi `id` (primarni ključ), `hash` (omogoča postopne prenose), tipizirane stolpce in `data` (celoten zapis JSON).

## Sinhronizacija

```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()` primerja zgoščene vrednosti zapisov z zrcalom in zapiše vsako zbirko v eni transakciji, zato ob sesutju ali neuspešnem prenosu nikoli ne ostane delno uveljavljen paket. Nespremenjeni zapisi zahtevajo en `SELECT` in pregled zapisov. Redni `push()` (na primer ob sprožilcu cron) ohranja usklajenost zrcala; pokličite `flush()` z ID-ji, ki ste jih zapisali med zahtevo, kadar bralci potrebujejo takojšen dostop do svojih zapisov v zrcalu.

## Poizvedovanje

Poizvedujte po zrcalu z navadnim SQL. `table()` vrne ime zrcaljene tabele:

```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 nazaj v datoteke

`pull()` prebere vrstice zrcala in jih po ID-ju zapiše v registrirano shrambo. Podpira dva poteka dela:

- Obnovitev: datotek ni več, zrcalo pa je ohranjeno. Registrirajte novo shrambo pod istim imenom zbirke in izvedite prenos.
- Začetno polnjenje: vrstice, ki jih v tabelo zrcala naložijo zunanja orodja, postanejo zapisi storh. Vrstice morajo vsebovati veljavne ID-je UUIDv7 in objekt v stolpcu `data`; tipizirani stolpci se pri prenosu prezrejo, ker je `data` celoten zapis.

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

Zapisi, preneseni nazaj, gredo skozi običajne mehanizme shrambe, zato veljajo preverjanje sheme in enolični indeksi, vsak zapis pa se shrani trajno. `pull()` nikoli ne izbriše lokalnih zapisov, zapisi, katerih podatki se že ujemajo z vrstico zrcala, pa so preskočeni. Po začetnem polnjenju z ročno vstavljenimi vrsticami enkrat zaženite `push()`: te vrstice prepiše v kanonični obliki, da se njihove zgoščene vrednosti uskladijo.

## Pogodba in omejitve

- Kanonične so datoteke. Zrcalo je izhod samo za branje; vse, kar vanj zapišete ročno, bo prepisal ali izbrisal naslednji `push()`.
- Enolični stolpci v zrcalu so varovalka, ne preverjanje ob zapisu: podvojena vrednost, ki so jo datoteke sprejele, se pokaže kot neuspešen prenos, ki se v celoti razveljavi. Za preverjanje enoličnosti ob zapisu uporabite enolične indekse storh.
- Zrcalo za datotekami zaostaja za največ en interval prenosa, razen za ID-je, ki jih izrecno `flush()`.
- Zrcaljenje ne dodaja transakcij v sam storh. Atomski zapisi nad več zapisi ostajajo naloga prave podatkovne zbirke, ki služi kot sistem evidenc.
- Spremembe sheme se uveljavijo tako, da se zrcalo izbriše in znova namesti: `uninstall()`, `install()`, `push()`.
