
# الدمج ثلاثي الاتجاهات

يقارن الدمج ثلاثي الاتجاهات مجموعتي تغييرات بالرجوع إلى أصل مشترك، ثم يجمعهما في نتيجة واحدة. يطبّق merql هذه الخوارزمية على صفوف قاعدة البيانات بدلًا من أسطر الملفات.

```php
use Merql\Merge\ThreeWayMerge;

$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);

echo $result->operationCount(); // resolved operations
echo $result->conflictCount();  // unresolved conflicts
```

## الحالات الثلاث

يتطلب كل دمج ثلاث لقطات بالضبط:

- **Base** -- حالة قاعدة البيانات عند تفرّع الفرعين.
- **Ours** -- أحد الفرعين بعد تغييراته، مثل نسخة معزولة جرى تعديلها.
- **Theirs** -- الفرع الآخر بعد تغييراته، مثل قاعدة البيانات الحية التي واصلت العمل.

يجب التقاط ours وtheirs من قاعدة البيانات التي تحتوي على الفرع المقابل لكل منهما. تعامل قواعد الدمج الطرفين بالتساوي؛ وتحدد الأسماء `source` لكل عملية، وطريقة تسمية أنواع التعارض (`update_delete` تعني أن ours حدّث وtheirs حذف)، والطرف الذي تفضّله `ConflictPolicy::OursWins` و`TheirsWins`.

```
         Base (snapshot at fork time)
        /                              \
   Ours (our changes)                   Theirs (their changes)
        \                              /
         --------- MERGE ----------
                     |
              Merged result
              + conflicts (if any)
```

من دون أساس مشترك، لا يمكن التمييز بين «مضاف» و«لم يتغير». تكشف المقارنة الثنائية أن لدى الجانب A قيمة بينما لا توجد لدى الجانب B، لكنها لا تحدد ما إذا كان A قد أضافها أم أن B حذفها. يجيب الأساس عن هذا السؤال.

## كيفية حساب مجموعات التغييرات

تقارن `Differ` كل لقطة بالأساس لإنتاج مجموعتي تغييرات. تحتوي كل مجموعة تغييرات على ثلاثة أنواع من العمليات:

| الأساس | الحالة الحالية | العملية |
|---|---|---|
| الصف موجود | الصف موجود، وبصمة البيانات متطابقة | لا تغيير |
| الصف موجود | الصف موجود، وبصمة البيانات مختلفة | **UPDATE** (مقارنة الفروق لكل عمود) |
| الصف موجود | الصف غير موجود | **DELETE** |
| الصف غير موجود | الصف موجود | **INSERT** |

توفر البصمات (SHA-256 تجزئات محتوى الصف الموحّد) مسارًا سريعًا. إذا تطابقت بصمتان، فالصف لم يتغير ولا حاجة لمقارنته على مستوى الأعمدة.

```php
use Merql\Diff\Differ;

$differ = new Differ();
$oursChangeset = $differ->diff($base, $ours);
$theirsChangeset = $differ->diff($base, $theirs);
```

## قواعد الدمج

بعد ذلك تُدمج مجموعتا التغييرات عمليةً بعملية. يحدد هذا الجدول النتيجة لكل الاحتمالات:

| ours | theirs | النتيجة |
|---|---|---|
| لا تغيير | لا تغيير | الإبقاء على الأساس |
| لا تغيير | تحديث | قبول theirs |
| تحديث | لا تغيير | قبول ours |
| تحديث (القيمة نفسها) | تحديث (القيمة نفسها) | قبول (اتفاق الطرفين) |
| تحديث (قيمة مختلفة) | تحديث (قيمة مختلفة) | **تعارض** |
| لا تغيير | حذف | قبول الحذف |
| حذف | لا تغيير | قبول الحذف |
| حذف | حذف | قبول الحذف (اتفاق الطرفين) |
| تحديث | حذف | **تعارض** |
| حذف | تحديث | **تعارض** |
| إدراج (ours فقط) | -- | قبول الإدراج |
| -- | إدراج (theirs فقط) | قبول الإدراج |
| إدراج (المفتاح الأساسي نفسه) | إدراج (المفتاح الأساسي نفسه) | **تعارض** |

عندما يحدّث الطرفان الصف نفسه، لا يعلن merql عن تعارض فورًا. بل ينتقل إلى الدمج على مستوى الأعمدة، ويقارن كل عمود على حدة. راجع [الدمج على مستوى الأعمدة](https://merql.inline0.dev/ar/docs/merge/column-level) للاطلاع على التفاصيل.

## استخدام الواجهة الموحدة

تعمل الواجهة الموحدة `Merql` على لقطات مسماة حُفظت على القرص. استدعِ `Merql::init()` مع قاعدة البيانات التي تحتوي على كل حالة قبل التقاط اللقطة:

```php
use Merql\Connection;
use Merql\Merql;

$live    = Connection::sqlite('/data/live.sqlite');
$sandbox = Connection::sqlite('/data/sandbox.sqlite');

Merql::init($live);
Merql::snapshot('base');      // before the sandbox is copied from live

// ... copy live.sqlite to sandbox.sqlite, then both databases change ...

Merql::init($sandbox);
Merql::snapshot('ours');
Merql::init($live);
Merql::snapshot('theirs');

$result = Merql::merge('base', 'ours', 'theirs');
```

لا تلتقط اللقطات الثلاث كلها بالتتابع من قاعدة بيانات واحدة. عندها ستتضمن theirs تغييرات ours أيضًا، فيظهر كل صف أدرجه ours كما لو أُدرج في كلا الطرفين، ويُبلّغ عنه بوصفه تعارض `insert_insert`.

تنشئ `Merql::merge()` و`Merql::patch()` و`Merql::plan()` كائن `ThreeWayMerge` من دون `CellMergeConfig`، لذا لا تدمج إلا على مستوى الأعمدة. لإجراء الدمج على مستوى الخلية، حمّل اللقطات واستخدم الفئة مباشرةً كما يلي.

## استخدام الفئات مباشرةً

لمزيد من التحكم، أنشئ مثيلات المكونات بنفسك. لا تحفظ `Snapshotter::capture()` أي شيء، لذا احتفظ باللقطات في الذاكرة أو احفظها باستخدام `SnapshotStore::save()`:

```php
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Snapshot\Snapshotter;
use Merql\Snapshot\SnapshotStore;

$base   = (new Snapshotter($live))->capture('base');
// ... copy live to the sandbox, then both databases change ...
$ours   = (new Snapshotter($sandbox))->capture('ours');
$theirs = (new Snapshotter($live))->capture('theirs');

$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);

// Snapshots saved earlier, merged with cell-level merge for TEXT and JSON columns.
$merge = new ThreeWayMerge(CellMergeConfig::auto());
$result = $merge->merge(
    SnapshotStore::load('base'),
    SnapshotStore::load('ours'),
    SnapshotStore::load('theirs'),
);
```

## الاختصار باستخدام التصحيح

عندما تكون التغييرات في طرف واحد فقط (من دون تعديلات متزامنة)، استخدم `patch()`. فهذا دمج ثنائي الاتجاه تكون فيه ours مساوية للأساس، لذا يستحيل حدوث تعارض:

```php
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.
```

عبر الواجهة الموحدة:

```php
$result = Merql::patch('base', 'changes');
```

يكافئ `patch()` `merge($base, $base, $changes)`. وبما أن ours لم يختلف قط عن الأساس، تُقبل كل تغييرات theirs من دون تعارض.

## اكتشاف اختلافات المخطط

إذا أُضيفت أعمدة إلى جدول أو أُزيلت منه بين اللقطات، يستمر الدمج مع تسجيل الاختلافات. لا تُقارن أنواع الأعمدة المتغيرة، ويظهر العمود الذي أُعيدت تسميته كعمود محذوف وآخر مضاف؛ راجع [التحقق من المخطط](https://merql.inline0.dev/ar/docs/advanced/schema).

```php
$result = $merge->merge($base, $ours, $theirs);

if ($result->hasSchemaMismatches()) {
    foreach ($result->schemaMismatches() as $mismatch) {
        echo $mismatch->getMessage() . "\n";
    }
}
```

لا يجري merql ترحيلًا تلقائيًا للمخططات. تقع تغييرات المخطط على عاتق المستدعي. ويبلّغ عنها الدمج لتتمكن من تحديد كيفية المتابعة.

## نتيجة الدمج

يحمل كائن النتيجة كل ما تحتاج إليه:

```php
$result->isClean();            // true if no conflicts
$result->operations();         // list of MergeOperation objects
$result->conflicts();          // list of Conflict objects
$result->operationCount();     // number of resolved operations
$result->conflictCount();      // number of unresolved conflicts
$result->hasSchemaMismatches(); // true if schemas differ
$result->baseSnapshot();       // the base snapshot used for the merge
```

لكل `MergeOperation` نوع (`insert` أو `update` أو `delete`)، واسم جدول، ومفتاح صف، وقيم أعمدة، ومصدر (`ours` أو `theirs` أو `merged`). تشير `merged` إلى صف حدّثه الطرفان (بدمج التغييرات عمودًا بعمود) أو حذفاه معًا.

## المواضع التي تنطبق عليها نتيجة الدمج

تصف نتيجة الدمج كيفية تحويل الأساس إلى الحالة المدمجة. وتحمل عملياتها تغييرات الطرفين: يظهر كل من الإدراج الذي أجراه ours والإدراج الذي أجراه theirs كعملية إدراج.

- تنفّذ `Merql::apply()` (أو `Applier`) كل عملية على قاعدة البيانات المتصلة. وتنطبق بسلاسة على قاعدة بيانات ما زالت تحتوي على الأساس، مثل نسخة جديدة من حالة الأساس. أما عند تطبيقها على قاعدة بيانات تتضمن تغييرات أحد الطرفين بالفعل، فسيفشل إدراج صف موجود وتُلغى المعاملة.
- لإدخال تغييرات ours إلى قاعدة بيانات تحتوي على تغييرات theirs بالفعل، مثل ترقية تعديلات النسخة المعزولة إلى البيئة الحية، طبّق العمليات التي لا تحتويها قاعدة البيانات فقط: تلك التي مصدرها `ours` وتحديثات `merged`. استخدم [خطة دمج](https://merql.inline0.dev/ar/docs/merge/plans) لتحديدها و[التطبيق المحروس](https://merql.inline0.dev/ar/docs/apply/guarded) لكتابتها مع شروط مسبقة على الصفوف الحية.
