
# Mirror SQL

`SqlMirror` mengirim koleksi storh ke tabel SQLite atau MySQL agar Anda dapat menjalankan join, pengurutan lintas tabel, pencarian substring, dan pelaporan di SQL sementara file tetap menjadi sumber kanonis. Anggap saja sebagai indeks sekunder yang kebetulan berupa basis data: mirror diturunkan dari data sumber, dapat dibuang, dan bisa dibangun ulang kapan saja. `ext-pdo` dengan driver `sqlite` atau `mysql` hanya diperlukan saat Anda menggunakannya; paket inti tetap bebas dependensi.

## Penyiapan

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

Kedua mesin dapat di-mirror: koleksi `DocStore` dan aliran `SegmentedLog` (hanya rekaman aktif yang dikirim). Memberikan `Schema` bersifat opsional dan tidak bergantung pada penyimpanan: kolom `string`/`int`/`float`/`bool` yang dideklarasikan menjadi kolom bertipe dan terindeks; semua yang lain tetap dapat dikueri di kolom `data` JSON. Kolom `mixed` tidak dapat ditandai untuk pengindeksan.

## Koneksi

Konstruktor menerima handle `PDO` (driver `sqlite` atau `mysql`, dideteksi secara otomatis), handle `mysqli`, atau implementasi `SqlMirrorConnection` apa pun:

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

Jalur mysqli berfungsi dengan kedua mode pelaporan kesalahan mysqli, sehingga handle yang sudah ada dari aplikasi host (misalnya `$wpdb->dbh` milik WordPress) dapat digunakan kembali apa adanya. Perilakunya sama di semua driver; kegagalan pengiriman membatalkan transaksi koleksi pada semuanya.

Setiap tabel memiliki `id` (kunci utama), `hash` (menggerakkan pengiriman inkremental), kolom bertipe, dan `data` (JSON rekaman lengkap).

## Sinkronisasi

```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()` membandingkan hash rekaman dengan mirror dan menulis setiap koleksi dalam satu transaksi, sehingga pengiriman yang crash atau gagal tidak pernah meninggalkan batch yang hanya diterapkan sebagian. Rekaman yang tidak berubah memerlukan satu `SELECT` ditambah pemindaian rekaman. `push()` berkala (misalnya dari tick cron) menjaga mirror tetap selaras; panggil `flush()` dengan id yang Anda tulis selama permintaan ketika pembaca perlu melihat hasil penulisan langsung dari mirror.

## Membuat kueri

Kueri mirror menggunakan SQL biasa. `table()` mengembalikan nama tabel yang di-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'
);
```

## Mengembalikan data ke file

`pull()` membaca baris mirror dan menulisnya ke penyimpanan terdaftar, menurut urutan id. Fitur ini mendukung dua alur:

- Pemulihan: file hilang, tetapi mirror masih ada. Daftarkan penyimpanan baru dengan nama koleksi yang sama, lalu tarik data.
- Pengisian awal: baris yang dimuat ke tabel mirror oleh alat eksternal menjadi rekaman storh. Baris harus memuat id UUIDv7 yang valid dan objek di kolom `data`; kolom bertipe diabaikan saat penarikan karena `data` adalah rekaman lengkap.

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

Penulisan hasil penarikan menggunakan mekanisme penyimpanan normal, sehingga validasi skema dan indeks unik tetap berlaku, dan setiap rekaman ditulis secara persisten. `pull()` tidak pernah menghapus rekaman lokal, dan rekaman dengan data yang sudah cocok dengan baris mirror akan dilewati. Setelah mengisi baris yang dimasukkan secara manual, jalankan `push()` sekali: tindakan ini menulis ulang baris tersebut dalam bentuk kanonis agar hash-nya selaras.

## Ketentuan dan batasan

- File adalah sumber kanonis. Mirror merupakan keluaran hanya-baca; apa pun yang ditulis secara manual ke dalamnya akan ditimpa atau dihapus oleh `push()` berikutnya.
- Kolom unik dalam mirror berfungsi sebagai pengaman tambahan, bukan penegakan saat penulisan: duplikat yang diizinkan oleh file akan menyebabkan pengiriman gagal (dan dibatalkan sepenuhnya). Gunakan indeks unik storh untuk penegakan saat penulisan.
- Mirror tertinggal dari file hingga satu interval pengiriman, kecuali untuk id yang secara eksplisit Anda `flush()`.
- Mirroring tidak menambahkan transaksi ke storh itu sendiri. Penulisan atomik untuk beberapa rekaman tetap perlu ditangani oleh basis data sungguhan sebagai sistem pencatatan utama.
- Perubahan skema diterapkan dengan menghapus dan memasang ulang mirror: `uninstall()`, `install()`, `push()`.
