
# SQL Огледало

`SqlMirror` изпраща колекциите на storh към таблици в SQLite или MySQL, за да можете да изпълнявате съединения, сортиране между таблици, търсене на поднизове и отчети в SQL, докато файловете остават канонични. Представете си го като вторичен индекс, който се оказва база данни: огледалото е производно, може да бъде изхвърлено и може да се изгради отново по всяко време. `ext-pdo` с драйвера `sqlite` или `mysql` е необходим само когато го използвате; основният пакет не зависи от външни пакети.

## Настройка

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

И двете машини могат да се отразяват: колекции `DocStore` и потоци `SegmentedLog` (изпращат се само активните записи). Подаването на `Schema` е незадължително и независимо от хранилището: декларираните полета `string`/`int`/`float`/`bool` стават типизирани, индексирани колони; всичко останало остава достъпно за заявки в колоната `data` JSON. Полетата `mixed` не могат да бъдат маркирани за индексиране.

## Връзки

Конструкторът приема манипулатор `PDO` (драйвер `sqlite` или `mysql`, разпознава се автоматично), манипулатор `mysqli` или която и да е реализация на `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 работи и с двата режима за отчитане на грешки в mysqli, така че съществуващ манипулатор от хост приложението (например `$wpdb->dbh` на WordPress) може да се използва повторно без промени. Поведението е еднакво при всички драйвери; неуспешното изпращане отменя транзакцията на колекцията при всеки от тях.

Всяка таблица получава `id` (първичен ключ), `hash` (управлява постепенното изпращане), типизираните колони и `data` (пълния JSON на записа).

## Синхронизиране

```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()` сравнява хешовете на записите с огледалото и записва всяка колекция в една транзакция, така че срив или неуспешно изпращане никога не оставя частично приложена партида. Непроменените записи изискват едно `SELECT` и обход на записите. Периодично `push()` (например при такт на cron) поддържа огледалото синхронизирано; извикайте `flush()` с идентификаторите, които сте записали по време на заявка, когато читателите трябва да виждат тези промени в огледалото веднага.

## Изпълнение на заявки

Правете заявки към огледалото с обикновен SQL. `table()` връща името на отразената таблица:

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

## Връщане във файловете

`pull()` чете редовете от огледалото и ги записва в регистрираното хранилище, подредени по идентификатор. Поддържа два случая:

- Възстановяване: файловете липсват, но огледалото е запазено. Регистрирайте ново хранилище със същото име на колекцията и извършете изтеглянето.
- Подготовка на начални данни: редовете, заредени в таблицата на огледалото с външни инструменти, стават записи storh. Редовете трябва да съдържат валидни идентификатори UUIDv7 и обект в колоната `data`; типизираните колони се игнорират при изтеглянето, защото `data` е целият запис.

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

Записите, изтеглени обратно, преминават през обичайните механизми на хранилището, така че се прилагат проверката на схемата и уникалните индекси, а всеки запис се съхранява трайно. `pull()` никога не изтрива локални записи и пропуска записите, чиито данни вече съвпадат с реда в огледалото. След добавяне на редове ръчно, изпълнете `push()` веднъж: той ги презаписва в каноничен формат, за да се синхронизират хешовете.

## Правила и ограничения

- Файловете са канонични. Огледалото е изход само за четене; всичко, записано ръчно в него, се презаписва или изтрива при следващото `push()`.
- Уникалните колони в огледалото са допълнителна защита, а не проверка при запис: дубликат, допуснат от файловете, води до неуспешно (и изцяло отменено) изпращане. Използвайте уникалните индекси storh за проверка при запис.
- Огледалото изостава от файловете с до един интервал между изпращанията, с изключение на идентификаторите, които изрично `flush()`.
- Отразяването не добавя транзакции към самото storh. Атомарните записи с няколко записа остават задача за реална база данни като основно хранилище.
- Промените в схемата се прилагат чрез премахване и повторно инсталиране на огледалото: `uninstall()`, `install()`, `push()`.
