
# SQL Mirror

`SqlMirror` invia le raccolte storh nelle tabelle SQLite o MySQL, così potete eseguire join, ordinamenti tra tabelle, ricerche di sottostringhe e report in SQL, mantenendo canonici i file. È come un indice secondario che è anche un database: il mirror è derivato, eliminabile e ricostruibile in qualsiasi momento. `ext-pdo` con il driver `sqlite` o `mysql` è necessario solo quando lo usate; il pacchetto principale non ha dipendenze.

## Configurazione

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

È possibile creare mirror per entrambi i motori: raccolte `DocStore` e flussi `SegmentedLog` (vengono inviati solo i record attivi). Passare un `Schema` è facoltativo e indipendente dallo store: i campi `string`/`int`/`float`/`bool` dichiarati diventano colonne tipizzate e indicizzate; tutto il resto rimane interrogabile nella colonna `data` JSON. I campi `mixed` non possono essere contrassegnati per l'indicizzazione.

## Connessioni

Il costruttore accetta un handle `PDO` (driver `sqlite` o `mysql`, rilevato automaticamente), un handle `mysqli` o qualsiasi implementazione `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_');
```

Il percorso mysqli funziona con entrambe le modalità di segnalazione degli errori di mysqli, quindi è possibile riutilizzare così com'è un handle esistente dell'applicazione host (per esempio WordPress di `$wpdb->dbh`). Il comportamento è identico con tutti i driver; gli errori di push annullano la transazione della raccolta in tutti i casi.

Ogni tabella include `id` (chiave primaria), `hash` (usato per i push incrementali), le colonne tipizzate e `data` (l'JSON completo del record).

## Sincronizzazione

```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()` confronta gli hash dei record con il mirror e scrive ogni raccolta in un'unica transazione, così un push interrotto o non riuscito non lascia mai un batch applicato solo in parte. I record invariati richiedono una sola `SELECT` oltre alla scansione dei record. Un `push()` periodico (per esempio a ogni ciclo di cron) mantiene il mirror aggiornato; chiamate `flush()` con gli id scritti durante una richiesta quando chi legge deve vedere nel mirror le proprie scritture.

## Query

Interrogate il mirror con SQL semplice. `table()` restituisce il nome della tabella sottoposta a mirror:

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

## Ripristino nei file

`pull()` legge le righe del mirror e le scrive nello store registrato, in ordine di id. Copre due flussi:

- Ripristino: i file sono scomparsi ma il mirror è ancora disponibile. Registrate un nuovo store con lo stesso nome di raccolta e richiamate il pull.
- Inizializzazione: le righe caricate nella tabella mirror da strumenti esterni diventano record storh. Le righe devono contenere id UUIDv7 validi e un oggetto nella colonna `data`; al pull le colonne tipizzate vengono ignorate perché `data` è il record completo.

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

Le scritture recuperate passano attraverso i normali meccanismi dello store, quindi si applicano la convalida dello schema e gli indici univoci, e ogni record viene scritto in modo persistente. `pull()` non elimina mai i record locali e salta quelli i cui dati corrispondono già alla riga del mirror. Dopo aver inizializzato le righe inserite manualmente, eseguite `push()` una volta: le riscrive nel formato canonico affinché i rispettivi hash siano coerenti.

## Contratto e limiti

- I file sono canonici. Il mirror è un output di sola lettura; tutto ciò che vi viene scritto manualmente viene sovrascritto o eliminato al successivo `push()`.
- Le colonne univoche nel mirror sono una protezione aggiuntiva, non un controllo in fase di scrittura: un duplicato consentito dai file genera un push non riuscito (e annullato completamente). Usate indici univoci storh per i controlli in fase di scrittura.
- Il mirror può essere in ritardo rispetto ai file fino a un intervallo di push, tranne per gli id che `flush()` esplicitamente.
- Il mirroring non aggiunge transazioni a storh. Le scritture atomiche su più record richiedono un vero database come sistema di registrazione.
- Le modifiche allo schema vengono applicate eliminando e reinstallando il mirror: `uninstall()`, `install()`, `push()`.
