
# Зеркало 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()`.
