
# Espejo de SQL

`SqlMirror` envía colecciones de storh a tablas de SQLite o MySQL para que pueda ejecutar uniones, ordenar entre tablas, buscar subcadenas y generar informes en SQL, mientras los archivos siguen siendo la fuente canónica. Piense en ello como un índice secundario que resulta ser una base de datos: el espejo se deriva de los datos, es prescindible y se puede reconstruir en cualquier momento. `ext-pdo` con el controlador `sqlite` o `mysql` solo es necesario cuando lo usa; el paquete principal no tiene dependencias.

## Configuración

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

Se pueden crear espejos de ambos motores: colecciones de `DocStore` y flujos de `SegmentedLog` (solo se envían los registros activos). Pasar un `Schema` es opcional e independiente del almacén: los campos declarados como `string`/`int`/`float`/`bool` se convierten en columnas tipadas e indexadas; todo lo demás sigue siendo consultable en la columna `data` JSON. Los campos `mixed` no se pueden marcar para indexación.

## Conexiones

El constructor acepta un controlador `PDO` (controlador `sqlite` o `mysql`, detectado automáticamente), un controlador `mysqli` o cualquier implementación de `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_');
```

La ruta de mysqli funciona con cualquiera de los modos de notificación de errores de mysqli, por lo que se puede reutilizar tal cual un controlador existente de una aplicación anfitriona (por ejemplo, el de WordPress, `$wpdb->dbh`). El comportamiento es idéntico en todos los controladores; los fallos al enviar datos revierten la transacción de la colección en todos ellos.

Cada tabla recibe `id` (clave primaria), `hash` (permite los envíos incrementales), las columnas tipadas y `data` (el JSON completo del registro).

## Sincronización

```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()` compara los hashes de los registros con el espejo y escribe cada colección en una sola transacción, para que un envío interrumpido o fallido nunca deje un lote aplicado parcialmente. Los registros sin cambios requieren una `SELECT` y un recorrido de registros. Una `push()` periódica (por ejemplo, desde un ciclo de cron) mantiene el espejo sincronizado; llame a `flush()` con los ids escritos durante una solicitud cuando los lectores necesiten ver sus propias escrituras en el espejo.

## Consultas

Consulte el espejo con SQL sin formato. `table()` devuelve el nombre de la tabla reflejada:

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

## Recuperación en archivos

`pull()` lee las filas del espejo y las escribe en el almacén registrado, en orden de id. Admite dos flujos de trabajo:

- Restauración: los archivos han desaparecido, pero el espejo sigue disponible. Registre un almacén nuevo con el mismo nombre de colección y recupere los datos.
- Carga inicial: las filas cargadas en la tabla del espejo mediante herramientas externas se convierten en registros de storh. Las filas deben incluir ids UUIDv7 válidos y un objeto en la columna `data`; al recuperar los datos se ignoran las columnas tipadas porque `data` contiene el registro completo.

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

Las escrituras recuperadas pasan por los mecanismos normales del almacén, por lo que se aplican la validación del esquema y los índices únicos, y cada registro se escribe de forma duradera. `pull()` nunca elimina registros locales y omite los registros cuyos datos ya coinciden con la fila del espejo. Después de cargar filas insertadas manualmente, ejecute `push()` una vez: reescribe esas filas en formato canónico para que sus hashes coincidan.

## Contrato y límites

- Los archivos son la fuente canónica. El espejo es una salida de solo lectura; lo que se escriba a mano en él se sobrescribirá o eliminará con el siguiente `push()`.
- Las columnas únicas del espejo sirven como respaldo, no como mecanismo de validación al escribir: si los archivos permitían un duplicado, este provocará un fallo al enviar los datos y la operación se revertirá por completo. Use índices únicos de storh para aplicar la validación al escribir.
- El espejo puede ir hasta un intervalo de envío por detrás de los archivos, salvo para los ids que `flush()` explícitamente.
- El reflejo no añade transacciones al propio storh. Las escrituras atómicas de varios registros siguen siendo tarea de una base de datos real como sistema de registro.
- Los cambios de esquema se aplican eliminando y reinstalando el espejo: `uninstall()`, `install()`, `push()`.
