
# SQL Mirror

`SqlMirror` przesyła kolekcje storh do tabel SQLite lub MySQL, aby umożliwić wykonywanie złączeń, sortowanie między tabelami, wyszukiwanie fragmentów tekstu i tworzenie raportów w SQL, przy zachowaniu kanonicznej postaci plików. To dodatkowy indeks, który jest jednocześnie bazą danych: mirror jest pochodny, można go usunąć i w każdej chwili odbudować.
`ext-pdo` sterownik `sqlite` lub `mysql` jest wymagany tylko wtedy, gdy z niego korzystasz; pakiet podstawowy nie ma zależności.

## Konfiguracja

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

Oba silniki mogą być synchronizowane z mirrorem: kolekcje `DocStore` i strumienie `SegmentedLog` (przesyłane są tylko aktywne rekordy). Przekazanie `Schema` jest opcjonalne i niezależne od magazynu: zadeklarowane pola `string`/`int`/`float`/`bool` stają się typowanymi, indeksowanymi kolumnami; pozostałe dane można odpytywać w kolumnie `data` JSON. Pola typu `mixed` nie mogą być oznaczone do indeksowania.

## Połączenia

Konstruktor przyjmuje uchwyt `PDO` (automatycznie wykrywany sterownik `sqlite` lub `mysql`), uchwyt `mysqli` albo dowolną implementację `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_');
```

Ścieżka mysqli działa w obu trybach raportowania błędów mysqli, więc można bez zmian użyć istniejącego uchwytu z aplikacji hosta (na przykład WordPress — `$wpdb->dbh`). Zachowanie jest identyczne dla wszystkich sterowników; błędy przesyłania wycofują transakcję kolekcji w każdym z nich.

Każda tabela zawiera `id` (klucz główny), `hash` (obsługuje przyrostowe przesyłanie), typowane kolumny i `data` (pełny rekord JSON).

## Synchronizacja

```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()` porównuje skróty rekordów z mirrorem i zapisuje każdą kolekcję w ramach jednej transakcji, więc awaria lub nieudane przesyłanie nigdy nie pozostawia częściowo zastosowanej partii. Niezmienione rekordy wymagają jednego `SELECT` i przeglądu rekordów. Okresowe `push()` (na przykład przy cron) pozwala utrzymać aktualny mirror; wywołaj `flush()` z identyfikatorami zapisanymi podczas żądania, jeśli czytelnicy mają widzieć swoje zapisy w mirrorze.

## Wykonywanie zapytań

Wykonuj zapytania do mirrora za pomocą zwykłego SQL. `table()` zwraca nazwę tabeli w mirrorze:

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

## Przywracanie danych do plików

`pull()` odczytuje wiersze mirrora i zapisuje je w zarejestrowanym magazynie, w kolejności identyfikatorów. Obsługuje dwa przypadki:

- Przywracanie: pliki zniknęły, ale mirror przetrwał. Zarejestruj nowy magazyn pod tą samą nazwą kolekcji i pobierz dane.
- Inicjowanie danych: wiersze załadowane do tabeli mirrora przez zewnętrzne narzędzia stają się rekordami storh. Wiersze muszą zawierać prawidłowe identyfikatory UUIDv7 i obiekt w kolumnie `data`; kolumny typowane są ignorowane przy pobieraniu, ponieważ `data` zawiera cały rekord.

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

Zapisane dane przechodzą przez standardowe mechanizmy magazynu, więc obowiązują walidacja schematu i unikalne indeksy, a każdy rekord jest trwale zapisywany. `pull()` nigdy nie usuwa lokalnych rekordów, a rekordy, których dane już odpowiadają wierszowi mirrora, są pomijane. Po ręcznym dodaniu wierszy uruchom raz `push()`: przepisze je do postaci kanonicznej, dzięki czemu ich skróty będą zgodne.

## Zasady i ograniczenia

- Pliki są kanoniczne. Mirror zawiera dane tylko do odczytu; wszystko, co zostanie w nim ręcznie zapisane, zostanie nadpisane lub usunięte przy następnym `push()`.
- Unikalne kolumny w mirrorze pełnią funkcję zabezpieczenia, a nie ograniczenia podczas zapisu: duplikat, który był dozwolony w plikach, spowoduje nieudane przesyłanie i pełne wycofanie zmian. Do wymuszania unikalności podczas zapisu używaj unikalnych indeksów storh.
- Mirror może być opóźniony względem plików o maksymalnie jeden okres między przesyłaniami, z wyjątkiem identyfikatorów, które jawnie `flush()`.
- Synchronizacja z mirrorem nie zapewnia transakcji w samym storh. Atomowe zapisy wielu rekordów nadal wymagają prawdziwej bazy danych jako głównego źródła danych.
- Zmiany schematu są stosowane przez usunięcie i ponowną instalację mirrora: `uninstall()`, `install()`, `push()`.
