
# Дзеркало 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, тож можна повторно використати наявний дескриптор із хост-застосунку (наприклад, WordPress: `$wpdb->dbh`) без змін. Поведінка однакова в усіх драйверах; у разі помилки передавання транзакція колекції відкочується в кожному з них.

Кожна таблиця містить `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()`.
