
# SQL Mirror

`SqlMirror` transferă colecțiile storh în tabele SQLite sau MySQL, pentru a putea executa join-uri, ordonări între tabele, căutări de subșiruri și raportări în SQL, păstrând în același timp fișierele canonice. Gândiți-vă la aceasta ca la un index secundar care se întâmplă să fie o bază de date: oglinda este derivată, dispensabilă și poate fi reconstruită oricând. `ext-pdo` cu driverul `sqlite` sau `mysql` este necesar doar când folosiți oglinda; pachetul de bază nu are dependențe.

## Configurare

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

Ambele motoare pot fi oglindite: colecțiile `DocStore` și fluxurile `SegmentedLog` (sunt transferate doar înregistrările active). Transmiterea unui `Schema` este opțională și independentă de stocare: câmpurile `string`/`int`/`float`/`bool` declarate devin coloane tipizate și indexate; toate celelalte rămân interogabile în coloana `data` JSON. Câmpurile `mixed` nu pot fi marcate pentru indexare.

## Conexiuni

Constructorul acceptă un handle `PDO` (driver `sqlite` sau `mysql`, detectat automat), un handle `mysqli` sau orice implementare `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_');
```

Calea mysqli funcționează cu oricare dintre modurile de raportare a erorilor mysqli, astfel că un handle existent dintr-o aplicație gazdă (de exemplu, cel al WordPress pentru `$wpdb->dbh`) poate fi reutilizat ca atare. Comportamentul este identic pentru toate driverele; eșecurile la transfer anulează tranzacția colecției în toate cazurile.

Fiecare tabel primește `id` (cheie primară), `hash` (pentru transferurile incrementale), coloanele tipizate și `data` (înregistrarea completă în JSON).

## Sincronizare

```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()` compară hash-urile înregistrărilor cu oglinda și scrie fiecare colecție într-o singură tranzacție, astfel că un transfer întrerupt sau eșuat nu lasă niciodată un lot aplicat parțial. Înregistrările neschimbate necesită un singur `SELECT` plus parcurgerea înregistrărilor. Un `push()` periodic (de exemplu, la un eveniment cron) menține oglinda sincronizată; apelați `flush()` cu id-urile scrise în timpul unei cereri atunci când cititorii au nevoie să vadă în oglindă propriile scrieri.

## Interogare

Interogați oglinda folosind instrucțiuni simple SQL. `table()` returnează numele tabelului oglindit:

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

## Transferul înapoi în fișiere

`pull()` citește rândurile din oglindă și le scrie în stocarea înregistrată, în ordinea id-urilor. Acoperă două scenarii:

- Restaurare: fișierele au dispărut, dar oglinda este încă disponibilă. Înregistrați o stocare nouă cu același nume de colecție și efectuați transferul.
- Inițializare: rândurile încărcate în tabelul oglinzii cu instrumente externe devin înregistrări storh. Rândurile trebuie să aibă id-uri UUIDv7 valide și un obiect în coloana `data`; coloanele tipizate sunt ignorate la transfer, deoarece `data` conține înregistrarea completă.

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

Scrierile transferate trec prin mecanismele normale ale stocării, astfel că se aplică validarea schemei și indexurile unice, iar fiecare înregistrare este scrisă durabil. `pull()` nu șterge niciodată înregistrările locale, iar înregistrările ale căror date corespund deja rândului din oglindă sunt omise. După inițializarea cu rânduri inserate manual, rulați o dată `push()`: acesta rescrie rândurile în formă canonică, astfel încât hash-urile lor să se reconcilieze.

## Contract și limite

- Fișierele sunt canonice. Oglinda este un rezultat doar pentru citire; orice date scrise manual în ea sunt suprascrise sau șterse la următorul `push()`.
- Coloanele unice din oglindă sunt o măsură de siguranță, nu o impunere la momentul scrierii: o dublură acceptată de fișiere va provoca eșecul transferului (și anularea completă a acestuia). Folosiți indexuri unice storh pentru impunerea unicității la scriere.
- Oglinda rămâne în urma fișierelor cu până la un interval de transfer, exceptând id-urile pe care le sincronizați explicit prin `flush()`.
- Oglindirea nu adaugă tranzacții în storh. Scrierile atomice pe mai multe înregistrări rămân responsabilitatea unei baze de date reale, folosită ca sistem de referință.
- Modificările schemei se aplică prin ștergerea și reinstalarea oglinzii: `uninstall()`, `install()`, `push()`.
