
# SQL انعكاس

يدفع `SqlMirror` مجموعات storh إلى جداول SQLite أو MySQL لتتمكن من إجراء عمليات الربط والترتيب عبر الجداول والبحث عن السلاسل الفرعية وإعداد التقارير في SQL، مع بقاء الملفات هي المصدر المعتمد. اعتبره فهرسًا ثانويًا يتخذ هيئة قاعدة بيانات: فالنسخة المرآتية مشتقة ويمكن التخلص منها وإعادة بنائها في أي وقت. لا يلزم `ext-pdo` مع برنامج التشغيل `sqlite` أو `mysql` إلا عند استخدامه؛ وتبقى الحزمة الأساسية بلا اعتماديات.

## الإعداد

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

يمكن إنشاء نسخة مرآتية لكلا المحركين: مجموعات `DocStore` وتدفقات `SegmentedLog` (تُدفع السجلات النشطة فقط). تمرير `Schema` اختياري ومستقل عن المخزن: تصبح الحقول المعلنة `string`/`int`/`float`/`bool` أعمدة مفهرسة ذات أنواع محددة؛ وتبقى بقية الحقول قابلة للاستعلام في عمود `data` JSON. لا يمكن تحديد الحقول `mixed` للفهرسة.

## الاتصالات

يقبل المُنشئ مقبض `PDO` (برنامج تشغيل `sqlite` أو `mysql`، ويُكتشف تلقائيًا)، أو مقبض `mysqli`، أو أي تطبيق لـ `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_');
```

يعمل مسار mysqli مع كلا وضعي الإبلاغ عن الأخطاء في mysqli، لذا يمكن إعادة استخدام مقبض موجود من تطبيق مضيف (مثل `$wpdb->dbh` لدى WordPress) كما هو. السلوك متماثل عبر برامج التشغيل؛ إذ تؤدي حالات فشل الدفع إلى التراجع عن معاملة المجموعة في جميعها.

يحصل كل جدول على `id` (المفتاح الأساسي)، و`hash` (يُستخدم لإجراء عمليات الدفع التدريجي)، والأعمدة ذات الأنواع المحددة، و`data` (JSON السجل الكامل).

## المزامنة

```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()` تجزئات السجلات بالنسخة المرآتية ويكتب كل مجموعة ضمن معاملة واحدة، لذا لا تترك عملية الدفع المتعطلة أو الفاشلة دفعةً مطبقة جزئيًا. لا تكلّف السجلات التي لم تتغير سوى عملية `SELECT` واحدة بالإضافة إلى المرور على السجلات. ويحافظ تشغيل `push()` دوري (مثلًا عند كل نبضة من cron) على تقارب النسخة المرآتية؛ واستدعِ `flush()` مع المعرّفات التي كتبتها أثناء الطلب عندما يحتاج القراء إلى رؤية كتاباتهم مباشرةً في النسخة المرآتية.

## الاستعلام

استعلم عن النسخة المرآتية باستخدام SQL العادي. يعرض `table()` اسم الجدول المنسوخ:

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

## استعادة الملفات من النسخة المرآتية

يقرأ `pull()` صفوف النسخة المرآتية ويكتبها في المخزن المسجّل، حسب ترتيب المعرّف. وهو يدعم حالتين:

- الاستعادة: اختفت الملفات لكن النسخة المرآتية ما زالت موجودة. سجّل مخزنًا جديدًا بالاسم نفسه للمجموعة ثم اسحب البيانات.
- التهيئة الأولية: تتحول الصفوف التي حمّلتها أدوات خارجية إلى جدول النسخة المرآتية إلى سجلات storh. يجب أن تحمل الصفوف معرّفات UUIDv7 صالحة وكائنًا في العمود `data`؛ ويُتجاهل عند السحب محتوى الأعمدة ذات الأنواع المحددة، لأن `data` هو السجل بأكمله.

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

تُكتب البيانات المسحوبة عبر آليات المخزن المعتادة، لذا ينطبق التحقق من المخطط والفهارس الفريدة، ويُكتب كل سجل بشكل دائم. لا يحذف `pull()` السجلات المحلية أبدًا، ويتجاوز السجلات التي تطابق بياناتها صف النسخة المرآتية بالفعل. بعد تهيئة الصفوف المدرجة يدويًا، شغّل `push()` مرة واحدة لإعادة كتابة هذه الصفوف بالتنسيق المعتمد كي تتطابق تجزئاتها.

## الشروط والحدود

- الملفات هي المصدر المعتمد. النسخة المرآتية مخرجات للقراءة فقط؛ وكل ما يُكتب فيها يدويًا سيُستبدل أو يُحذف في عملية `push()` التالية.
- الأعمدة الفريدة في النسخة المرآتية بمثابة خط دفاع إضافي، وليست آلية إنفاذ وقت الكتابة: إذ يؤدي التكرار الذي سمحت به الملفات إلى فشل عملية الدفع والتراجع عنها بالكامل. استخدم الفهارس الفريدة في storh لإنفاذ التفرد وقت الكتابة.
- قد تتأخر النسخة المرآتية عن الملفات بمقدار فاصل دفع واحد، باستثناء المعرّفات التي تحددها صراحةً باستخدام `flush()`.
- لا تضيف النسخ المرآتي معاملات إلى storh نفسه. تظل الكتابات الذرية متعددة السجلات من مهام قاعدة بيانات حقيقية تكون مصدر السجلات المعتمد.
- تُطبّق تغييرات المخطط بحذف النسخة المرآتية وإعادة تثبيتها: `uninstall()`، `install()`، `push()`.
