> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> مواصفة صيغة Native العمودية في ClickHouse: البنى الأساسية لتنسيق wire، وبنية كتلة وColumn، وترميز كل نوع من أنواع البيانات، وإطار الضغط

# صيغة Native

صيغة Native هي تنسيق wire العمودي الذي يعتمده ClickHouse لنقل البيانات الجدولية، وتُستخدم في عدة مواضع:

* متن حزم `Data` و`Totals` و`Extremes` و`Log` و`ProfileEvents` في [بروتوكول TCP الأصلي](https://clickhouse.com/docs/ar/resources/develop-contribute/native-protocol/protocol.md) (علمًا بأن حزمة `TableColumns` **ليست** كتلة Native، إذ تحمل سلسلتين ثنائيتين، ولذلك يُشرح تخطيطها في [مواصفة البروتوكول الأصلي](https://clickhouse.com/docs/ar/resources/develop-contribute/native-protocol/protocol.md))؛
* إخراج `SELECT ... FORMAT Native` عبر HTTP؛
* الملفات المُصدَّرة باستخدام `INTO OUTFILE ... FORMAT Native`؛
* حمولات النسخ المتماثل بين الخوادم.

تصف هذه الصفحة البايتات داخل كتلة — أي الحمولة العمودية — وترميزات الأنواع الخاصة بكل عمود التي تتكوّن منها. أما تأطير الحزم وحالة الاتصال والتفاوض على الإصدار فتتناولها [مواصفة البروتوكول الأصلي](https://clickhouse.com/docs/ar/resources/develop-contribute/native-protocol/protocol.md).

تُرمَّز جميع حقول الأعداد الصحيحة متعددة البايتات بترتيب little-endian، وتُمثَّل الأعداد الصحيحة ذات الإشارة بنظام المتمم الثنائي (two's complement).

> [!TIP]
>
> للاطلاع على مقدمة موجّهة للمستخدمين حول صيغة `Native` (مع أمثلة باستخدام `curl`)، راجع [صفحة صيغة Native](https://clickhouse.com/docs/ar/reference/formats/Native.md). أما هذه المواصفة فهي مرجع منخفض المستوى لتنسيق wire.

## نظرة عامة

كل ما ينقل الصفوف في تنسيق النقل هو **كتلة**: مقطع ذاتي الوصف من الصفوف مُخزَّن عمودًا تلو الآخر، إذ تأتي جميع قيم العمود 1 أولًا، ثم جميع قيم العمود 2، وهكذا. ولا تحمل الكتلة إلا الأعمدة التي يشير إليها الاستعلام، ولا تحمل الجدول كاملًا أبدًا.

تُرتَّب بيانات `data` الخاصة بالعمود وفقًا لـ*العائلة* التي ينتمي إليها نوعه. وفيما يلي هذه العائلات مرتبةً تصاعديًا بحسب تعقيد أداة فك الترميز:

![بنية كتلة wire عمودية وعائلات تخطيط بياناتها](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/block-structure.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=e4e067d9d428c13b9fce7fa7fe812828)

* الأنواع **ثابتة العرض** ترتّب `data` على هيئة بايتات خام بحجم `bytes_per_value × num_rows`، دون أي تأطير على مستوى الصف.
* الأنواع **المركّبة** (`Nullable` و`Array` و`Tuple` و`Map` و`Nested`) لها بنية تعاودية يمكن اشتقاقها بالكامل من سلسلة النوع، فلا بادئة إصدار لها ولا حالة تمتد عبر الكتل.
* الأنواع **ذات الإصدارات / ذات الحالة** (`LowCardinality` و`JSON` و`Variant` و`Dynamic`) تبدأ كل كتلة غير فارغة ببادئة إصدار التسلسل/بادئة الحالة. وفي تنسيق النقل `Native`، تكون هذه البادئة وأي قاموس مرتبطَين **بكل كتلة على حدة** — فالصيغة لا تحمل أي حالة *عبر* الكتل (إذ يُنشئ الكاتب حالة تسلسل جديدة لكل كتلة ويضبط `low_cardinality_max_dictionary_size = 0`). أما الحالة الممتدة عبر الكتل فتخص تخزين MergeTree على القرص، ولا علاقة لها بتخطيط wire الخاص بصيغة Native.

## العناصر الأولية للتمثيل الثنائي

تعتمد صيغة Native على أربعة ترميزات أولية.

| العنصر الأولي | الحجم | الوصف |
| - | - | - |
| VarUInt | 1–10 B | عدد صحيح بلا إشارة ومتغير الطول بترميز LEB-128 |
| عدد صحيح ثابت العرض | 1, 2, 4, 8, 16, 32 B | ترتيب بايتات little-endian، مع تمثيل المتمم الثنائي (two's complement) للأعداد ذات الإشارة |
| String | متغير | بادئة طول من نوع VarUInt تليها البايتات الخام لكل قيمة؛ ويُستخدم [دفق إزاحات](#string-type) منفصل بدءًا من revision `54492`+ |
| Bool | 1 B | `0x00` = false، وأي قيمة غير صفرية = true |

### VarUInt

عدد صحيح بدون إشارة متغير الطول يستخدم ترميز LEB-128. يحمل كل بايت 7 بتات بيانات في المواضع 0–6، وبت استمرار واحدًا في الموضع 7. تكون قيمة بت الاستمرار `1` عندما تليه بايتات أخرى، و`0` في البايت الأخير.

| نطاق القيمة | عدد البايتات |
| - | - |
| 0 – 127 | 1 |
| 128 – 16383 | 2 |
| 16384 – 2097151 | 3 |
| حتى النطاق الكامل لـ UInt64 | حتى 10 |

ترميز القيمة `300`:

```text theme={null}
300 = 0b100101100

Byte 0: 0xAC = 0b10101100   (data: 0101100, continuation: 1)
Byte 1: 0x02 = 0b00000010   (data: 0000010, continuation: 0)
```

فك ترميز البايتين `0xAC 0x02`:

```text theme={null}
Byte 0: data = 0x2C, continuation = 1 → accumulator = 0x2C, shift = 7
Byte 1: data = 0x02, continuation = 0 → accumulator = (0x02 << 7) | 0x2C = 300
```

### الأعداد الصحيحة ذات العرض الثابت

| النوع | البايتات | الترميز |
| - | - | - |
| UInt8 | 1 | بايت خام |
| UInt16 | 2 | Little-endian |
| UInt32 | 4 | Little-endian |
| UInt64 | 8 | Little-endian |
| UInt128 | 16 | Little-endian |
| UInt256 | 32 | Little-endian |
| Int8 | 1 | بايت خام، بتمثيل المتمم الثنائي |
| Int16 | 2 | Little-endian، بتمثيل المتمم الثنائي |
| Int32 | 4 | Little-endian، بتمثيل المتمم الثنائي |
| Int64 | 8 | Little-endian، بتمثيل المتمم الثنائي |
| Int128 | 16 | Little-endian، بتمثيل المتمم الثنائي |
| Int256 | 32 | Little-endian، بتمثيل المتمم الثنائي |
| Float32 | 4 | IEEE 754 أحادي الدقة، little-endian |
| Float64 | 8 | IEEE 754 مزدوج الدقة، little-endian |

على سبيل المثال، تُرمَّز القيمة `1` من النوع UInt32 بالشكل `01 00 00 00`، والقيمة `-1` من النوع Int32 بالشكل `FF FF FF FF`.

### String

سلسلة من البايتات مسبوقة بطولها:

```text theme={null}
[VarUInt: byte_length] [byte_length bytes: raw value]
```

لا يُشترط أن يكون تسلسل البايتات بترميز UTF-8 صالحًا. تُرمَّز السلسلة الفارغة في صورة بايت واحد هو `0x00`، ويمكن أن تحتوي السلاسل النصية على أي قيم للبايتات، بما في ذلك محارف NUL المُضمَّنة. فعلى سبيل المثال، تُرمَّز السلسلة `"ab"` في صورة `02 61 62`؛ ولفك ترميزها، اقرأ الطول المُرمَّز بصيغة VarUInt (`2`)، ثم اقرأ العدد نفسه من البايتات.

### Bool

بايت واحد. تمثّل القيمة `0x00` القيمة false، بينما تمثّل أي قيمة غير صفرية القيمة true (والصيغة القياسية لها هي `0x01`).

## بنية الكتلة والأعمدة

### تخطيط wire للكتلة

```text theme={null}
[BlockInfo]               metadata (only on the TCP Data-packet path; see below)
[VarUInt: num_columns]    number of columns in this block
[VarUInt: num_rows]       number of rows in this block
[Column × num_columns]    column entries, omitted when num_columns = 0
```

يعتمد وجود البادئة `BlockInfo` على القناة المستخدمة، لأن الكاتب يُضبط وفق *revision* (راجع [مراجعة البروتوكول وصيغة Native](#protocol-revision) للاطلاع على الشرح الكامل، بما في ذلك كون `client_protocol_version` خاصًا بالإخراج فقط):

* في **بروتوكول TCP الأصلي**، يكتب الخادم الكتل وفق الـ revision المتفاوض عليها للاتصال (وهي قيمة كبيرة — `DBMS_TCP_PROTOCOL_VERSION`، راجع `src/Core/ProtocolDefines.h`). وتُكتب `BlockInfo` متى كانت قيمة الـ revision أكبر من الصفر، وهو ما يتحقق دائمًا في أي اتصال فعلي. أما البايت `has_custom_serialization` في كل عمود (راجع [تخطيط wire للأعمدة](#column-wire-layout)) فيُكتب ابتداءً من الـ revision `54454`.
* أما *تنسيق الإخراج* `Native` — أي `SELECT ... FORMAT Native` عبر HTTP، و`INTO OUTFILE ... FORMAT Native`، وصيغة `Native` التي يُنتجها `clickhouse-client` — فيُجري التسلسل عند الـ revision `0` *افتراضيًا*. وعند الـ revision `0` تُحذف البادئة `BlockInfo` والبايت `has_custom_serialization` معًا، فلا تتكوّن الكتلة إلا من `num_columns` و`num_rows` والأعمدة.

  غير أن هذه الـ revision ليست ثابتة عبر HTTP، إذ يمكن للعميل رفعها باستخدام معامل الاستعلام `?client_protocol_version=<n>`، فيعتمد الخادم تلك القيمة revision للتسلسل في الاستجابة.

  وعند استخدام قيمة مرتفعة بما يكفي، يتضمن إخراج HTTP البادئة `BlockInfo` (التي تُكتب متى كانت الـ revision أكبر من `0`) والبايت `has_custom_serialization` (الذي يُكتب ابتداءً من الـ revision `54454`)، تمامًا كما في مسار TCP. لذا لا ينبغي للعملاء أن يفترضوا أن كل حمولة `FORMAT Native` عبر HTTP تكون بالـ revision `0`.

بعبارة أخرى، تصف أمثلة البايتات الواردة في هذا القسم والتي تبدأ ببادئة `BlockInfo` حمولةَ حزمة Data في TCP، في حين يُنتج الاستعلام نفسه عند تنفيذه عبر `FORMAT Native` الشكلَ الأقصر المعروض بجانبها.

### BlockInfo

‏BlockInfo هو تسلسل من الحقول، يسبق كلًّا منها معرّف حقل من النوع VarUInt، وينتهي بمعرّف حقل قيمته `0`. وتنسيق wire هنا **ليس** ذاتي الوصف: فمعرّف الحقل لا يُرمِّز طول قيمته ولا نوعها، ولذا يجب أن يعرف القارئ مسبقًا نوع كل معرّف حقل قد يصادفه. ويعامل القارئ الخاص بـ ClickHouse أي معرّف حقل غير معروف على أنه تلف في البيانات، فيُطلق استثناءً (`UNKNOWN_BLOCK_INFO_FIELD`). أما التوافق المستقبلي فتتكفّل به revision البروتوكول بدلًا من ذلك: إذ لا يكتب المُرسِل حقلًا إلا إذا كانت revision المتفاوض عليها لا تقل عن الحد الأدنى لـ revision الخاص بذلك الحقل، وبذلك لا يصادف المستقبِل الأقدم أبدًا حقلًا لا يعرفه.

| معرّف الحقل | الحقل | النوع | الحد الأدنى لـ revision | الوصف |
| - | - | - | - | - |
| 1 | is\_overflows | UInt8 | 0 | كتلة الفائض الناتجة عن GROUP BY. القيمة `0` للكتل غير الفائضة. |
| 2 | bucket\_number | Int32 | 0 | حاوية التجميع. القيمة `-1` للكتل غير المقسّمة إلى حاويات. |
| 3 | out\_of\_order\_buckets | قائمة من Int32 | 54480 | الحاويات المؤجَّلة أثناء التجميع الموزّع. تُرمَّز على هيئة عدد من النوع VarUInt تليه قيم `Int32` بالعدد نفسه. |
| 0 | (علامة الإنهاء) | — | — | نهاية BlockInfo. مطلوبة دائمًا. |

الحد الأدنى لـ revision للحقلين `1` و`2` هو `0`، ولذا يكونان موجودين كلما كُتب `BlockInfo`. أما الحقل `3` فلا يُكتب إلا عند revision `54480` فما فوق. فيما يلي تخطيط wire للحالة الشائعة (revision أقل من `54480`):

```text theme={null}
[VarUInt: 1] [UInt8: is_overflows]
[VarUInt: 2] [Int32: bucket_number]
[VarUInt: 0]
```

### تخطيط wire للعمود

يظهر العمود `num_columns` مرة داخل الكتلة (كتلة).

| # | الحقل | النوع | الشرط | الوصف |
| - | - | - | - | - |
| 1 | name | String | دائمًا | اسم العمود |
| 2 | type | String *أو* ترميز ثنائي للنوع | دائمًا | سلسلة نوع ClickHouse (مثل `"UInt64"` و`"Array(String)"`) افتراضيًا؛ أو ترميز ثنائي للنوع عند ضبط `output_format_native_encode_types_in_binary_format = 1`، باستثناء الاستعلام الثانوي (راجع الملاحظة أدناه) |
| 3 | has\_custom\_serialization | UInt8 | الميزة `CUSTOM_SERIALIZATION` (v54454) | `0` = افتراضي، `1` = مخصص (يليه kind\_stack) |
| 4 | kind\_stack | bytes | عندما يكون الحقل 3 = `1` | بايت تعداد واحد من نوع UInt8 (راجع أدناه) يصف التسلسل غير الافتراضي (متناثر وغيره). في حالة القيمة `COMBINATION`، يليه عدد من نوع VarUInt ثم بايتات نوع إضافية بقدر ذلك العدد. أما في حالة `Tuple` (والأنواع المركبة الأخرى التي تحمل معلومات تسلسل على مستوى العناصر)، فتكون الحمولة تعاودية — راجع أدناه. |
| 5 | data | bytes | دائمًا | قيم العمود لجميع الصفوف البالغ عددها `num_rows`. يختلف التخطيط بحسب النوع — راجع [أنواع البيانات](#data-types). أما الأعمدة المتناثرة، فراجع ما يلي. |

تحدد أداة فك الترميز طريقة المعالجة بناءً على سلسلة `type`. وكثيرًا ما تتضمن سلاسل الأنواع معلمات بين قوسين؛ فتُزيل أداة فك الترميز اللاحقة `(...)` لاستخراج النوع الأساسي، ثم تحلل المعلمات لتحديد الحجم أو المقياس أو النوع الداخلي. ويتطلب تحليل قائمة معلمات تتضمن أنواعًا متداخلة (مثل `Tuple` داخل `Array`) أداة تقسيم بالفواصل تراعي العمق وتتتبع تداخل الأقواس، بدلًا من التقسيم البسيط على `,`.

> [!NOTE]
>
> **الترميز الثنائي للنوع**
>
> لا يكون الحقل `type` من النوع النصي `String` إلا في الوضع الافتراضي. أما عند ضبط إعداد الاستعلام `output_format_native_encode_types_in_binary_format = 1`، فيصبح هذا الحقل **ترميزًا ثنائيًا للنوع** — وهو الترميز نفسه القائم على الوسوم والموثّق في [الترميز الثنائي لأنواع البيانات](https://clickhouse.com/docs/ar/reference/data-types/data-types-binary-encoding.md) — كما تستخدم قوائم النوع `Dynamic` المسطّحة الترميز الثنائي نفسه لأسماء أنواعها. وأي أداة فك ترميز تقرأ الحقل 2 دائمًا على أنه سلسلة مسبوقة بطولها ستفسّر أول وسم ثنائي للنوع على أنه طول سلسلة فتفقد التزامن، لذا يجب أن تعرف الوضع الذي يستخدمه التدفق.
>
> بالنسبة إلى الاستعلام الذي يكون فيه `query_kind = SECONDARY_QUERY` (المُرسَل من خادم آخر عبر `remote` أو `Distributed` أو النسخ المتماثلة المتوازية، أو عبر `clickhouse-client --query_kind secondary_query`)، يتجاهل كلا الطرفين `output_format_native_encode_types_in_binary_format` و`input_format_native_decode_types_in_binary_format` في كتل ذلك الاستعلام، إذ تحمل هذه الكتل أسماء الأنواع بصيغتها النصية في كلا الاتجاهين. أما الخوادم الأقدم من هذه القاعدة فتطبّق هذه الإعدادات على الاستعلام الثانوي كما تطبّقها على أي استعلام آخر.

![توجيه أداة فك الترميز من وصف النوع إلى عائلة الترميز الخاصة به](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/decoder-dispatch.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=b922e9bfee45e9c3f8d7e090bc5a31ea)

#### kind\_stack والترميز المتناثر

يُعدِّد البايت `kind_stack` تسلسلًا غير افتراضي خاصًا بكل عمود:

| البايت | الاسم | المعنى | الأثر على `data` في تنسيق wire |
| - | - | - | - |
| `0x00` | DEFAULT | التسلسل الافتراضي | مطابق لـ `has_custom = 0` |
| `0x01` | SPARSE | التسلسل المتناثر (v54465+) | دفق الإزاحات + القيم غير الافتراضية؛ انظر أدناه |
| `0x02` | DETACHED | عمود مُغلَّف داخل `ColumnBLOB` بواسطة التجهيز المتوازي للكتل (v54478+) | كائن blob مُنظَّم مسبقًا: `VarUInt size` يليه ذلك العدد من البايتات؛ انظر أدناه |
| `0x03` | DETACHED\_OVER\_SPARSE | عمود متناثر مُغلَّف داخل `ColumnBLOB` | حمولة blob نفسها كما في `DETACHED`؛ انظر أدناه |
| `0x04` | REPLICATED | صيغة قاموس للقيم المتكررة (v54482+) | دفق الفهارس + قيم العناصر الكثيفة؛ انظر أدناه |
| `0x05` | COMBINATION | مكدس متعدد الأنواع | يليه `count` من نوع VarUInt ثم `count` من بايتات الأنواع الإضافية — انظر الملاحظة أدناه |

**تستخدم حمولة `COMBINATION` تعدادًا مختلفًا.** الصفوف الخمسة أعلاه رموز *مضغوطة* بطول بايت واحد. أما `COMBINATION` (`0x05`) فهو المخرج العام لأي مكدس لا تغطيه تلك الرموز: إذ يليه `count` من نوع `VarUInt` ثم `count` من الإدخالات بطول بايت واحد. وهذه الإدخالات **ليست** الرموز المضغوطة الواردة في الجدول، بل هي قيم `ISerialization::Kind` الخام:

| البايت | `Kind` المتداخل |
| - | - |
| `0x00` | DEFAULT |
| `0x01` | SPARSE |
| `0x02` | DETACHED |
| `0x03` | REPLICATED |

تختلف قيم البايتات هنا عن الرموز المضغوطة: فقيمة `REPLICATED` هي `0x03` في هذا التعداد المتداخل، بينما هي `0x04` كرمز مضغوط، ولا يوجد إدخال `DETACHED_OVER_SPARSE`، إذ يظهر هذا التركيب على هيئة إدخالين متتاليين هما `SPARSE` ثم `DETACHED`. وأي أداة فك ترميز تستمر في استخدام الجدول المضغوط للبايتات المتداخلة ستخطئ في تعيين `0x03`/`0x04` وتفقد التزامن.

يمثل `count` الطول الكامل للمكدس **بما في ذلك إدخال `DEFAULT` الأول** الذي يبدأ به كل مكدس. ولأن الرموز المضغوطة تغطي أصلًا كل مكدس مكوّن من إدخال واحد أو إدخالين، فإن قيمة `count` في `COMBINATION` لا تقل أبدًا عن ثلاثة.

**صلاحية مكدس الأنواع.** ترفض أداة فك الترميز المكدس غير السليم بالخطأ `INCORRECT_DATA` بدلًا من بناء التسلسل الذي يصفه، إذ يجب أن:

* يبدأ المكدس بـ `DEFAULT`؛
* يكون المكدس تسلسلًا فرعيًا من `DEFAULT` و`SPARSE` و`REPLICATED` و`DETACHED`، وهو الترتيب الذي تُغلِّف به الأنواع بعضها بعضًا. فيمكن أن يقع `SPARSE` داخل `REPLICATED` لا العكس، ويأتي `DETACHED` دائمًا في النهاية لأن `ColumnBLOB` يحتوي على الصيغة المُسلسَلة لكل ما تحته. ويعني هذا أيضًا أن أي نوع لا يتكرر، مما يجعل `count` محدودًا بعدد الأنواع.

لاحظ أن هذا الترتيب ليس ترتيب قيم بايتات الأنواع المذكورة أعلاه. فأي مكدس آخر يصف تخطيطًا للعمود لا يبنيه أي كاتب، ولا تفكّ أي خطوة تجسيد تغليفه ليعود عمودًا كاملًا من النوع المُعلَن؛ فمثلًا يبقى `ColumnSparse` مُغلَّفًا إذا كان عمود قيمه نفسه متناثرًا أو متماثلًا.

**`kind_stack` التعاودي لأعمدة `Tuple`.** حمولة `kind_stack` المذكورة أعلاه هي البايت (أو تسلسل `COMBINATION`) الخاص بمعلومات التسلسل لعمود واحد. أما `Tuple` فيحمل `SerializationInfoTuple`، الذي يكتب أولًا حمولة مكدس الأنواع *الخاصة* بالـ Tuple، ثم يكتب حمولة كاملة لمكدس الأنواع لـ*كل* عنصر بالترتيب؛ وتقرأ أداة فك الترميز البنية التعاودية نفسها. وبذلك تكون بايتات الحقل 4 لـ `Tuple(A, B, C)` هي `[tuple_kind][A_kind][B_kind][C_kind]`، وتكون حمولة كل عنصر بدورها تعاودية إذا كان ذلك العنصر مركّبًا أيضًا. يُضبط البايت `has_custom_serialization` (الحقل 3) كلما كانت معلومات الـ Tuple نفسه *أو معلومات أي من عناصره* غير افتراضية، لذا فإن `Tuple` الذي لا يتميز من عناصره إلا عنصر واحد متناثر أو متماثل يستدعي هو الآخر حمولة مكدس الأنواع. وأي أداة فك ترميز تكتفي بقراءة بايت التعداد الأول لـ `Tuple` ستتوقف قبل الأوان، وتقرأ خطأً بايتات أنواع العناصر المتبقية على أنها بيانات أعمدة.

**تنسيق wire المتناثر.** عندما تكون `kind_stack = 0x01`، تتكوّن `data` الخاصة بالعمود من دفقين مكتوبين على التوالي ضمن دفق TCP المشترك الوحيد:

1. **دفق الإزاحات** — تسلسل من قيم `VarUInt`. وكل قيمة `v` تكون على إحدى حالتين:
   * `v` يكون فيها البت ذو الموضع 62 غير مضبوط: `(v & 0x3FFFFFFFFFFFFFFF)` = عدد المواضع الافتراضية التي تسبق القيمة الصريحة غير الافتراضية التالية. ويكون ذلك الموضع غير الافتراضي هو `cursor + group_size`، حيث `cursor` هو الموضع الحالي؛ ثم يتقدّم `cursor` بمقدار `group_size + 1`.
   * `v` يكون فيها البت 62 مضبوطًا (`END_OF_GRANULE_FLAG`): القيمة بعد مسح العلامة = عدد المواضع الافتراضية اللاحقة لآخر قيمة غير افتراضية. ويدل ذلك على نهاية دفق الإزاحات الخاص بالكتلة.
2. **دفق القيم** — عدد `count` من القيم غير الافتراضية المُرمَّزة بشكل متراص وفق النوع الداخلي، حيث `count` هو عدد قيم VarUInt المقروءة أعلاه التي لا تدل على نهاية الحبيبة (EOG).

تعيد أداة فك الترميز بناء عمود متراص يضم `num_rows` عنصرًا، وذلك بملء كل موضع غير صريح بالقيمة الافتراضية للنوع الداخلي (`0` للأعداد الصحيحة والأعداد ذات الفاصلة العائمة، و`""` لـ `String`، و`0` يوم لـ `Date`، وهكذا).

يُعدّ العمود المتناثر من النوع `Nullable(T)` حالة خاصة، لأن القيمة الافتراضية لـ `Nullable(T)` هي **NULL**. إذ يستغني الترميز المتناثر كليًّا عن دفق null-map المعتاد في `Nullable`: فدفق الإزاحات يحدّد المواضع غير الافتراضية — أي المواضع غير NULL — ودفق القيم يضم تلك القيم غير NULL وحدها بشكل متراص وفق `T`، ويُعاد بناء كل موضع غير صريح على أنه NULL. لذا يجب على أداة فك الترميز *ألّا* تبحث عن null map في دفق القيم، و*ألّا* تملأ الفجوات بقيمة `0` فعلية، بل تملؤها بـ NULL.

**تنسيق wire للنوع Replicated.** عندما تكون `kind_stack = 0x04`، تكون بيانات العمود `data` قاموسًا، أي قائمة بقيم العناصر المميزة مع فهرس لكل صف يشير إلى موضعه في تلك القائمة (وهي بنية البحث نفسها المستخدمة في `LowCardinality`). وإذا كان النوع الداخلي نفسه ذا إصدار — مثل `LowCardinality(T)` — فإن بادئة الحالة الخاصة به تُكتب **أولًا**، قبل دفق الفهرس: إذ يفوّض التسلسل المتماثل مرحلة البادئة إلى النوع الداخلي قبل كتابة `num_rows`. أما الأنواع الداخلية ذات البادئة الفارغة (الأنواع الطرفية والأنواع المركّبة البسيطة) فلا تضيف أي بايتات هنا.

```text theme={null}
[inner type's state prefix]              empty for leaf inners; e.g. LowCardinality version (Int64 = 1)
[VarUInt num_rows]
[UInt8  size_of_indexes_type]            width of each index: 1, 2, 4, or 8 bytes
[indexes: num_rows × size_of_indexes_type bytes]
[VarUInt num_elements]
[elements: num_elements dense inner-type values]
```

تعيد أداة فك الترميز بناء column كثيف عبر اختيار `elements[indexes[i]]` لكل صف إخراج `i`. أما الأنواع الداخلية المركّبة فتُعالَج تعاوديًا: إذ تُجسَّد قائمة العناصر بالنوع الداخلي أولًا، ثم تُفهرَس. وتشمل الأنواع الداخلية المدعومة الأنواع الطرفية، و`Nullable(T)`، و`Array(T)`، و`Tuple(...)`، و`Map(K, V)`، و`Nested(...)` (حيث يُوسَّع كل حقل كما في `Array`)، و`LowCardinality(T)` (حيث يُحتفَظ بالقاموس المشترك، ولا تُفهرَس إلا المفاتيح الخاصة بكل عنصر).

**تنسيق wire المنفصل.** يظهر `DETACHED` (`0x02`) و`DETACHED_OVER_SPARSE` (`0x03`) *فعلًا* أثناء النقل عبر الشبكة، فهما ليسا داخليين بحتين. ففي مسار TCP، عند تفعيل الضغط وكون قيمة revision المتفاوض عليها لا تقل عن `DBMS_MIN_REVISON_WITH_PARALLEL_BLOCK_MARSHALLING` (v54478)، يمر الـ column بثلاث خطوات:

1. يُغلَّف كل column مؤهَّل (أي غير `const`، وغير `Tuple`، وموجود في كتلة تضم أكثر من صف واحد) داخل `ColumnBLOB` يحتفظ بالـ column بعد ترتيبه وضغطه مسبقًا خارج خيط التنفيذ الرئيسي.
2. يُضاف `DETACHED` إلى مكدس الأنواع الخاص بالـ column المُغلَّف.
3. تُكتب `data` الخاصة بالـ column على هيئة حجم blob بصيغة `VarUInt`، يليه عدد مطابق له تمامًا من بايتات الـ blob.

إذا كان الـ column المُغلَّف متناثرًا، يكون مكدسه `{DEFAULT, SPARSE, DETACHED}`، ويُسلسَل على هيئة `DETACHED_OVER_SPARSE`. وعند فك ترميز column كهذا، يقرأ العميل طول الـ blob وبايتاته، ثم يفك ضغط الـ blob لاستعادة حمولة الـ column الداخلي (راجع [ملاحظة `ColumnBLOB`](#compression-negotiation) في قسم الضغط).

`DETACHED` أحادي الاتجاه: فلا يمكن أن يظهر إلا في كتل النتائج التي يرسلها الخادم، ولا يُطبَّق إلا على عمود كامل من المستوى الأعلى، ولا يظهر أبدًا كأحد أنواع العناصر داخل `Tuple`. وخلافًا للأنواع الأخرى، فإنه يجعل العمود في وقت التشغيل من النوع `ColumnBLOB` بدلًا من عمود من النوع المُعلَن، ولذلك لا يقبله المستقبِل إلا إذا كان مسار المعالجة لديه يعيد تحويل الـ blob إلى صيغته الأصلية. ولهذا يرفض الخادم `DETACHED` في أي كتلة يرسلها العميل، سواء أكانت بيانات جدول خارجي أم قيمًا مفردة أم بيانات إدراج، ويُرجع الخطأ `INCORRECT_DATA`.

### بدائل الكتلة

تشترك جميع حزم عائلة Data في تنسيق wire نفسه الخاص بالكتلة، ولا تختلف البدائل فيما بينها إلا في عدد الأعمدة والصفوف:

| البديل | num\_columns | num\_rows | الغرض |
| - | - | - | - |
| كتلة الترويسة (كتلة الترويسة) | N > 0 | 0 | تُعلن عن مخطط النتيجة (أسماء الأعمدة وأنواعها). |
| كتلة النتيجة (كتلة النتيجة) | N > 0 | M > 0 | صفوف النتيجة الفعلية. |
| الكتلة الفارغة (كتلة فارغة) | 0 | 0 | قيمة حارسة؛ تدل على نهاية الإدخال من جهة العميل، وتُستخدم وسمًا للحدود من جهة الخادم. |

### أمثلة على مستوى البايت

جميع الأمثلة في هذا القسم مأخوذة من **مسار حزمة Data عبر TCP**، ولذلك تتضمن البادئة `BlockInfo` والبايت `has_custom_serialization`. أما عبر `FORMAT Native` فتكون الكتل نفسها أقصر، وسنورد الصيغة المختصرة المكافئة حيثما كان ذلك مفيدًا.

كتلة فارغة (مع BlockInfo)، حجمها الإجمالي 8 بايتات:

```text theme={null}
01 00                   BlockInfo: field_id=1, is_overflows=0
02 FF FF FF FF          BlockInfo: field_id=2, bucket_number=-1
00                      BlockInfo terminator
00                      num_columns = 0
00                      num_rows = 0
```

تُعلن كتلة الترويسة الخاصة بالاستعلام `SELECT 1` عن column واحد باسم `"1"` من النوع `UInt8`، دون أي صفوف. وعندما يكون إصدار البروتوكول ≥ 54454، يُضاف البايت `has_custom_serialization`:

```text theme={null}
01 00                   BlockInfo: is_overflows = 0
02 FF FF FF FF          BlockInfo: bucket_number = -1
00                      BlockInfo terminator
01                      num_columns = 1
00                      num_rows = 0
01 "1"                  Column[0].name = "1"
05 "UInt8"              Column[0].type = "UInt8"
00                      Column[0].has_custom_serialization = 0
                        Column[0].data: no bytes (num_rows = 0)
```

كتلة النتائج للاستعلام نفسه، وتحتوي على صف واحد:

```text theme={null}
01 00                   BlockInfo: is_overflows = 0
02 FF FF FF FF          BlockInfo: bucket_number = -1
00                      BlockInfo terminator
01                      num_columns = 1
01                      num_rows = 1
01 "1"                  Column[0].name = "1"
05 "UInt8"              Column[0].type = "UInt8"
00                      Column[0].has_custom_serialization = 0
01                      Column[0].data: one UInt8 byte = 1
```

عند استخدام `FORMAT Native` (الـ revision `0`)، لا تتضمّن كتلة النتيجة نفسها `BlockInfo` ولا البايت `has_custom_serialization`، إذ يبلغ حجم `SELECT 1 FORMAT Native` ‏11 بايت:

```text theme={null}
01                      num_columns = 1
01                      num_rows = 1
01 "1"                  Column[0].name = "1"
05 "UInt8"              Column[0].type = "UInt8"
01                      Column[0].data: one UInt8 byte = 1
```

(لا تُنتج النتيجة الخالية من الصفوف، كالكتلة التي تقتصر على الترويسة، أي بايتات على الإطلاق عبر `FORMAT Native`: إذ لا يُصدر تنسيق الإخراج هذا كتلًا فارغة.)

## revision البروتوكول وصيغة Native

يتحدد شكل دفق البايتات بصيغة Native قبل كل شيء بحسب **revision البروتوكول** التي يعمل بها كلٌّ من الكاتب والقارئ. ولا تَرِد revision في أي موضع من البايتات نفسها — إذ لا يوجد حقل لـ revision في تنسيق النقل — ومع ذلك فهي التي تحدد ما إذا كانت عدة ميزات ستظهر من الأساس. ولذلك يتعين على أداة فك الترميز أن تعرف revision التي كُتبت بها الحمولة قبل أن تتمكن من تحليلها. وبما أن revision غير موجودة في الدفق، فلا بد أن يتفق القارئ والكاتب عليها بوسيلة أخرى.

وrevision قيمة واحدة من النوع `UInt64`، يستقبلها كلٌّ من `NativeWriter` و`NativeReader` وسيطًا في المُنشئ. ويسميها الكاتب `client_revision` بينما يسميها القارئ `server_revision`، لكنها الرقم نفسه في الحالتين. وأحدث revision يعرفها هذا الإصدار هي `DBMS_TCP_PROTOCOL_VERSION` (راجع `src/Core/ProtocolDefines.h`).

### ما الذي تتحكم فيه المراجعة (revision)

تخضع كل ميزة لعتبة من نوع `DBMS_MIN_REVISION_WITH_*`. لا يُصدر الكاتب الميزة إلا عندما تبلغ مراجعته (revision) تلك العتبة، ويبحث عنها القارئ وفق القاعدة ذاتها تمامًا، فيبقى الطرفان متزامنين؛ أما إذا أخطأت في تحديد المراجعة (revision) على أي من الجانبين فسيفقدان التزامن. البوابات المهمة لصيغة Native هي:

| الميزة | ثابت العتبة | المراجعة (revision) | الأثر عند الوقوع دون العتبة |
| - | - | - | - |
| بادئة `BlockInfo` | (أي قيمة `> 0`) | `1` | تُحذف بادئة [`BlockInfo`](#blockinfo) كليًا؛ وتتكون الكتلة من `num_columns` و`num_rows` والأعمدة (columns) فقط. |
| بايت `has_custom_serialization` | `DBMS_MIN_REVISION_WITH_CUSTOM_SERIALIZATION` | `54454` | يُحذف بايت [`has_custom_serialization`](#column-wire-layout) الخاص بكل عمود (column)؛ ويستخدم كل عمود (column) التسلسل الافتراضي (دون الأشكال المتناثرة أو المتماثلة أو المنفصلة). |
| `LowCardinality` في تنسيق النقل | `DBMS_MIN_REVISION_WITH_LOW_CARDINALITY_TYPE` | `54405` | حالة خاصة — **لا** تخضع للقاعدة البسيطة لما دون العتبة. يُجرَّد `LowCardinality(T)` إلى النوع الأساسي `T` فقط عندما تكون المراجعة (revision) *غير صفرية* وأقل من `54405`، أو عند فرض التجريد بشكل منفصل. أما المراجعة (revision) `0` فتُبقي عليه. راجع الملاحظة أدناه. |
| تسلسل `Dynamic` / `JSON` بالإصدار V2 | `DBMS_MIN_REVISION_WITH_V2_DYNAMIC_AND_JSON_SERIALIZATION` | `54473` | يستخدم `Dynamic` و`JSON`/`Object` تسلسل V1 (مع المعلمة `max_dynamic_*`) بدلًا من V2. |
| تسلسل `String` بالإزاحات | `DBMS_MIN_REVISION_WITH_STRING_WITH_SIZE_STREAM_SERIALIZATION` | `54492` | دون العتبة، تستخدم بيانات أعمدة (columns) `String` تخطيط القيمة المفردة (بادئة طول `VarUInt` + بايتات خام لكل صف) بدلًا من [تخطيط الإزاحات](#string-type) (إزاحات بايتات تراكمية بنوع `UInt64`، تليها جميع البيانات متسلسلة). يعتمد ذلك على المراجعة (revision) وحدها، دون أي علامة لكل عمود (column) في تنسيق النقل. عند العتبة أو فوقها، ينطبق تخطيط الإزاحات أيضًا على `String` المتداخل داخل الأنواع المركبة (`Array` و`Nullable` و`Map` و`Tuple` و`Variant` و`Dynamic` و`JSON`)، لكنه **لا** ينطبق على قاموس عمود (column) `LowCardinality`: إذ يُكتب قاموس `LowCardinality` دائمًا بالتسلسل المتداخل الافتراضي، فتحتفظ قيم `String` فيه بتخطيط القيمة المفردة (ويشمل ذلك `LowCardinality(String)` و`LowCardinality(Nullable(String))`). أما صيغة `Buffers` فليست جزءًا من البروتوكول، وتستخدم دائمًا تخطيط القيمة المفردة. |
| إدارة إصدارات دوال التجميع | `DBMS_MIN_REVISION_WITH_AGGREGATE_FUNCTIONS_VERSIONING` | `54452` | تُكتب حالة `AggregateFunction` دون إصدار مضمَّن. |
| `skip_degree` في حالة `quantileDeterministic` | `DBMS_MIN_REVISION_WITH_QUANTILE_DETERMINISTIC_SKIP_DEGREE` | `54491` | تُكتب حالات `AggregateFunction(quantileDeterministic, ...)` (وكذلك صيغتا `quantiles`/`median` للدالة نفسها) بإصدار الحالة `0`، دون الحقل اللاحق `UInt8 skip_degree`. راجع الملاحظة ضمن [الأسماء البديلة للأنواع](#type-aliases). |
| عينة تجزئات 64 بت في حالة `uniq` | `DBMS_MIN_REVISION_WITH_UNIQ_STATE_VERSION_1` | `54493` | تُكتب حالات `AggregateFunction(uniq, ...)` بإصدار الحالة `0`: دون بايت الأعلام البادئ ودون العينة اللاحقة من تجزئات 64 بت. راجع الملاحظة ضمن [الأسماء البديلة للأنواع](#type-aliases). |
| `out_of_order_buckets` في `BlockInfo` | `DBMS_MIN_REVISION_WITH_OUT_OF_ORDER_BUCKETS_IN_AGGREGATION` | `54480` | لا يُكتب الحقل ذو المعرّف `3` في `BlockInfo` (راجع [BlockInfo](#blockinfo)). |
| التجهيز المتوازي للكتل (`DETACHED`) | `DBMS_MIN_REVISON_WITH_PARALLEL_BLOCK_MARSHALLING` | `54478` | لا تُغلَّف الأعمدة (columns) أبدًا في `ColumnBLOB`؛ ولا تظهر الأنواع `DETACHED` / `DETACHED_OVER_SPARSE` (راجع [kind\_stack](#kind-stack-and-sparse-encoding)). |
| معلمة النوع `DateTime(tz)` | `DBMS_MIN_REVISION_WITH_TIME_ZONE_PARAMETER_IN_DATETIME_DATA_TYPE` | `54337` | تُحذف معلمة المنطقة الزمنية من سلسلة `type` — فيُعلَن عن `DateTime('UTC')` على أنه `DateTime` مجردًا. |

وهذا يجعل الـ revision `0` الترميز الأكثر تحفّظًا في كل شيء تقريبًا: إذ لا يحمل الدفق أي `BlockInfo`، ولا بايت `has_custom_serialization`، ويستخدم الإصدار V1 من `Dynamic`/`JSON`، ولا يتضمن إصدارًا لدوال التجميع، ويحتوي على `DateTime` مجرّد مع حذف معلمة المنطقة الزمنية. وتُستثنى من ذلك إصدارات *حالة* دوال التجميع عند الـ revision `0`: فلا يوجد طرف مقابل يُشتقّ منه الإصدار، إذ تكتب الدفقَ الجهةُ نفسها التي أنتجته وتعيد قراءته (ملف بيانات `StripeLog`، أو نسخة احتياطية لـ `Set`/`Join`، أو ملف بصيغة `Native`). لذا يحتفظ الكاتب بالإصدار المثبّت على النوع (مع التصريح به في سلسلة النوع كالمعتاد) بدلًا من خفض الحالة إلى الإصدار `0`، ولا يُكتب بالإصدار `0` إلا النوع الذي لا يثبّت أي إصدار.

أما `LowCardinality` فهو الاستثناء الوحيد، وهو استثناء مهم. فشرط الكاتب هو `remove_low_cardinality || (client_revision && client_revision < DBMS_MIN_REVISION_WITH_LOW_CARDINALITY_TYPE)`. ويكمن السرّ في الجزء الأول `client_revision &&`: فعندما تساوي قيمة الـ revision `0` تمامًا، يُقيَّم الشرط بأكمله فورًا إلى false وفق مبدأ التقييم المختصر (short-circuit).

لذلك عند الـ revision `0`، وهي القيمة الافتراضية لـ `FORMAT Native`، **لا** يُزال `LowCardinality(T)`، بل تبقى سلسلة نوعه وبادئة الحالة الخاصة بكل كتلة في الدفق، ويعيد القارئ ذو الـ revision `0` قراءتهما مباشرةً. ولا تُطبَّق الإزالة إلا عند revision غير صفرية أقل من `54405`، أو عند فرضها بصرف النظر عن الـ revision.

ويتم هذا الفرض عبر الخيار `remove_low_cardinality`. ولا يضبطه إخراج `FORMAT Native` أبدًا، في حين يضبطه مسار البروتوكول الأصلي عندما تكون `low_cardinality_allow_in_native_format = 0` (القيمة الافتراضية `1`). بعبارة أخرى، يغيّر هذا الإعداد إخراج البروتوكول الأصلي، لكنه لا يؤثر إطلاقًا في `FORMAT Native`.

الخلاصة العملية: قد يحتوي دفق `FORMAT Native` الافتراضي على `LowCardinality` بشكل مشروع، فلا تفترض أنه ميزة غائبة عند الـ revision `0`.

### مصدر الـ revision بحسب طريقة انتقال البيانات

يمكن أن تنتقل بايتات Native ذاتها عبر مسارات مختلفة: بروتوكول TCP الأصلي، أو طلب HTTP، أو ملف على القرص. ولكل مسار طريقته الخاصة في تحديد الـ revision. وثمة أمر ينبغي الانتباه إليه: يُهيَّأ جانبا القراءة والكتابة كلٌّ على حدة، ولذلك قد ينتهي بهما الأمر إلى قيمتَي revision مختلفتين.

#### بروتوكول TCP الأصلي — قيمة متفاوض عليها، في كلا الاتجاهين

في [بروتوكول TCP الأصلي](https://clickhouse.com/docs/ar/resources/develop-contribute/native-protocol/protocol.md)، تُستمد قيمة revision من مصافحة Hello. يرسل العميل `DBMS_TCP_PROTOCOL_VERSION`، ويردّ الخادم بقيمته الخاصة، ومن ثمّ يُجري كل طرف التسلسل وفق **قيمة revision التي أعلنها الطرف الآخر**: إذ يبني الخادم `NativeReader`/`NativeWriter` الخاصين به استنادًا إلى `client_tcp_protocol_version`، في حين يستخدم العميل قيمة `server_revision` التي تلقّاها. لا توجد عملية `min` صريحة، غير أن أيًّا من الطرفين لا يستطيع إرسال ميزة لم يُنفّذها، ولذلك يتحدد سقف كل اتجاه فعليًا بالطرف الأقدم من الطرفين.

عندما يعمل الطرفان على الإصدار الحديث نفسه، يستقر الاتجاهان على قيمة revision نفسها (`DBMS_TCP_PROTOCOL_VERSION`، راجع `src/Core/ProtocolDefines.h`) وتكون جميع البوابات مفعّلة. هذه هي الحالة الشائعة، لكنها ليست مضمونة. فإذا اختلفت إصدارات الطرفين أو كان أحدهما من جهة خارجية، فقد يعمل الاتجاهان بقيمتَي revision مختلفتين، ولذا يجب تقييم البوابات لكل اتجاه على حدة: يكون `BlockInfo` حاضرًا عند أي قيمة revision غير صفرية، أما البقية — بما فيها `has_custom_serialization` — فلا تظهر إلا حين تبلغ قيمة revision الفعلية لذلك الاتجاه الحدود الخاصة بها. فعلى سبيل المثال، الطرف الذي يعلن قيمة revision أقل من `54454` لا يرسل البايت `has_custom_serialization` ولا يستقبله.

#### إخراج `FORMAT Native` — revision 0 افتراضيًا، مع إمكانية رفعها عبر HTTP

تستخدم صيغة *الإخراج* `Native` revision **`0`** افتراضيًا. ويشمل ذلك `SELECT ... FORMAT Native` عبر HTTP، و`INTO OUTFILE ... FORMAT Native`، وإخراج `Native` الذي يكتبه `clickhouse-client`؛ ففي كل حالة من هذه الحالات، يمرّر مصنع الإخراج `FormatSettings::client_protocol_version` مباشرةً إلى `NativeWriter`، الذي يتحكم في بنية التأطير الخاصة بـ `BlockInfo` / `has_custom_serialization` *وكذلك* في تخطيط `String` القائم على الإزاحات. وعليه، فإن استعلام `SELECT ... FORMAT Native` عبر HTTP مع قيمة مرفوعة لـ `client_protocol_version` (≥ `54492`) يكتب التخطيط القائم على الإزاحات، بينما تُبقي revision الافتراضية `0` على التخطيط المحمول الذي تُرمَّز فيه كل قيمة على حدة.

غير أن هذه القيمة الافتراضية ليست نهاية المطاف عند استخدام HTTP، إذ يمكن للعميل رفعها عبر معلمة الاستعلام `?client_protocol_version=<n>`، التي يعاملها معالج HTTP على أنها معلمة محجوزة لا إعداد SQL: فهي تُخزَّن في سياق الاستعلام، ثم تنسخها طبقة الصيغة إلى `FormatSettings`. وعند ضبطها بقيمة مرتفعة بما يكفي، يبدأ إخراج `FORMAT Native` عبر HTTP بتضمين البادئة `BlockInfo` والبايت `has_custom_serialization`، تمامًا كما في مسار TCP — لذا لا تفترض أن حمولة `FORMAT Native` عبر HTTP تكون دائمًا بـ revision `0`.

ولأن المعلمة تُخزَّن في سياق الاستعلام بأكمله، فإنها تصل إلى كل `NativeWriter` يُنشأ لذلك الطلب، لا إلى دفق النتائج وحده. فعلى سبيل المثال، تؤدي عملية كتابة من جهة الخادم مثل `INSERT INTO FUNCTION file('x.native', 'Native', ...) SELECT ...` عبر HTTP مع قيمة مرفوعة لـ `?client_protocol_version=` إلى كتابة الملف بتلك revision. وهذا سلوك قائم منذ زمن طويل (إذ يسبق ظهور تسلسل String القائم على الإزاحات، وينطبق كذلك على `BlockInfo` و`has_custom_serialization`)، وهو غير متناظر: فجهة القراءة (الموضحة أدناه) لا تُرفع revision الخاصة بها بالطريقة نفسها، ولذلك تتعذّر إعادة قراءة مثل هذا الملف عبر `file(...)`. لا تُضمِّن `client_protocol_version` في أي طلب يكتب بيانات يُراد قراءتها لاحقًا. أما `INTO OUTFILE` وإخراج `clickhouse-client` المحلي فلا يتوفر لهما مثل هذا الخيار، ويبقيان عند revision `0`.

#### إدخال `FORMAT Native` — دائمًا بـ revision 0

تُنشئ صيغة *الإدخال* `Native` قارئها `NativeReader` دائمًا عند revision **`0`**. فهي لا تتوقع أبدًا بادئة `BlockInfo`، ولا تقرأ البايت `has_custom_serialization` إطلاقًا، وتفترض دائمًا التسلسل الافتراضي، سواء أكانت تحلّل متن استعلام `INSERT ... FORMAT Native`، أم تقرأ ملف `Native`، أم تستنتج المخطط. ولا يرفع `?client_protocol_version=<n>` قيمة revision الإدخال، إذ لا يقرأ هذه المعلمة سوى جانب الإخراج.

ولأن revision الإدخال يساوي `0` دائمًا، فإن صيغة الإدخال تقرأ دائمًا تخطيط `String` الذي تُرمَّز فيه كل قيمة على حدة؛ أما الدفق المكتوب بتخطيط الإزاحات (عبر بروتوكول TCP الأصلي، أو عبر `SELECT ... FORMAT Native` مع رفع قيمة `client_protocol_version`) فلا يمكن قراءته مجددًا عبر إدخال `FORMAT Native`، وهو عدم التماثل نفسه الذي نجده في تأطير `BlockInfo`.

### تداعيات الكتابة ثم القراءة

بالنسبة إلى `FORMAT Native`، تكون revision الافتراضية `0` في كلا الطرفين. فالبيانات المكتوبة بواسطة `SELECT ... FORMAT Native` عند revision `0` يمكن قراءتها مباشرةً في `INSERT ... FORMAT Native` دون أي مفاجآت.

أما الدفق المُنتَج بـ revision إخراج مرفوعة (`?client_protocol_version=<n>` في `SELECT`) فيحمل بنية التأطير `BlockInfo` و`has_custom_serialization` وتخطيط `String` القائم على الإزاحات، ولا تستطيع صيغة الإدخال قراءة أيٍّ منها لأنها تعمل دائمًا عند revision `0`. لذا لا يكون رفع `client_protocol_version` منطقيًا إلا لمستهلك يتعامل ببروتوكول TCP الأصلي، لا لعملية كتابة ثم قراءة باستخدام `FORMAT Native` عبر HTTP.

أما الملفات فهي الحالة غير المتناظرة. إذ إن رفع `?client_protocol_version=` في عملية `INSERT INTO FUNCTION file(...)` من جهة الخادم (أو `s3`، أو `url`) *يكتب* الملف بتلك revision، في حين أن *قراءة* `file(...)` تحلّل دائمًا عند revision `0`، ولذلك يتعذّر قراءة مثل هذا الملف بعد كتابته. والحل إما أن تحذف `client_protocol_version` من طلب الكتابة ليُكتب الملف بـ revision `0` العادية، وإما أن تنقل البيانات عبر بروتوكول TCP الأصلي، حيث يستخدم كل اتجاه revision المتفاوض عليها أثناء المصافحة.

| القناة | revision الكتابة | revision القراءة | `BlockInfo` / التسلسل المخصّص |
| - | - | - | - |
| حزمة Data في بروتوكول TCP الأصلي | revision التي يُعلنها الطرف الآخر (لكل اتجاه) | revision التي يُعلنها الطرف الآخر (لكل اتجاه) | `BlockInfo` كلما كانت revision `> 0`؛ و`has_custom_serialization` عند `≥ 54454` |
| `SELECT ... FORMAT Native` عبر HTTP | `client_protocol_version` (الافتراضي `0`) | لا ينطبق | فقط إذا رُفع `client_protocol_version` |
| `INSERT ... FORMAT Native` عبر HTTP (في متن الطلب) | لا ينطبق | `0` (دائمًا) | لا يوجد تأطير؛ و`String` دائمًا لكل قيمة على حدة |
| `INSERT INTO FUNCTION file`/`s3`/`url(..., 'Native')` عبر HTTP | `client_protocol_version` (الافتراضي `0`) | `0` (القراءة دائمًا) | عند الكتابة فقط إذا رُفع `client_protocol_version`، ويتعذّر حينها قراءة الملف |
| `INTO OUTFILE` / `clickhouse-client` المحلي مع `FORMAT Native` | `0` | `0` | غير موجود (لكن يُحتفَظ بـ `LowCardinality`، راجع الملاحظة أعلاه) |

> [!NOTE]
>
> **revision البروتوكول مقابل إصدار التسلسل**
>
> لا تخلط بين revision البروتوكول و[إصدار التسلسل](#serialization-version-concept). فـ revision هنا تسري على مستوى الاتصال أو الطلب بأكمله، ولا تظهر أبدًا في البايتات. أما إصدار التسلسل فهو خاص بكل عمود، وتحمله [الأنواع ذات الإصدارات](#versioned-types)، ويُكتب في كل كتلة غير فارغة. فـ revision تحدّد ما إذا كانت ميزة ما موجودة أصلًا، بينما يحدّد إصدار التسلسل، داخل العمود ذي الإصدار، أيّ متغيّر من ترميز ذلك النوع بعينه يأتي بعده.

## أنواع البيانات

يوثّق هذا القسم ترميز wire للأنواع التي يمكن لصيغة Native حملها ضمن `data` الخاصة بالعمود، مقسّمةً إلى أربع عائلات يتزايد فيها تعقيد أداة فك الترميز تدريجيًا. يُعدّ النوعان `AggregateFunction(func, ...)` و`QBit(T, N[, stride])` من أنواع الأعمدة الصالحة في `Native`، إلا أن لكلٍّ منهما حمولة خاصة بالدالة أو بالنوع تقع خارج نطاق هذا القسم؛ وسننبّه إليهما أدناه في المواضع التي قد يُظنّ فيها خطأً أنهما أسماء بديلة.

| العائلة | القسم | عدد التدفقات لكل عمود | الحالة عبر الكتل |
| - | - | - | - |
| ذات عرض ثابت | [الأنواع ذات العرض الثابت](#fixed-width-types) | واحد | لا توجد |
| ذات طول متغير | [الأنواع ذات الطول المتغير](#variable-length-types) | واحد | لا توجد |
| مركّبة (بنية ثابتة) | [الأنواع المركّبة](#composite-types) | متعددة | لا توجد |
| ذات إصدارات / ذات حالة | [الأنواع ذات الإصدارات](#versioned-types) | متعددة | لا توجد في Native wire — بادئة حالة لكل كتلة، تُعاد تهيئتها مع كل كتلة جديدة |

### الأنواع ذات العرض الثابت

تشغل كل قيمة عددًا ثابتًا من البايتات. ويشغل عمود مكوَّن من `M` صفًا `bytes_per_row × M` بايتًا بالضبط في تنسيق النقل، متلاصقةً دون أي فواصل أو حشو.

| سلسلة النوع | البايتات لكل قيمة | القيمة المنطقية | ترميز wire |
| - | - | - | - |
| `UInt8` | 1 | عدد صحيح بلا إشارة بطول 8 بت | بايت خام |
| `UInt16` | 2 | عدد صحيح بلا إشارة بطول 16 بت | Little-endian |
| `UInt32` | 4 | عدد صحيح بلا إشارة بطول 32 بت | Little-endian |
| `UInt64` | 8 | عدد صحيح بلا إشارة بطول 64 بت | Little-endian |
| `UInt128` | 16 | عدد صحيح بلا إشارة بطول 128 بت | Little-endian |
| `UInt256` | 32 | عدد صحيح بلا إشارة بطول 256 بت | Little-endian |
| `Int8` | 1 | عدد بإشارة بطول 8 بت، بتمثيل المتمم الثنائي | بايت خام |
| `Int16` | 2 | عدد بإشارة بطول 16 بت، بتمثيل المتمم الثنائي | Little-endian |
| `Int32` | 4 | عدد بإشارة بطول 32 بت، بتمثيل المتمم الثنائي | Little-endian |
| `Int64` | 8 | عدد بإشارة بطول 64 بت، بتمثيل المتمم الثنائي | Little-endian |
| `Int128` | 16 | عدد بإشارة بطول 128 بت، بتمثيل المتمم الثنائي | Little-endian |
| `Int256` | 32 | عدد بإشارة بطول 256 بت، بتمثيل المتمم الثنائي | Little-endian |
| `Float32` | 4 | IEEE 754 أحادي الدقة | Little-endian |
| `Float64` | 8 | IEEE 754 مزدوج الدقة | Little-endian |
| `BFloat16` | 2 | البتات الـ 16 العليا من `Float32` وفق IEEE 754 | Little-endian |
| `Bool` | 1 | `0x00` = false، `0x01` = true | بايت خام |
| `Date` | 2 | عدد الأيام منذ `1970-01-01` | Little-endian UInt16 |
| `Date32` | 4 | عدد الأيام منذ `1970-01-01` (بإشارة؛ يدعم ما قبل 1970) | Little-endian Int32 |
| `DateTime` | 4 | طابع زمني لـ Unix بالثواني | Little-endian UInt32 |
| `DateTime(tz)` | 4 | مماثل لـ `DateTime`؛ المنطقة الزمنية بيانات وصفية | Little-endian UInt32 |
| `DateTime64(s)` | 8 | عدد tick بمقياس `s` (10^-s ثانية منذ epoch) | Little-endian Int64 |
| `DateTime64(s, tz)` | 8 | مماثل لـ `DateTime64(s)`؛ المنطقة الزمنية بيانات وصفية | Little-endian Int64 |
| `Time` | 4 | مدة زمنية بإشارة بالثواني | Little-endian Int32 |
| `Time64(s)` | 8 | مدة زمنية بإشارة بعدد tick بمقياس `s` | Little-endian Int64 |
| `Interval<Unit>` | 8 | عدد بإشارة؛ والوحدة محددة في سلسلة النوع | Little-endian Int64 |
| `UUID` | 16 | معرّف بطول 128 بت | نصفان من نوع UInt64 بترتيب LE، مع تبديل بايتات كلٍّ منهما (راجع [UUID](#uuid)) |
| `IPv4` | 4 | عنوان IPv4 | Little-endian UInt32 |
| `IPv6` | 16 | عنوان IPv6 | ترتيب بايتات الشبكة، دون تبديل |
| `Enum8` | 1 | عدد بإشارة بطول 8 بت (فهرس قيمة التعداد) | بايت خام |
| `Enum16` | 2 | عدد بإشارة بطول 16 بت (فهرس قيمة التعداد) | Little-endian |
| `Decimal(P, S)` | 4 / 8 / 16 / 32 | `value × 10^S` كعدد صحيح بإشارة؛ يعتمد العرض على P (‏≤9 → 4 B، ≤18 → 8 B، ≤38 → 16 B، ≤76 → 32 B) | عدد صحيح بإشارة بترتيب Little-endian |

#### أنواع الأعداد الصحيحة

تمثّل الأنواع `UInt8`–`UInt256` و`Int8`–`Int256` ترميزات ثنائية مباشرة لقيم الأعداد الصحيحة، إذ تقرأ أداة فك الترميز `bytes_per_row × num_rows` بايت وتفسّرها وفقًا للنوع.

مثال على عمود من النوع `UInt32` يحتوي على القيم `[1, 256, 65536]`:

```text theme={null}
01 00 00 00              row 0: 1
00 01 00 00              row 1: 256
00 00 01 00              row 2: 65536
```

عمود من النوع `Int32` يحتوي على القيم `[-1, 42]`:

```text theme={null}
FF FF FF FF              row 0: -1
2A 00 00 00              row 1: 42
```

#### Float32 و Float64

أعداد ذات فاصلة عائمة ثنائية قياسية وفق معيار IEEE 754: بطول 4 بايتات أحادية الدقة (`binary32`) و8 بايتات بدقة مزدوجة (`binary64`)، وكلاهما بترتيب little-endian. وتُحفَظ قيم NaN و±Infinity و±0.0 والقيم دون الطبيعية (subnormals) كما هي عند كتابتها ثم قراءتها (round-trip)، دون أي تطبيع.

القيمة `1.5` من النوع `Float32` (`0x3FC00000`):

```text theme={null}
00 00 C0 3F              little-endian IEEE 754
```

القيمة `1.5` من النوع `Float64` (`0x3FF8000000000000`):

```text theme={null}
00 00 00 00 00 00 F8 3F  little-endian IEEE 754
```

#### BFloat16

صيغة الفاصلة العائمة الدماغية (brain-floating-point): وهي البتات الـ 16 العليا من `Float32` وفق معيار IEEE 754 — بت واحد للإشارة، و8 بتات للأس، و7 بتات للجزء العشري. تشغل كل قيمة بايتين بترتيب little-endian، وتحمل النمط الخام المؤلف من 16 بت. لاستعادة القيمة العددية، وسّعها مجددًا إلى `Float32` بوضع النمط في النصف الأعلى وتصفير النصف الأدنى (أي تفسير `bits << 16` على أنه `Float32`)؛ وعندئذٍ تُنسَّق القيمة الموسّعة نصيًا بالطريقة نفسها التي يُنسَّق بها `Float32`.

قيمة `BFloat16` ‏`1.5` (النمط `0x3FC0`، وهو النصف الأعلى من `Float32` ‏`0x3FC00000`):

```text theme={null}
C0 3F                    little-endian, widens to Float32 1.5
```

#### Bool

متوافق في تنسيق النقل مع `UInt8`: بايت واحد لكل صف، حيث `0x00` = false و`0x01` = true. غير أن سلسلة النوع في تنسيق النقل هي `Bool` حرفيًا (وليست `UInt8`)، لذا يجب على أداة فك الترميز التي توجّه المعالجة وفقًا لسلسلة النوع أن تتعرّف عليها بشكل مستقل.

عمود من النوع `Bool` يحتوي على القيم `[true, false, true]`:

```text theme={null}
01 00 01
```

#### Date و Date32

يُرمِّز كلاهما التواريخ في صورة عدد صحيح يمثّل عدد الأيام منذ حقبة Unix `1970-01-01`، ولا يتضمن أيٌّ منهما مكوّنًا زمنيًا.

| النوع | البايتات | الترميز | النطاق |
| - | - | - | - |
| `Date` | 2 | UInt16 بترتيب little-endian | من `1970-01-01` إلى `2149-06-06` |
| `Date32` | 4 | Int32 بترتيب little-endian | نطاق واسع بإشارة، ويدعم التواريخ السابقة لعام 1970 |

القيمة `1970-01-02` من النوع `Date` (يوم واحد):

```text theme={null}
01 00                    UInt16 LE = 1
```

قيمة `Date32` ‏`1900-01-01` (‎-25567 يومًا):

```text theme={null}
21 9C FF FF              Int32 LE = -25567
```

#### DateTime

متوافق على مستوى التمثيل الثنائي (wire) مع `UInt32`: طابع زمني لـ Unix بالثواني، يُخزَّن في 4 بايتات بترتيب little-endian. قد يظهر النوع بالصيغة `DateTime` أو `DateTime('Timezone')`؛ ولا تؤثر المنطقة الزمنية إلا في طريقة العرض، فهي ليست جزءًا من القيمة المُرسَلة. وبذلك يُنتج عمودان من النوع `DateTime` بمعلمات منطقة زمنية مختلفة البايتات نفسها للّحظة الزمنية ذاتها. تُزيل أداة فك الترميز لاحقة المعلمة `(...)` وتعالج العمود على أنه `UInt32`.

القيمة `2024-03-15 14:30:00 UTC` من النوع `DateTime('UTC')` (الطابع الزمني `1710513000`):

```text theme={null}
68 5B F4 65              UInt32 LE = 1710513000
```

#### DateTime64(scale\[, timezone])

8 بايت: قيمة Int64 بترتيب little-endian تمثّل عدد وحدات tick بمقياس `10^-scale` ثانية منذ حقبة Unix. تُحدَّد المعلمة `scale` (من 0 إلى 9) ضمن سلسلة النوع، وهي التي تحدد وحدة الوقت:

| المقياس | حجم الـ tick | الاسم الشائع |
| - | - | - |
| 0 | ثانية واحدة | ثوانٍ |
| 3 | ملّي ثانية واحدة | ms |
| 6 | ميكروثانية واحدة | µs |
| 9 | نانوثانية واحدة | ns |

يظهر النوع بالصيغة `DateTime64(s)` (ضمنيًا بالمنطقة الزمنية الافتراضية للخادم) أو `DateTime64(s, 'TimezoneName')` (بمنطقة زمنية صريحة تُستخدم للعرض فقط). تمثّل القيم السالبة وحدات tick سابقة للحقبة.

القيمة `2024-01-15 12:30:45.123 UTC` من النوع `DateTime64(3, 'UTC')` (1705321845123 ms):

```text theme={null}
83 51 1A 0D 8D 01 00 00  Int64 LE = 1705321845123
```

قيمة `DateTime64(0)` ‏`2024-01-15 12:30:45 UTC` (1705321845 ثانية):

```text theme={null}
75 25 A5 65 00 00 00 00  Int64 LE = 1705321845
```

#### Time و Time64(scale)

مدة زمنية بحساب الساعة، لا لحظة زمنية محددة. النوع `Time` عدد ثوانٍ بإشارة، يُخزَّن في 4 بايتات بصيغة Int32 بترتيب little-endian؛ أما `Time64(scale)` فعدد tick بإشارة وفق المقياس العشري المحدد (0–9)، يُخزَّن في 8 بايتات بصيغة Int64 بترتيب little-endian، أي بالبنية الثنائية نفسها المستخدمة في `DateTime64`.

الصيغة النصية هي `[-]HH:MM:SS[.fraction]`، لكن حقل الساعات، على خلاف `DateTime`، **لا** يعود إلى الصفر بعد 24 ساعة، بل يمثّل العدد الإجمالي للساعات وقد يتجاوز 23. الحد الأقصى للمقدار المعروض هو `999:59:59` (أي `3599999` ثانية)؛ وأي مقدار أكبر من ذلك تُعرض ثوانيه عند هذا الحد مع الإبقاء على الكسر المخزَّن كما هو، فعدد tick قدره `3700000.25` مثلًا يُعرض بالشكل `999:59:59.250000`. أما عملية `CAST` فتُقيّد القيمة المخزَّنة بأكبر tick يمكن تمثيله في المقياس الهدف (`999:59:59.999999` في حالة `Time64(6)`)، لا بأكبر ثانية كاملة، في حين قد تُخلّف العمليات الحسابية في العمود عدد tick خارج النطاق لا يُطبَّق عليه الحد إلا عند العرض. ولا يؤثر شيء من ذلك في البايتات المنقولة، فهي مجرد العدد الصحيح بإشارة.

القيمة `45296` من النوع `Time` (`12:34:56`):

```text theme={null}
F0 B0 00 00              Int32 LE = 45296
```

قيمة `Time64(3)` هي `45296789` tick (`12:34:56.789`):

```text theme={null}
95 2C B3 02 00 00 00 00  Int64 LE = 45296789
```

> [!NOTE]
>
> النوعان `Time` و`Time64` تجريبيان، ويتطلّب استخدامهما ضبط `allow_experimental_time_time64_type = 1` على الخادم.

#### Interval

`Interval<Unit>` — `IntervalSecond` و`IntervalMinute` و`IntervalHour` و`IntervalDay` و`IntervalWeek` و`IntervalMonth` و`IntervalQuarter` و`IntervalYear` و`IntervalNanosecond` وغيرها. تشترك جميع الوحدات في ترميز wire واحد: يُخزَّن العدد بوصفه Int64 بإشارة (signed) بطول 8 بايت وبترتيب little-endian. ولا تظهر الوحدة **إلا** في سلسلة النوع، فهي لا تغيّر بايتات wire ولا الشكل النصي، الذي يقتصر على العدد الصحيح وحده. ويكفي مسار واحد في أداة فك الترميز للتعامل مع جميع الوحدات.

القيمة `5` من النوع `IntervalDay`:

```text theme={null}
05 00 00 00 00 00 00 00  Int64 LE = 5
```

#### UUID

16 بايت لكل قيمة. ترميز wire **ليس** البايتات القياسية الستة عشر بترتيب big-endian، بل تُعكس بايتات كلٍّ من النصفين (8 بايت لكل منهما) على حدة.

النموذج المنطقي هو معرّف بطول 128 بت بصيغته النصية القياسية `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`، حيث تُكتب البايتات عادةً بترتيب big-endian. أما نموذج النقل فيأخذ هذه البايتات القياسية الستة عشر، ويقسمها إلى نصفين طول كلٍّ منهما 8 بايت، ثم يكتب كل نصف بترتيب little-endian:

* البايتات المنقولة من 0 إلى 7 = البايتات القياسية من 0 إلى 7 بترتيب معكوس.
* البايتات المنقولة من 8 إلى 15 = البايتات القياسية من 8 إلى 15 بترتيب معكوس.

معرّف UUID ‏`550e8400-e29b-41d4-a716-446655440000`:

```text theme={null}
Canonical bytes (16):    55 0E 84 00 E2 9B 41 D4  A7 16 44 66 55 44 00 00

Wire bytes:
D4 41 9B E2 00 84 0E 55  high half byte-reversed
00 00 44 55 66 44 16 A7  low half byte-reversed
```

يظهر معرّف UUID الصفري (المكوّن بالكامل من أصفار) بالشكل نفسه في كلا التمثيلين.

#### IPv4 وIPv6

نوعان من العناوين مترابطان، لكن لكلٍّ منهما طريقة ترميز مختلفة.

يشغل `IPv4` أربعة بايتات، ويُرمَّز على هيئة UInt32 بترتيب little-endian يحمل العنوان المعياري ذا 32 بت (أي القيمة `(a << 24) | (b << 16) | (c << 8) | d` المستخرجة من `a.b.c.d`). وبذلك تكون البايتات المنقولة (wire bytes) هي بايتات ترتيب الشبكة بترتيب معكوس.

`192.168.1.10` (القيمة المعيارية ذات 32 بت هي `0xC0A8010A`):

```text theme={null}
0A 01 A8 C0              Little-endian UInt32
```

يشغل `IPv6` مساحة 16 بايت، ويُكتب **كما هو بترتيب بايتات الشبكة** دون أي تبديل، أي بترتيب البايتات نفسه الذي تُنتجه `inet_pton(AF_INET6, ...)`.

`2001:db8::1`:

```text theme={null}
20 01 0D B8 00 00 00 00  network bytes 0..7
00 00 00 00 00 00 00 01  network bytes 8..15
```

هذا التباين مقصود: إذ يُخزَّن IPv4 على هيئة `u32` لتيسير العمليات الحسابية وإجراء استعلامات نطاق موجزة، في حين يحتفظ IPv6 بتخطيط ترتيب بايتات الشبكة (network order) المعتمد في معظم واجهات برمجة التطبيقات الخاصة بالشبكات.

#### Enum8 و Enum16

متوافقان على مستوى التمثيل الثنائي (wire) مع `Int8` و`Int16` على الترتيب: بايت واحد أو بايتان لكل صف، مع تمثيل المتمم الثنائي (two's complement) بترتيب little-endian في النوع ذي 16 بت. ويرد التعيين الكامل للقيم ضمن سلسلة النوع:

```text theme={null}
Enum8('active' = 1, 'inactive' = 2, 'banned' = -1)
Enum16('a' = 1, 'b' = 30000)
```

قد تُزيل أداة فك الترميز لاحقة المعلمات `(...)` وتتعامل مع القيمة على أنها `Int8` / `Int16`، إذ إن البايتات المنقولة ليست سوى الفهرس العددي الصحيح. أما العميل الذي يعرض الوسم، فيستخرج خريطة `'name' = value` من سلسلة النوع ويحتفظ بها إلى جانب العمود، لأن العدد الصحيح وحده لا يكفي لاستعادة الوسم. ويعرض الإخراج النصي الوسم (`active`) بدلًا من الفهرس، محاطًا بعلامتي اقتباس مفردتين (`'active'`) عندما يكون الـ enum متداخلًا ضمن نوع مركّب. ولمّا كانت الخريطة لا تُستعاد من عمود الأعداد الصحيحة، فلا بد من الاحتفاظ بها في حالة أنواع enum المتداخلة، مثل `Array(Enum8(...))` أو `Map(Enum16(...), V)`.

عمود من النوع `Enum8('active' = 1, 'inactive' = 2)` يحتوي على `[active, inactive, active]`:

```text theme={null}
01 02 01
```

قيمة من النوع `Enum16(...)` تساوي `30000`:

```text theme={null}
30 75                    Int16 LE = 30000
```

#### Decimal(P, S)

عدد صحيح ذو إشارة مضروب في قوة للعدد 10. تُحدِّد **الدقة** `P` عرض العدد الصحيح بالبايت ضمنيًا، أما **المقياس** `S` فهو الأس السالب (أي عدد الأرقام بعد الفاصلة العشرية). وكلاهما مُضمَّن في سلسلة النوع.

| الدقة (P) | العدد الصحيح الأساسي | البايتات |
| - | - | - |
| 1 ≤ P ≤ 9 | Int32 | 4 |
| 10 ≤ P ≤ 18 | Int64 | 8 |
| 19 ≤ P ≤ 38 | Int128 | 16 |
| 39 ≤ P ≤ 76 | Int256 | 32 |

ترميز wire هو العدد الصحيح الأساسي ممثَّلًا بالمتمم الثنائي بترتيب little-endian، أما القيمة العشرية المنطقية فهي `wire_integer × 10^(-S)`.

يُصدر ClickHouse دائمًا `Decimal(P, S)` أيًّا كانت طريقة التصريح عن النوع؛ إذ تُوحَّد `Decimal32(S)` و`Decimal64(S)` وغيرها جميعًا إلى `Decimal(P, S)` في تنسيق النقل (مع ضبط `P` على الحد الأقصى الطبيعي للعرض: 9، 18، 38، 76). وبالتالي فإن أداة فك الترميز التي لا تتعرّف إلا على `Decimal(P, S)` تغطي جميع الصيغ التي يُصدرها الخادم.

القيمة `123.4567` من النوع `Decimal(9, 4)` ← العدد الصحيح الأساسي `1234567`:

```text theme={null}
87 D6 12 00              Int32 LE = 1234567
```

القيمة `-1.5` من النوع `Decimal(18, 1)` ← العدد الصحيح الأساسي `-15`:

```text theme={null}
F1 FF FF FF FF FF FF FF  Int64 LE = -15
```

القيمة `123.4567` من النوع `Decimal(38, 4)` (16 بايت إجمالًا):

```text theme={null}
87 D6 12 00 00 00 00 00 00 00 00 00 00 00 00 00
```

#### Nothing

لا يحمل النوع `Nothing` أي قيم. ولا يظهر عمليًا إلا بوصفه النوع الداخلي لـ `Nullable(Nothing)`، وهو ما يُرجعه الخادم لتعبير مثل `SELECT NULL` لا قيمة صالحة له سوى غياب القيمة. ومن الناحية المفاهيمية، فهو نوع وحدة (unit type).

في تنسيق النقل، يشغل هذا النوع **بايتًا نائبًا واحدًا لكل صف** بالضبط. يُصدر الخادم حرف ASCII `'0'` (`0x30`)، لكن أداة إلغاء التسلسل تتجاهل هذه البايتات؛ فمحتواها غير محدد، ويجب ألا تعتمد أدوات فك الترميز على أي قيمة بعينها. ولأن عدد البايتات المكتوبة يساوي `num_rows × 1`، فإن قيمة `num_rows` في ترويسة العمود تحدد وحدها مقدار البيانات الواجب قراءتها.

يحافظ تخصيص بايت لكل صف على ثبات خاصية الكتلة: إذ يمتد كل عمود على طول يمكن اشتقاقه من `num_rows`، فتواصل أدوات فك الترميز الفحص قُدُمًا دون الحاجة إلى بادئات طول لكل خلية. ولأن النوع المحيط `Nullable` يُبلغ دائمًا عن كل موضع بأنه NULL، فلا تُفحص العناصر النائبة أبدًا.

عمود من النوع `Nullable(Nothing)` يحتوي على 3 صفوف (جميعها NULL):

```text theme={null}
01 01 01                 null map: 1, 1, 1 (three NULLs)
30 30 30                 Nothing placeholder bytes (one per row)
```

بادئة null-map هي بنية التأطير القياسية لـ `Nullable` (راجع [Nullable](#nullable))؛ أما البايتات الثلاثة الداخلية فتمثّل حمولة `Nothing` التي تتخطاها أداة فك الترميز.

### الأنواع ذات الطول المتغير

تحمل كل قيمة طولها الخاص بها في تنسيق النقل.

#### String

سلسلة النوع: `String`. لعمود `String` تخطيطا wire، ويُختار أحدهما وفقًا لـ revision البروتوكول (راجع [ما الذي تتحكم فيه الـ revision](#what-the-revision-gates)). والـ revision هي القيمة المتفاوض عليها في بروتوكول TCP الأصلي، أو قيمة `client_protocol_version` في حالة إخراج `FORMAT Native` (القيمة الافتراضية `0`)؛ أما الإدخال بصيغة `FORMAT Native` فيكون دائمًا بالـ revision `0`، في حين تستخدم صيغة `Buffers` دائمًا التخطيط القائم على كل قيمة على حدة.

**التخطيط القائم على كل قيمة على حدة** — يُستخدم عندما تكون الـ revision أقل من `DBMS_MIN_REVISION_WITH_STRING_WITH_SIZE_STREAM_SERIALIZATION` (`54492`)، ويشمل ذلك الـ revision الافتراضية `0` لـ `FORMAT Native`: ويتألف من سلسلة تضم `num_rows` من تسلسلات البايتات، كلٌّ منها مسبوق بطوله:

```text theme={null}
[VarUInt: byte_length] [byte_length bytes: raw value]
[VarUInt: byte_length] [byte_length bytes: raw value]
...
```

لا توجد فواصل بين الصفوف باستثناء بادئات الطول، ولا توجد حالة على مستوى الصف. وتُمثَّل السلسلة الفارغة ببايت واحد هو `0x00`. ويبلغ إجمالي عدد البايتات التي يشغلها العمود `Σ (varuint_size(len_i) + len_i)` محسوبًا على جميع الصفوف.

مثال لعمود يضم 3 سلاسل نصية `["ab", "", "c"]` (بإجمالي 6 بايتات):

```text theme={null}
02 61 62                 row 0: length 2, "ab"
00                       row 1: length 0, empty
01 63                    row 2: length 1, "c"
```

**تخطيط الإزاحات** — في الـ revision `54492` وما بعده: دفقان متتاليان، أولهما إزاحات البايتات التراكمية، ثم تليها جميع البيانات:

```text theme={null}
[UInt64 × num_rows: cumulative byte offset (end of each value), little-endian]
[last offset bytes: all values concatenated, no separators]
```

تُرسَل قيم الإزاحة كما هي، تمامًا كما يُرسل `Array` قيم الإزاحة الخاصة به عبر البروتوكول الأصلي. تمثّل الإزاحة `i` موضع نهاية القيمة `i` في كتلة البيانات (blob)، ومن ثَمّ يكون حجم القيمة `i` هو `offset[i] - offset[i-1]` (حيث `offset[-1] = 0`)، وتساوي الإزاحة الأخيرة الحجم الكلي لكتلة البيانات. تقرأ أداة فك الترميز `8 × num_rows` بايت من قيم الإزاحة، ثم تقرأ كتلة البيانات كاملةً دفعةً واحدة، مستخدمةً الإزاحة الأخيرة طولًا دقيقًا لها، وهذا يتيح حجز المخزن المؤقت مسبقًا بدقة والنسخ المجمّع بدلًا من التحليل صفًّا صفًّا، دون الحاجة إلى أي تحويل من الأحجام إلى قيم الإزاحة. أما العمود الذي يكون فيه `num_rows = 0` فلا يُضيف أي بايتات إلى أيٍّ من الدفقين.

السلاسل النصية الثلاث نفسها `["ab", "", "c"]` (‏27 بايت إجمالًا):

```text theme={null}
02 00 00 00 00 00 00 00  row 0 offset: 2  (end of "ab")
02 00 00 00 00 00 00 00  row 1 offset: 2  (end of "", so unchanged)
03 00 00 00 00 00 00 00  row 2 offset: 3  (end of "c")
61 62 63                 "ab" + "" + "c"
```

عندما يكون تخطيط الإزاحات (offsets layout) مفعّلًا، فإنه ينطبق على كل `String` في الكتلة، بما في ذلك `String` المتداخل داخل الأنواع المركّبة (`Array(String)`، و`Nullable(String)`، و`Map`، و`Tuple`، و`Variant`، و`Dynamic`، و`JSON`)، باستثناء حالة واحدة: يُكتب قاموس عمود [`LowCardinality`](#lowcardinality) دائمًا بالتسلسل المتداخل الافتراضي، ولذلك تظل قيم `String` فيه محتفظة بالتخطيط لكل قيمة (per-value layout) في جميع قيم revision. ويشمل ذلك `LowCardinality(String)`، و`LowCardinality(Nullable(String))`، وأي قاموس `LowCardinality` آخر يحتوي على قيم String.

في كلا التخطيطين، يتعامل `String` في ClickHouse مع البيانات بوصفها بايتات لا نصوصًا: فلا يُشترط أن تكون بترميز UTF-8 صالح، ويمكن أن تحتوي القيمة على أي بايتات، بما فيها محرف NUL المضمّن. لذا، فإن أداة فك الترميز التي تحوّل القيم إلى نوع سلسلة UTF-8 يجب عليها إما أن تتحقق من صلاحيتها عند القراءة، أو أن تمرّر البايتات الخام كما هي إلى الشيفرة المستدعية.

#### FixedString(N)

سلسلة النوع: `FixedString(N)`، حيث `N` عدد صحيح موجب (مثل `FixedString(16)`). يتكوّن العمود من `N × num_rows` بايت خام بالضبط، دون بادئات طول ودون فواصل. تستخرج أداة فك الترميز قيمة `N` من سلسلة النوع، ثم تقرأ هذا العدد من البايتات لكل صف.

عندما يُدرج استعلام SQL قيمة أقصر من `N` بايت (مثل `CAST('abc' AS FixedString(5))`)، يُكمل الخادم القيمة من اليمين ببايتات NUL (`0x00`) حتى تبلغ الطول المُعلَن. وتُعدّ بايتات الحشو هذه جزءًا من القيمة المخزَّنة، وتُرسَل في تنسيق النقل كما هي؛ أما إزالتها فمسؤولية جهة العميل. وكما هو الحال مع `String`، فإن `FixedString(N)` أقرب إلى مصفوفة بايتات منه إلى نص، ويُستخدم عادةً للمعرّفات ثابتة العرض، أو بايتات العناوين، أو ملخصات التجزئة.

قيمتان من النوع `FixedString(3)` هما `["abc", "de\0"]` (بإجمالي 6 بايتات):

```text theme={null}
61 62 63                 row 0: 3 bytes, "abc"
64 65 00                 row 1: 3 bytes, "de" + NUL padding
```

مقارنة بين نوعَي string:

| الخاصية | `String` | `FixedString(N)` |
| - | - | - |
| بادئة طول لكل صف | نعم (VarUInt)، أو دفق إزاحات منفصل بدءًا من revision `54492`+ | لا |
| حجم الصف | متغير | `N` بايت بالضبط |
| إجمالي بايتات العمود | متغير | `N × num_rows` |
| الحشو ببايتات NUL | لا ينطبق | يحشوه الخادم من اليمين |
| يُتوقَّع ترميز UTF-8 | عادةً (دون إلزام) | لا (يُعامَل كبايتات خام) |
| معلمة النوع | لا يوجد | عدد صحيح `N` إلزامي |

### الأنواع المركبة

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

وتشترك هذه الأنواع في ثلاث خصائص بنيوية:

* **شكل ثابت لكل مخطط.** تتحدد البنية بالكامل من سلسلة النوع وقت فك الترميز. فالنوع `Array(UInt32)` له دائمًا تخطيط الدفقات نفسه في كل كتلة.
* **لا بادئة إصدار خاصة بها.** لا يضيف الغلاف المركب بحد ذاته أي بايت إصدار؛ إذ يظل تأطيره (الإزاحات، وnull-map، ودفقات العناصر) مستقرًا عبر إصدارات ClickHouse. ينطبق هذا على *الغلاف* فقط — راجع الملاحظة الخاصة بمرحلة البادئة أدناه بشأن الأنواع الداخلية ذات الإصدارات.
* **لا حالة خاصة بها عبر الكتل.** تأطير الغلاف ذاتي الوصف بالكامل في كل كتلة؛ وأي مسألة تتعلق بالحالة عبر الكتل مصدرها نوع داخلي ذو إصدار، لا الغلاف.

الأنواع المركبة تعاودية، أي أن النوع الداخلي قد يكون هو نفسه نوعًا مركبًا.

**مرحلة البادئة قبل دفقات البيانات.** تجري قراءة العمود على مرحلتين بهذا الترتيب: **مرحلة بادئة الحالة** ثم **مرحلة دفقات البيانات**. لا يملك الغلاف المركب بايتات بادئة خاصة به، لكنه *يفوّض* مرحلة البادئة إلى التسلسل الداخلي قبل كتابة أيٍّ من دفقات بياناته: فالمكوّن `SerializationArray` ينفّذ مرحلة البادئة لنوعه الداخلي قبل كتابة إزاحات المصفوفة، وكذلك تفعل الأنواع `Tuple` و`Map` و`Nested` و`Nullable` عبر تسلسلات عناصرها (إذ ينفّذ `Nullable` البادئة الداخلية قبل null map الخاصة به).

ولذلك، عندما يغلّف نوع مركب [نوعًا ذا إصدار/حالة](#versioned-types) (`LowCardinality` أو `Variant` أو `Dynamic` أو `JSON`)، تُصدَر بادئة الإصدار/الحالة الخاصة بذلك النوع الداخلي *أولًا*، قبل إزاحات الغلاف وحمولة عناصره. فعلى سبيل المثال، يُرتَّب `Array(LowCardinality(String))` على النحو التالي: `[LowCardinality state prefix]` ← `[array offsets]` ← `[flattened LowCardinality element payload]`، لا بالإزاحات أولًا.

وأداة فك الترميز التي تقرأ الإزاحات قبل تنفيذ مرحلة البادئة الداخلية ستفقد التزامن مع أي نوع مركب يحتوي على `LowCardinality` أو `Variant` أو `Dynamic` أو `JSON`. أما إذا كان كل نوع داخلي نوعًا طرفيًا بسيطًا أو نوعًا مركبًا آخر بلا إصدار، فلا تُصدر مرحلة البادئة أي بايتات، وينطبق الوصف الوارد أدناه، القائم على البدء بالإزاحات، كما هو تمامًا.

#### Nullable(T)

سلسلة النوع: `Nullable(InnerType)`. أمثلة: `Nullable(UInt32)`، `Nullable(String)`، `Nullable(FixedString(16))`، `Nullable(DateTime('UTC'))`.

على غرار الأنواع المركّبة الأخرى، يُحيل `Nullable` [مرحلة البادئة](#composite-types) إلى التسلسل الداخلي للنوع المضمَّن قبل كتابة خريطة القيم الفارغة (null map): فإذا كان النوع الداخلي ذا إصدار، تُرسَل بادئة الحالة الخاصة به **أولًا**. ولذلك يبدأ `Nullable(Tuple(LowCardinality(String)))` ببادئة الحالة الخاصة بـ `LowCardinality`، لا بخريطة القيم الفارغة. أما إذا كان النوع الداخلي نوعًا طرفيًا (leaf) أو نوعًا آخر غير ذي إصدار، فلا تُرسِل مرحلة البادئة أي بايتات.

يتألف تخطيط wire من مرحلة البادئة الداخلية (وتكون فارغة ما لم يكن النوع الداخلي ذا إصدار)، يليها دفقان متتاليان تتقدّمهما خريطة القيم الفارغة (null-map):

```text theme={null}
[inner type's state prefix]   empty for leaf/non-versioned inners; emitted first when the inner is versioned
[null-map stream]             num_rows × UInt8
[values stream]               inner type's encoding for num_rows values
```

يبلغ حجم null-map ‏`num_rows` بايت بالضبط، أي بايت واحد لكل صف:

| قيمة البايت | المعنى |
| - | - |
| `0x00` | توجد قيمة في هذا الصف. |
| غير صفرية (الشكل القياسي `0x01`) | القيمة NULL، والبايتات المقابلة لها في دفق القيم مجرد عنصر نائب. |

يحتوي دفق القيم على الترميز القياسي للنوع الداخلي لـ**جميع** الصفوف البالغ عددها `num_rows`، بما فيها مواضع القيم الفارغة. وعلى أداة فك الترميز أن تقرأ بايتات العنصر النائب في مواضع القيم الفارغة كي تتقدّم في الدفق، لكن عليها الرجوع إلى null-map قبل تفسير أي قيمة بعينها. ونظرًا لأن المُرسِلين قد يكتبون أي بايتات في مواضع القيم الفارغة، فلا يجوز لأدوات فك الترميز الاعتماد على قيمة محددة للعنصر النائب.

قيم العنصر النائب بحسب عائلة النوع الداخلي:

| عائلة النوع الداخلي | العنصر النائب في موضع القيمة الفارغة |
| - | - |
| ثابتة العرض (UInt/Int/Float/DateTime/UUID/إلخ) | بايتات مُهيّأة بالأصفار بعرض النوع |
| `String` | سلسلة فارغة — بايت واحد `0x00` |
| `FixedString(N)` | `N` بايت صفري |
| `Array(T)` | مصفوفة فارغة — تتقدّم الإزاحات بمقدار صفر |
| `Tuple(T1, T2, ...)` | يستخدم كل عنصر العنصر النائب الخاص بنوعه |

يمكن أن يرد `Nullable(T)` داخل `Array` و`Tuple` و`Map` و`Nested`، ومن الشائع استخدام `Array(Nullable(T))` و`Tuple(Nullable(T1), T2)`. غير أنه لا يمكن تداخل قابلية القيم الفارغة مع نفسها، إذ يرفض الخادم `Nullable(Nullable(T))`.

مثال على `Nullable(UInt8)` يضم ثلاثة صفوف `[5, NULL, 9]` (بإجمالي 6 بايتات):

```text theme={null}
00 01 00                 null-map: present, null, present
05 00 09                 values:   5, placeholder, 9
```

عمود من النوع `Nullable(String)` يحتوي على ثلاثة صفوف `["hello", NULL, "world"]` (بإجمالي 15 بايت):

```text theme={null}
00 01 00                 null-map
05 'h' 'e' 'l' 'l' 'o'   row 0: "hello"
00                       row 1: placeholder (empty string)
05 'w' 'o' 'r' 'l' 'd'   row 2: "world"
```

#### Array(T)

سلسلة النوع: `Array(InnerType)`. أمثلة: `Array(UInt32)` و`Array(String)` و`Array(Nullable(UInt32))` و`Array(Array(UInt8))`.

يتكوّن تخطيط wire من [مرحلة البادئة](#composite-types) الخاصة بالنوع الداخلي (وتكون فارغة ما لم يكن النوع الداخلي ذا إصدار)، يليها دفقان متتاليان، أولهما دفق الإزاحات:

```text theme={null}
[inner type's state prefix]   empty for leaf/non-versioned inners; emitted first when the inner is versioned
[offsets stream]              num_rows × UInt64 LE
[values stream]               inner type's encoding for offsets[num_rows - 1] values
```

يتكوّن دفق الإزاحات من `num_rows` قيمة UInt64 بالضبط بترتيب little-endian، وتمثّل كل قيمة منها **موضع النهاية التراكمي** في دفق القيم بعد عناصر ذلك الصف:

* فهرس بداية العناصر للصف `N` = `offsets[N - 1]` (أو `0` عندما يكون `N == 0`).
* فهرس نهاية العناصر (غير شامل) للصف `N` = `offsets[N]`.
* عدد عناصر الصف `N` = `offsets[N] - offsets[N - 1]`.

وبناءً على ذلك، تمثّل `offsets[num_rows - 1]` إجمالي عدد العناصر في جميع الصفوف، ويضمّ دفق القيم هذا العدد من القيم الداخلية متتاليةً دون فواصل.

الإزاحات **رتيبة غير متناقصة**؛ فتساوي إزاحتين متتاليتين يدل على صف فارغ، وينبغي لأداة فك الترميز أن ترفض الإزاحات غير الرتيبة وتعدّها تلفًا في البيانات. أما العمود الفارغ (`num_rows == 0`) فلا يُكتب له أي بايت، فلا يوجد دفق إزاحات ولا دفق قيم. ويجوز أن تكون الأنواع الداخلية من أي نوع، بما في ذلك الأنواع المركّبة الأخرى؛ فالأنواع `Array(Array(T))` و`Array(Tuple(...))` و`Array(Nullable(T))` كلها صالحة.

`Array(UInt32)` بالصفوف `[[10, 20, 30], [], [40, 50]]` (44 بايت إجمالًا):

```text theme={null}
Offsets (3 × UInt64 LE = 24 bytes):
03 00 00 00 00 00 00 00      offsets[0] = 3
03 00 00 00 00 00 00 00      offsets[1] = 3 (empty row)
05 00 00 00 00 00 00 00      offsets[2] = 5

Values (5 × UInt32 LE = 20 bytes):
0A 00 00 00                  10
14 00 00 00                  20
1E 00 00 00                  30
28 00 00 00                  40
32 00 00 00                  50
```

تمثّل كل إزاحة *نهايةً* تراكمية لجزء الصف من دفق القيم المشترك، أما بدايته فهي الإزاحة السابقة (أو `0` للصف 0). وتدل الإزاحتان المتتاليتان المتساويتان على صف فارغ:

![إزاحات تربط الصفوف بدفق قيم مشترك](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/array-offsets.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=caba90e4578ba0cd297f97007d747b98)

النوع `Array(String)` بالصفوف `[["a", "bb"], []]` (20 بايت إجمالًا):

```text theme={null}
Offsets (2 × UInt64 LE = 16 bytes):
02 00 00 00 00 00 00 00      offsets[0] = 2
02 00 00 00 00 00 00 00      offsets[1] = 2 (empty row)

Values (2 strings, 4 bytes total):
01 'a'                       row's first string: "a"
02 'b' 'b'                   row's second string: "bb"
```

يتّبع `Array(Array(UInt32))` ذو الصفوف `[[[1,2]], [], [[3], [4,5]]]` البنيةَ نفسها على نحوٍ متداخل:

* الإزاحات الخارجية: `[1, 1, 3]`؛ إذ يحتوي الصف 0 على مصفوفة داخلية واحدة، والصف 1 على 0، والصف 2 على 2.
* يفكّ `Array(UInt32)` الأوسط ترميز 3 صفوف بالإزاحات `[2, 3, 5]`.
* يفكّ `UInt32` الأعمق ترميز 5 قيم: `[1, 2, 3, 4, 5]`.

وبذلك يبلغ المجموع 24 (الإزاحات الخارجية) + 24 (الإزاحات الوسطى) + 20 (القيم) = 68 بايت.

#### Tuple(T1, T2, ...)

سلسلة النوع: `Tuple(T1, T2, ..., Tn)`. أمثلة: `Tuple(UInt32, String)`، `Tuple(Int32)`، `Tuple(Array(UInt32), String)`، `Tuple(UInt8, Tuple(Int32, String))`. يدعم ClickHouse أيضًا **Tuple المسمّاة** باستخدام `Tuple(a UInt32, b String)`؛ والأسماء مجرد بيانات وصفية لا تؤثر في تنسيق wire.

يتكوّن تخطيط wire من [مرحلة البادئة](#composite-types) الخاصة بالعناصر (إذ يضيف كل عنصر ذي إصدار بادئة الحالة الخاصة به وفق ترتيب التصريح، وتكون هذه المرحلة فارغة للعناصر التي لا إصدار لها)، تليها *N* من التدفقات المتتالية، بواقع تدفق واحد لكل نوع عنصر، وفق ترتيب التصريح:

```text theme={null}
[element state prefixes]   in declaration order; empty unless an element type is versioned
[stream for T1]    inner T1's encoding for num_rows values
[stream for T2]    inner T2's encoding for num_rows values
 ...
[stream for Tn]    inner Tn's encoding for num_rows values
```

يُشفِّر كل دفق `num_rows` قيمةً بالضبط. لا توجد بادئة طول، ولا دفق إزاحات، ولا فواصل بين الدفقات. ولا يُكتب للعمود الفارغ (`num_rows == 0`) أي بايت في أي دفق. ويمكن أن تكون أنواع العناصر من أي نوع، بما في ذلك الأنواع المركّبة الأخرى؛ إذ إن `Tuple(Tuple(...), ...)` و`Tuple(Array(...), ...)` و`Tuple(Nullable(T1), T2)` كلها صالحة.

والـ Tuple الخالي من العناصر `Tuple()` صالح أيضًا، وهو ينشأ من تعبيرات مثل `SELECT tuple()` أو `CAST(x AS Tuple())`. ولأنه لا يملك دفقات عناصر، فإنه يُسلسَل بطريقة [Nothing](#nothing) نفسها: **بايت عنصر نائب واحد (`0x30`، أي `'0'` في ASCII) لكل صف**، تتجاهله أداة إلغاء التسلسل. ويُستمد عدد الصفوف من ترويسة الكتلة، تمامًا كما في `Nothing`.

`Tuple(UInt8, UInt8)` مع 3 صفوف `(1,4), (2,5), (3,6)`:

```text theme={null}
Element 0 stream (3 × UInt8 = 3 bytes):
01 02 03

Element 1 stream (3 × UInt8 = 3 bytes):
04 05 06
```

التخطيط **ليس** بترتيب الصفوف أولًا (row-major)، إذ تُعيد قراءة البايتات الخام القيمة `[1, 2, 3]` للعنصر 0 والقيمة `[4, 5, 6]` للعنصر 1.

`Tuple(UInt32, String)` بصفّين `(10, "a")` و`(20, "bb")` (بإجمالي 13 بايت):

```text theme={null}
Element 0 stream (2 × UInt32 LE = 8 bytes):
0A 00 00 00                  10
14 00 00 00                  20

Element 1 stream (2 strings, 5 bytes total):
01 'a'                       "a"
02 'b' 'b'                   "bb"
```

#### Map(K, V)

سلسلة النوع: `Map(KeyType, ValueType)`. أمثلة: `Map(String, UInt32)`، `Map(String, Array(UInt32))`، `Map(UInt8, Tuple(Int32, String))`، `Map(Array(String), Int8)`. لا يفرض تنسيق wire أي قيد على أيٍّ من النوعين، إذ يمكن أن يكون كلٌّ من `K` و`V` أيَّ نوع مدعوم، بما في ذلك الأنواع المركّبة. (اختلفت قواعد ClickHouse على مستوى SQL بشأن أنواع المفاتيح المقبولة من إصدار إلى آخر؛ لذا راجع توثيق SQL الخاص بإصدار الخادم المستهدف.)

يتطابق تخطيط wire بايتًا ببايت مع `Array(Tuple(K, V))`، ولذلك يبدأ بـ[مرحلة البادئة](#composite-types) الداخلية (وتكون فارغة ما لم يكن `K` أو `V` ذا إصدار تسلسل):

```text theme={null}
[K/V state prefixes]   from the inner Tuple's prefix phase; empty unless K or V is versioned
[offsets stream]    num_rows × UInt64 LE                   ← from Array
[keys stream]       K's encoding for total_pairs values    ┐ from Tuple's
[values stream]     V's encoding for total_pairs values    ┘ per-element streams
```

حيث `total_pairs = offsets[num_rows - 1]` (أو `0` عندما يكون `num_rows == 0`). ولدفق الإزاحات الدلالات نفسها المتبعة في [Array](#array). والمفاتيح محاذاة موضعيًا مع القيم: فالزوج `i` هو `(keys[i], values[i])`.

يُمثِّل ClickHouse عمود Map في الذاكرة على هيئة مصفوفة من صفوف مرتبة، غير أن نظام الأنواع يعرضه نوعًا مستقلًا لتيسير الاستخدام في SQL (`m['key']`، `mapKeys`، `mapValues`). وتنسيق wire ما هو إلا تسلسل مباشر لهذا التخزين، ولذلك فإن `Map` و`Array(Tuple(K, V))` قابلان للتبادل بايتًا ببايت.

الإزاحات رتيبة غير متناقصة، ويحتوي كلٌّ من دفق المفاتيح ودفق القيم على `total_pairs` قيمة بالضبط. ولا يُكتب أي بايت للعمود الفارغ. وعادةً ما تكون المفاتيح مميزة ضمن الصف الواحد، لكن هذه قاعدة دلالية لا يفرضها تنسيق wire: إذ يسمح تنسيق wire بكتابة المفاتيح المكررة وقراءتها مجددًا دون تغيير، ولا تُحسم التكرارات وفق الدلالات على جانب الخادم إلا عندما تستهلك الصفَّ دالةٌ تتعامل مع Map.

`Map(UInt8, UInt8)` مع صفين `{1:10, 2:20}`، `{3:30}` (22 بايت إجمالًا):

```text theme={null}
Offsets (2 × UInt64 LE = 16 bytes):
02 00 00 00 00 00 00 00      offsets[0] = 2
03 00 00 00 00 00 00 00      offsets[1] = 3

Keys (3 × UInt8 = 3 bytes):
01 02 03                     keys: 1, 2, 3

Values (3 × UInt8 = 3 bytes):
0A 14 1E                     values: 10, 20, 30
```

تُخزَّن المفاتيح والقيم في تدفقات منفصلة لا متداخلة، إذ يُعاد بناء الزوج `i` بقراءة `keys[i]` و`values[i]` معًا.

النوع `Map(String, UInt32)` مع صف واحد `{'a':1, 'b':2}` (بإجمالي 20 بايت):

```text theme={null}
Offsets (1 × UInt64 LE = 8 bytes):
02 00 00 00 00 00 00 00      offsets[0] = 2

Keys (2 strings, 4 bytes total):
01 'a'                       "a"
01 'b'                       "b"

Values (2 × UInt32 LE = 8 bytes):
01 00 00 00                  1
02 00 00 00                  2
```

#### Nested(name1 T1, name2 T2, ...)

يتوقف تمثيل `Nested` أثناء النقل على قيمة الإعداد `flatten_nested` من جهة الخادم، ما يُفضي إلى حالتين مختلفتين.

![تخطيط العمود المركّب مع التسطيح وبدونه](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/nested-layout.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=786631c14f4e8b40cbd3ad9d47eb3c56)

**الحالة أ: `flatten_nested = 1` (القيمة الافتراضية للخادم).** إذا أُنشئ الجدول بالإعدادات الافتراضية، فإن `Nested` **ليس نوعًا من أنواع النقل (wire type)**، إذ يخزّن الخادم العمود ويعرضه على هيئة N من أعمدة `Array(T_i)` المتوازية ذات **أسماء منقّطة** (`outer.field1` و`outer.field2` وهكذا). وبالنسبة لطبقة الصيغة لا جديد في ذلك، فكل عمود منقّط ما هو إلا [Array](#array) عادية:

```text theme={null}
DESCRIBE TABLE t   -- t has column n Nested(a UInt8, b String)
id     UInt8
n.a    Array(UInt8)
n.b    Array(String)
```

**الحالة B: `flatten_nested = 0`.** عند إنشاء الجدول باستخدام `flatten_nested = 0`، يظهر العمود في تنسيق النقل عمودًا واحدًا سلسلةُ نوعه `Nested(name1 T1, name2 T2, ...)`، ويكون تخطيطه بعد سلسلة النوع **مطابقًا بايتًا ببايت لـ `Array(Tuple(T1, T2, ..., Tn))`**، بما في ذلك [مرحلة البادئة](#composite-types) الداخلية، ومن ثَمّ فإن أي حقل ذي إصدار `T_i` يُصدِر بادئة الحالة الخاصة به أولًا، قبل الإزاحات. يستخدم المثال أدناه حقولًا بلا إصدار، ولذلك تكون مرحلة البادئة فارغة:

```text theme={null}
Nested(a UInt8, b String) bytes (after type string):
  02 00 00 00 00 00 00 00       offsets[0] = 2
  03 00 00 00 00 00 00 00       offsets[1] = 3
  0A 14 1E                       UInt8 stream
  01 'x' 01 'y' 01 'z'           String stream

Array(Tuple(a UInt8, b String)) bytes (after type string):
  02 00 00 00 00 00 00 00       offsets[0] = 2
  03 00 00 00 00 00 00 00       offsets[1] = 3
  0A 14 1E                       UInt8 stream
  01 'x' 01 'y' 01 'z'           String stream
```

الفرق الوحيد هو نص سلسلة النوع: يحتفظ `Nested` بأسماء الحقول (`a`، `b`)، بينما لا يحمل `Array(Tuple)` هذه الأسماء كخانات مسمّاة.

سلسلة النوع في الحالة B هي قائمة من أزواج (الاسم، النوع) مفصولة بفواصل. تفصل أول مسافة بيضاء بين الاسم ونوعه؛ وقد يحتوي النوع نفسه على مزيد من المسافات البيضاء والفواصل والأقواس، لذا يتطلب التحليل أداة التقسيم نفسها المستخدمة مع `Tuple`، والتي تراعي مستوى التداخل. تخطيط wire:

```text theme={null}
[offsets stream]    num_rows × UInt64 LE                       ← from Array
[field1 stream]     T1's encoding for total_elements values    ┐ from Tuple's
[field2 stream]     T2's encoding for total_elements values    │ per-element
 ...                                                            │ streams
[fieldn stream]     Tn's encoding for total_elements values    ┘
```

حيث `total_elements = offsets[num_rows - 1]` (أو `0` عندما تكون `num_rows == 0`). قيم الإزاحة رتيبة غير متناقصة، ويحتوي كل دفق حقل على `total_elements` قيمة بالضبط. ويشترط الخادم عند الإدراج (INSERT) أن تحمل جميع الحقول داخل الصف الواحد العدد نفسه من العناصر. أما العمود الفارغ فلا يُكتب له أي بايت.

`Nested(a UInt8, b String)` بصفين هما `[(10,'x'),(20,'y')]` و`[(30,'z')]` (‏25 بايت بعد سلسلة النوع):

```text theme={null}
Offsets (2 × UInt64 LE = 16 bytes):
02 00 00 00 00 00 00 00      offsets[0] = 2
03 00 00 00 00 00 00 00      offsets[1] = 3

Field 'a' stream (3 × UInt8 = 3 bytes):
0A 14 1E                     10, 20, 30

Field 'b' stream (3 strings, 6 bytes):
01 'x' 01 'y' 01 'z'         "x", "y", "z"
```

### الأسماء البديلة للأنواع

بعض الأنواع ليست سوى أسماء بديلة بحتة: إذ يرسل الخادم الاسم البديل في ترويسة العمود، غير أن البايتات التي تليه تعود إلى نوع أساسي. تربط أداة فك الترميز الاسم البديل بذلك النوع وتعيد استخدام المرمِّز الخاص به، دون الحاجة إلى أي تنسيق wire جديد.

الأنواع الجغرافية أسماء بديلة لمصفوفات وصفوف مرتبة متداخلة:

| سلسلة النوع | نوع wire الأساسي |
| - | - |
| `Point` | `Tuple(Float64, Float64)` |
| `Ring`, `LineString`, `MultiPoint` | `Array(Point)` |
| `Polygon`, `MultiLineString` | `Array(Ring)` |
| `MultiPolygon` | `Array(Polygon)` |

وبذلك يُفك ترميز عمود `Point` تمامًا كما يُفك ترميز `Tuple(Float64, Float64)` (ويُعرض بالشكل `(1,2)`)، ويُفك ترميز `Ring` كما يُفك ترميز `Array(Tuple(Float64, Float64))` (`[(0,0),(1,1)]`)، وهكذا صعودًا في التسلسل الهرمي.

`Geometry` هو أيضًا اسم بديل، لكنه بديل لـ [`Variant`](#variant) لا لمصفوفة متداخلة: فحمولته هي الـ variant المكوّن من الأنواع الجغرافية السبعة المذكورة أعلاه. لا تحمل ترويسة العمود سوى سلسلة النوع `Geometry`، **ولا** تفصّل مكونات الـ variant، لذا يتعيّن على أداة فك الترميز توسيعه بنفسها. وخلافًا لـ `Variant` الذي يكتبه المستخدم صراحةً، فإن مميّزات `Geometry` **لا** تتبع الترتيب الأبجدي للأسماء: فهي ثابتة لضمان التوافق، ولا تُضاف الأنواع الجغرافية الجديدة إلا في النهاية. والتعيين كالتالي: `0` = `LineString`، `1` = `MultiLineString`، `2` = `MultiPolygon`، `3` = `Point`، `4` = `Polygon`، `5` = `Ring`، `6` = `MultiPoint` (أُضيف في الإصدار 26.7، ولذا لن تحتوي التدفقات الواردة من الخوادم الأقدم على المميّز `6` أصلًا). ثم يُفك ترميز كل قيمة مختارة عبر اسمها الجغرافي البديل المذكور أعلاه (وتستخدم `NULL` مميّز `NULL` الخاص بـ `Variant`، وهو `255`).

`SimpleAggregateFunction(func, T)` اسم بديل لنوع قيمته `T`. فهو يخزّن قيمة تجميعية مكتملة مسبقًا، ولذلك فإن صيغته على wire وطريقة عرضه مطابقتان تمامًا لـ `T` (إذ يُفك ترميز `SimpleAggregateFunction(sum, UInt64)` على أنه `UInt64`). ولا يُعدّ اسمًا بديلًا بهذه الطريقة إلا الشكل ذو نوع القيمة الواحد؛ وقد يكون النوع الأساسي نفسه نوعًا مركّبًا.

> [!NOTE]
>
> ثمة نوعان مرتبطان **ليسا** اسمين بديلين، رغم أنهما نوعا أعمدة `Native` صالحان، إذ يمكن للعميل مثلاً أن يستقبل عمود `AggregateFunction` ناتجاً عن مُركِّب `-State` أو عن تجميع موزّع. غير أن لكل منهما حمولته المتخصصة التي تقع خارج نطاق هذه الصفحة:
>
> * يحتفظ `AggregateFunction(func, ...)` بحالة تجميع *وسيطة* (لا بقيمة نهائية)، ويختلف تخطيطها الثنائي باختلاف الدالة التجميعية وإصدارها. وقد يتضمن اسم الدالة معلمات، تُطبع كل منها في صورة قيمة حرفية SQL، كما في `AggregateFunction(topK(3), UInt64)`. وتطبع بعض الدوال التجميعية لاحقة `::Type` صريحة على المعلمة، كما في `AggregateFunction(topK('3'::Decimal32(0)), UInt64)`، لأن قيم `Decimal` والأعداد الصحيحة الواسعة لا صيغة حرفية مجردة لها تصلح للطباعة؛ وقيام الدالة بذلك من عدمه سمةٌ للدالة نفسها لا للتنسيق، لذا يجب على أداة فك الترميز التي تفحص المعلمات أن تقبل الصيغتين كلتيهما. أما أداة فك الترميز التي لا تحتاج إلا إلى أنواع الوسيطات فلا تتأثر بذلك. وتكتب الدالة التجميعية ذات الإصدارات إصدار الحالة غير الصفري صراحةً بوصفه الوسيطة الأولى في سلسلة النوع — `AggregateFunction(1, quantileDeterministic, UInt64, UInt64)` — في حين لا يُطبع الإصدار `0`، فتبقى سلسلة النوع `AggregateFunction(quantileDeterministic, UInt64, UInt64)`. وفي الاتصال المتفاوض عليه (أي حين تكون revision أعلى من `0`)، يحدد الكاتب الإصدار استناداً إلى مراجعة البروتوكول **وحدها**، ولا يعتمد مطلقاً على إصدار قد يكون مثبّتاً على نوعه المحلي، لأن الإصدار `0` لا يمكن تمييزه أثناء النقل عن "عدم الإعلان عن أي إصدار". ولهذا قد تعلن الدالة نفسها عن إصدارات مختلفة باختلاف الاتصالات، ويجب على القارئ أن يستمد الإصدار من سلسلة النوع لا أن يفترض أنه الأحدث. أما عند revision `0` فلا يوجد نظير يُشتق له إصدار، إذ يصف الدفق نفسه بنفسه ويقرؤه من كتبه؛ لذا يبقى الإصدار المثبّت على النوع محفوظاً في الدفق (وفي سلسلة النوع المُعلنة) ولا يُخفَض إلى `0`. وتصبح `quantileDeterministic` (ومعها الصيغتان `quantilesDeterministic` و`medianDeterministic`) ذات إصدارات بدءاً من revision `54491`: إذ يضيف الإصدار `1` حقلاً `UInt8 skip_degree` في نهاية الحالة، بينما تبقى بادئتها مطابقة بايتاً ببايت للإصدار `0`. وفيما دون هذه المراجعة يُصدر الكاتب حالات بالإصدار `0`، فلا يتأثر النظير الأقدم. وتصبح `uniq` ذات إصدارات بدءاً من revision `54493`: إذ يضيف الإصدار `1` بايت أعلام في بداية تخطيط الإصدار `0`، وعندما يكون البت `0` من الأعلام مضبوطاً، يُلحق في النهاية عيّنة إضافية صغيرة من قيم التجزئة الكاملة بطول 64 بت، تُطبَّق على كل منها عملية XOR مع الثابت `0x9E3779B97F4A0000` (`UInt8 skip_degree`، ثم `VarUInt count`، ثم القيم ذات الثمانية بايتات)، مما يحافظ على دقة التقدير عند الأعداد الأصلية التي تتجاوز ملياري قيمة؛ أما حالة الإصدار `0` فلا تحمل سوى العيّنة الرئيسية بطول 32 بت. ويشير البت `1` من الأعلام إلى أن عيّنة 64 بت *غير مكتملة*، أي أن الحالة قد استوعبت حالة بالإصدار `0` لم تُعرض عناصرها قط على عيّنة 64 بت؛ وعندئذٍ لا تُكتب أي عيّنة 64 بت، ويجب على القارئ أن يُجري التقدير استناداً إلى العيّنة الرئيسية بطول 32 بت وحدها (وأن يواصل الإشارة إلى ذلك في الحالات التي يكتبها لاحقاً)؛ ولذا فالبتان متنافيان، ويجب على القارئ رفض أي حالة تضبط كليهما. ولا يوجد أي بت آخر معرَّف، ويجب على القارئ رفض أي حالة تضبط بتاً غيرهما. وينطبق ذلك كله أيضاً على الحالة المتداخلة داخل نوع حاوٍ، مثل `Array` أو `Tuple` أو `Map` أو `Nullable` أو `Variant` (حيث يغيّر الإصدار المكتوب صراحةً للبديل سلسلة النوع دون أن يغيّر ترتيب المميّز)، أو داخل غلاف `Nested` / `SimpleAggregateFunction`. كما ينطبق على مُركِّبات الدوال التجميعية: فالمُركِّب التمريري المطبَّق على دالة ذات إصدارات — مثل `uniqIf` و`uniqArray` و`uniqForEach` و`uniqMap` و`uniqArgMin` / `uniqArgMax` و`uniqOrNull` / `uniqOrDefault` و`uniqResample`، والمُكيِّف الضمني لوسيطات `Nullable` — يمرّر الإصدار إلى الحالات المتداخلة التي تتضمنها حمولته، ويكتب الإصدار الحالي للدالة المتداخلة صراحةً في سلسلة نوع الحالة الجديدة تماماً كما تفعل الدالة المجردة (`AggregateFunction(1, uniqIf, UInt64, UInt8)`)، ويشتق الإصدار من مراجعة البروتوكول في الاتصال المتفاوض عليه بالطريقة نفسها. ولأن الاسم المخصص للغلاف يتضمن نسخة ثانية من نوع وسيطته، يجب على الكاتب إعادة بناء تلك النسخة بإصدار حالة التجميع المتفاوض عليه نفسه، وإلا فقد تعلن ترويسته عن الإصدار `1` لحمولة مكتوبة بالإصدار `0`. وثمة حالة حدّية واحدة في `Variant` لا يمكن تمثيلها أثناء النقل إطلاقاً: فعندما يختلف بديلان في إصدار الحالة المكتوب صراحةً فقط (وهو أمر جائز في نوع العمود، لأن سلسلتي النوع مختلفتان)، فإن إعادة تعيين إصدار كليهما وفق المراجعة المتفاوض عليها ستدمجهما في بديل واحد، لذا يجب على المُرسل أن يُفشل عملية النقل بدلاً من الإعلان عن إصدارات لم تُكتب بها الحمولة. ويُعد `Dynamic` استثناءً مقصوداً من قاعدة اشتقاق الإصدار من المراجعة: إذ يعلن عمود `Dynamic` عن أنواعه المتداخلة بنفسه، عبر قوائم أنواعه الخاصة وعبر [الترميز الثنائي للأنواع](https://clickhouse.com/docs/ar/reference/data-types/data-types-binary-encoding.md)، الذي يتضمن حقل إصدار صريحاً يحمل إصدار الحالة المكتوب صراحةً (وبهذه الطريقة تُضمِّن قيم المتغير المشترك نوعها حتى أثناء التخزين). وهذه الإعلانات جزء من بيانات العمود ذاتها، وهي متطابقة لكل نظير ولكل وسيط، لذا لا يُعاد كتابتها أبداً وفق مراجعة متفاوض عليها. وعليه، تُكتب الحالة المتداخلة داخل قيمة `Dynamic` بالإصدار المحدد في نوعها (وهو ما تحمله الحالة الجديدة، لأن نتيجة استعلام `-State` تكتب إصدارها صراحةً)، وبالإصدار `0` إذا لم يحدد النوع أي إصدار، وذلك على جميع الاتصالات والوسائط دون استثناء.
> * يخزّن `QBit(T, N[, stride])` متجهاً بعد تبديل مستويات البتات فيه، لخدمة أحمال عمل البحث المتجهي. ويرد توثيق تخطيط دفقه أثناء النقل (دفقات مستويات بتات `FixedString` مرتبة حسب المجموعة أولاً، وعددها `element_size * (N / stride)` عند تحديد `stride` صراحةً) وترميزه الثنائي للنوع (الوسم `0x36`، أو `0x37` `QBitWithStride` عندما يكون `stride != N`) في [صفحة نوع البيانات `QBit`](https://clickhouse.com/docs/ar/reference/data-types/qbit) وفي المرجع الخاص بـ[الترميز الثنائي للأنواع](https://clickhouse.com/docs/ar/reference/data-types/data-types-binary-encoding.md)، فلا يضطر قارئ `Native` إلى استخلاصها من شيفرة C++ المصدرية.

### الأنواع ذات الإصدارات

تحمل الأنواع ذات الإصدارات، ضمن التمثيل المُرسَل عبر الاتصال، بادئةً تُحدِّد إصدار التسلسل وتُبيّن أيّ صيغة من صيغ الترميز تليها. وقد تستخدم أيضًا عدة تدفقات (كما هو الحال في الأنواع المركّبة). وفي تمثيل `Native` المُرسَل عبر الاتصال، تكون البادئة وأي قاموس مرتبطَين بكل كتلة على حدة، إذ لا تحتفظ هذه الأنواع بأي حالة تمتد عبر الكتل (راجع [ملاحظة البادئة الخاصة بكل كتلة](#serialization-version-concept) أدناه)؛ أما حالة التسلسل الممتدة عبر الكتل فلا توجد إلا في الدفق المخزَّن على القرص الخاص بـ MergeTree.

هذه الأنواع أكثر تعقيدًا بكثير من الأنواع المركّبة ذات البنية الثابتة، ولذا يمكن للعميل الذي يستهدف استعلامات تحليلية بسيطة أن يؤجّل دعمها.

#### إصدار التسلسل: المفهوم

**إصدار التسلسل** هو رقم إصدار يُنقل ضمن البيانات المُرسَلة، ويكون خاصًا بكل نوع وبكل عمود، ويحدد أيّ صيغة من صيغ ترميز النوع يستخدمها المُرسِل. وهو أول ما يرد في بادئة الحالة الخاصة بالعمود، فتقرؤه أداة فك الترميز أولًا ثم توجّه بقية العمود إلى المحلّل المناسب.

وهو يختلف عن إصدار البروتوكول:

| البُعد | إصدار البروتوكول | إصدار التسلسل (هذا القسم) |
| - | - | - |
| النطاق | على مستوى الاتصال بأكمله | لكل نوع ولكل عمود |
| يُتفاوض عليه | نعم، أثناء المصافحة | لا — يكتبه المُرسِل ويقرؤه المُستقبِل |
| يتحكم في | الميزات المفعّلة على مستوى الحزمة | صيغة النقل المستخدمة لنوع واحد |
| إلزامي القراءة | نعم | نعم، لكل عمود ذي إصدار |

تكتب معظم الأنواع ذات الإصدارات رقمَ الإصدار بصيغة UInt64 بترتيب little-endian مباشرةً قبل أي بيانات أخرى في بادئة الحالة، بينما يستخدم عدد قليل منها VarUInt أو UInt8. تقرأ أداة فك الترميز الإصدار أولًا وترفض القيم غير المعروفة؛ إذ يدل الإصدار الأعلى على صيغة أحدث لدى المُرسِل لا تفهمها أداة فك الترميز، وأي خطأ في تحليلها يُفسد كل البايتات التالية.

تُرسَل بادئة الحالة في بداية **كل كتلة يزيد عدد صفوفها على صفر**، مباشرةً قبل حمولة تلك الكتلة.

لا يحتفظ كاتب Native وقارئه بحالة التسلسل بين الكتل: إذ ينشئ `NativeWriter` حالة تسلسل جديدة ويكتب بادئة حالة لكل كتلة عمود غير فارغة يكتبها، وينشئ `NativeReader` حالة إلغاء تسلسل جديدة ويقرؤها لكل كتلة غير فارغة يقرؤها (ويتخطى كلاهما البادئة كليًا عندما يكون `rows == 0`).

ولذلك لا تُرسِل كتل الترويسة (rows = 0) والكتل الفارغة أي شيء، ويجب على أداة فك الترميز أن تقرأ بادئة الحالة من جديد في بداية كل كتلة غير فارغة. أما أداة فك الترميز التي تقرأ البادئة مرة واحدة فقط وتعدّ الكتل اللاحقة حمولةً بلا بادئة، فستقرأ بادئة الكتلة التالية على أنها بيانات وتفقد التزامن:

![بادئة الحالة مكررة لكل كتلة غير فارغة](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/state-prefix-per-block.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=753ca07b93f3b88feea72d9584eca779)

#### مرجع إصدارات التسلسل

| النوع | عرض الحقل | القيمة | الاسم | المعنى |
| - | - | - | - | - |
| **Object** (الأساس لـ JSON) | UInt64 LE | `0` | `V1` | الترميز الأصلي. يتضمن المعلمة `max_dynamic_paths` وقائمة بالمسارات الديناميكية. |
| | | `1` | `STRING` | وضع التوافق مع صيغة Native — يُنقل Object على هيئة column `String` واحد يحتوي على نص JSON. |
| | | `2` | `V2` | تخطيط V1 من دون المعلمة `max_dynamic_paths`. |
| | | `3` | `FLATTENED` | وضع التوافق مع صيغة Native — تمثيل مسطّح للمسارات. |
| | | `4` | `V3` | يُكتب في أجزاء بيانات MergeTree فقط، ولا يُرسل عبر الشبكة أبدًا؛ وتخطيطه غير محدَّد هنا. |
| **Dynamic** | UInt64 LE | `1` | `V1` | الترميز الأصلي. يتضمن `max_dynamic_types` وقائمة بالأنواع البديلة المحددة وقت التشغيل. |
| | | `2` | `V2` | V1 من دون المعلمة `max_dynamic_types`. |
| | | `3` | `FLATTENED` | وضع التوافق مع صيغة Native. |
| | | `4` | `V3` | يُكتب في أجزاء بيانات MergeTree فقط، ولا يُرسل عبر الشبكة أبدًا؛ وتخطيطه غير محدَّد هنا. |
| وضع المميّزات في **Variant** | UInt64 LE | `0` | `BASIC` | يُكتب مميّز كل صف كما هو. |
| | | `1` | `COMPACT` | إذا اشتركت جميع صفوف الحبيبة في مميّز واحد، فلا تُكتب سوى قيمة واحدة مع وسم الحبيبة. |
| صيغة الحبيبة في **Variant** (عندما يكون الوضع `COMPACT`) | UInt8 | `0` | `PLAIN` | تحتوي الحبيبة على مميّزات غير متجانسة. |
| | | `1` | `COMPACT` | تحتوي الحبيبة على مميّز واحد لجميع الصفوف. |
| تسلسل المفاتيح في **LowCardinality** | Int64 | `1` | `sharedDictionariesWithAdditionalKeys` | الإصدار الوحيد المعرَّف حاليًا. |
| البديل الاحتياطي **JSON-as-String** (عند تفعيل `output_format_native_write_json_as_string`) | UInt64 LE | `1` | `JSONStringSerializationVersion` | يصل column JSON على هيئة column `String` تسبقه هذه البادئة. |

ملاحظات جديرة بالانتباه حول هذا الجدول:

* **القيم ليست متتالية.** يستخدم `Dynamic` القيم `1` و`2` و`3` و`4`، إذ تقابل `V3` القيمة `4` وتقابل `FLATTENED` القيمة `3`. فالرقم الأعلى لا يعني بالضرورة إصدارًا أحدث.
* **بعض القيم خاصة بصيغة Native فقط.** توجد `Object::STRING` و`Object::FLATTENED` و`Dynamic::FLATTENED` لضمان التوافق عبر البروتوكول الأصلي مع العملاء الذين لا يدعمون Object/Dynamic دعمًا كاملًا. ولا تظهر هذه القيم في التخزين على القرص في MergeTree.
* **`V3` خاص بالتخزين على القرص فقط.** أُدرج `Object::V3` و`Dynamic::V3` لأن قيمتيهما مخصّصتان ولا يجوز إعادة استخدامهما، لا لأن أداة فك الترميز قد تصادفهما: فـClickHouse لا يرسلهما أبدًا عبر `Native`، بل يرفضهما فيه مع الخطأ `INCORRECT_DATA`. وكل ما يحملانه — الإصدار الفرعي لمنطقة shared-data في `Object::V3` وتخطيطات الحاويات والحبيبات الخاصة به — يصف جزء بيانات لا كتلة، ولذلك يقع خارج نطاق هذه الصفحة. أما العملاء الذين يستهلكون بروتوكول TCP الأصلي فلا يرون سوى `V1` أو `V2` أو `STRING` أو `FLATTENED`.

#### LowCardinality(T)

أبسط الأنواع ذات الإصدارات، إذ يستبدل column مكوّنًا من `N` من القيم الداخلية بقاموس صغير من القيم المميزة، إضافةً إلى `N` من الفهارس التي تشير إلى هذا القاموس.

سلسلة النوع: `LowCardinality(InnerType)`. أمثلة: `LowCardinality(String)`، `LowCardinality(FixedString(4))`، `LowCardinality(Nullable(String))`.

```text theme={null}
[per block with rows > 0]:
  [8 bytes:  Int64 LE state prefix = 1]             ← repeated at the start of every non-empty block
  [8 bytes:  UInt64 LE metadata]                    ← key type code (low byte) + flag bits
  [8 bytes:  UInt64 LE dict_size]                   ← number of dict entries (incl. placeholder slot)
  [N bytes:  dict values]                           ← inner type's encoding for dict_size values
  [8 bytes:  UInt64 LE keys_count]                  ← number of values at this recursive level (see below)
  [K bytes:  keys]                                  ← (1 << key_type_code) bytes per key
```

بادئة الحالة (Int64 LE = 1) هي الإصدار الوحيد المعرَّف، `sharedDictionariesWithAdditionalKeys`؛ أما القيم الأخرى فمحجوزة.

البيانات الوصفية لكل كتلة من نوع UInt64، وهي حقل بتات:

| نطاق البتات | المعنى |
| - | - |
| 0..7 | رمز نوع المفتاح: `0` = UInt8، `1` = UInt16، `2` = UInt32، `3` = UInt64. ويُختار أصغر نوع يكفي لفهرسة `dict_size` من الإدخالات. |
| 8 (`0x100`) | `NeedGlobalDictionaryBit` — قاموس واحد مشترك بين الكتل. **لا يُضبط أبدًا في صيغة `Native`**: إذ يستخدم Native writer الإعداد `low_cardinality_max_dictionary_size = 0`، ويرفض Native reader هذا البت (يُطلق `native_format` الخطأ `INCORRECT_DATA` — "cannot use global dictionary"). فهذا البت خاص بدفق MergeTree على القرص، لا بتنسيق النقل. |
| 9 (`0x200`) | `HasAdditionalKeysBit` — يُضبط عندما تحمل الكتلة مفاتيح قاموس إضافية (تُكتب قبل الفهارس). ويكون مضبوطًا دائمًا في أي كتلة `Native` غير فارغة. |
| 10 (`0x400`) | `NeedUpdateDictionary` — يُضبط عندما تحمل الكتلة تحديثًا للقاموس. ويكون مضبوطًا دائمًا في أي كتلة `Native` غير فارغة، لأن كل كتلة تحمل قاموسها المستقل الخاص بها. |

في الاستجابة المعتادة لاستعلام يتضمن كتلة بيانات واحدة لكل column، تكون قيمة البيانات الوصفية `0x600` (HasAdditionalKeys + NeedUpdateDictionary).

قيم القاموس هي `dict_size` قيمةً مُرمَّزة باستخدام النوع الداخلي T. ويحجز القاموس الخانات الأولى للقيم الخاصة: فالـ column غير القابل للقيم الفارغة يحجز خانة واحدة (تحتوي `dict[0]` على القيمة الافتراضية للنوع الداخلي، مثل `""` في حالة `String`)، وتبدأ القيم المميزة الفعلية من `dict[1]`.

أما في حالة `LowCardinality(Nullable(T))` فيظل القاموس مُرمَّزًا بالنوع T العادي (دون دفق null-map)، لكن تُحجز خانتان **اثنتان**: `dict[0]` هي وسم NULL و`dict[1]` هي القيمة الافتراضية للنوع الداخلي (مثل `""` في حالة `String`)؛ وتبدأ القيم المميزة الفعلية من `dict[2]`. ويشير مفتاح الصف ذي القيمة NULL إلى `dict[0]`، وتُكتب تلك الخانة في تنسيق النقل بالبايتات الافتراضية للنوع الداخلي.

المفاتيح فهارس تشير إلى مواضع في القاموس؛ وحجم كل فهرس `1 << key_type_code` بايت (1 أو 2 أو 4 أو 8)، وتُعاد بناء القيمة `N` بوصفها `dict[keys[N]]`.

يمثل `keys_count` عدد قيم `LowCardinality` في **المستوى التعاودي الحالي**، وليس بالضرورة عدد صفوف الكتلة. ففي column `LowCardinality` على المستوى الأعلى يتساوى العددان. أما عندما يكون `LowCardinality` متداخلًا ضمن نوع مركّب، فإن العدد هو عدد القيم المسطّحة الذي يمرّره النوع المركّب إلى المستوى الأدنى: ففي `Array(LowCardinality(String))` بثلاثة صفوف تضم خمسة عناصر إجمالًا، تكون قيمة `keys_count` هي `5` لا `3`؛ وفي `Map(K, LowCardinality(V))` تساوي إجمالي عدد الأزواج، وهكذا. ويجب على أداة فك الترميز أن تأخذ `keys_count` من هذا الحقل بدلًا من افتراض أنه يساوي عدد صفوف الكتلة. وعندما يكون هذا العدد المسطّح صفرًا — كما في كتلة جميع مصفوفاتها فارغة — فإن مرحلة بيانات `LowCardinality` لا تكتب **شيئًا على الإطلاق**: إذ لا توجد سوى بادئة الحالة (المُصدَرة في [مرحلة البادئة للأنواع المركّبة](#composite-types))، دون أن تليها بيانات وصفية أو قاموس أو `keys_count`.

تُقرأ بادئة الحالة في بداية كل كتلة يزيد عدد صفوفها على صفر، أما كتل الترويسة (rows = 0) والكتل الفارغة فلا يُكتب لها شيء. وداخل الكتلة، تساوي قيمة `keys_count` عدد الصفوف، وتساوي قيمة `dict_size` عدد القيم في دفق القاموس، ويشغل كل مفتاح `1 << key_type_code` بايت.

> [!NOTE]
>
> في صيغة `Native`، تحمل كل كتلة **قاموسًا مستقلًا خاصًا بها** — إذ لا توجد حالة قاموس مشتركة بين الكتل. يضبط Native writer القيمة `low_cardinality_max_dictionary_size = 0`، ولذلك لا ينشئ `SerializationLowCardinality` قاموسًا مشتركًا أبدًا: فكل كتلة غير فارغة تكتب مفاتيحها بوصفها مفاتيح إضافية خاصة بالكتلة دون ضبط `NeedGlobalDictionaryBit` (البيانات الوصفية `0x600`)، ويرفض Native reader البت `NeedGlobalDictionaryBit` عندما تكون قيمة `native_format` هي true. لذا يجب على أداة فك الترميز إعادة تعيين القاموس عند كل كتلة وقراءة عناصر `dict_size` الموجودة في تلك الكتلة؛ إذ إن الإبقاء على قاموس من كتلة سابقة سيؤدي إلى قراءة مفاتيح الكتلة التالية قراءةً خاطئة. (الاحتفاظ بقاموس LC عبر الكتل مسألة تخص تخزين MergeTree على القرص، ولا علاقة لها بتخطيط wire لصيغة Native.)

`LowCardinality(String)` مع القيم `['a', 'b', 'a', 'c', 'b']`:

```text theme={null}
01 00 00 00 00 00 00 00      state prefix Int64 = 1
00 06 00 00 00 00 00 00      metadata UInt64 = 0x600
04 00 00 00 00 00 00 00      dict_size = 4
00                           dict[0] = "" (placeholder)
01 'a'                       dict[1] = "a"
01 'b'                       dict[2] = "b"
01 'c'                       dict[3] = "c"
05 00 00 00 00 00 00 00      keys_count = 5
01 02 01 03 02               keys (UInt8): 1, 2, 1, 3, 2
```

إعادة البناء: `dict[1], dict[2], dict[1], dict[3], dict[2]` = `["a", "b", "a", "c", "b"]`.

يُظهر النوع `LowCardinality(Nullable(String))` مع القيم `['a', NULL, '', 'b']` الخانتين المحجوزتين كلتيهما — `dict[0]` لقيمة NULL، و`dict[1]` للقيمة الافتراضية المتمثلة في الـ string الفارغ:

```text theme={null}
01 00 00 00 00 00 00 00      state prefix Int64 = 1
00 06 00 00 00 00 00 00      metadata UInt64 = 0x600
04 00 00 00 00 00 00 00      dict_size = 4
00                           dict[0] = "" → NULL marker
00                           dict[1] = "" → inner default value
01 'a'                       dict[2] = "a"
01 'b'                       dict[3] = "b"
04 00 00 00 00 00 00 00      keys_count = 4
02 00 01 03                  keys (UInt8): 2, 0, 1, 3
```

بعد إعادة البناء: `dict[2]` = `"a"`، و`dict[0]` = `NULL`، و`dict[1]` = `""`، و`dict[3]` = `"b"`، أي `["a", NULL, "", "b"]`. وكلٌّ من `dict[0]` و`dict[1]` بايتاتٌ فارغة في تنسيق النقل؛ فكون القيمة NULL ناتجٌ عن إشارة المفتاح إلى الخانة `0`، لا عن البايتات نفسها.

#### JSON (Tier 1: String fallback)

يدعم النوع `JSON` في ClickHouse عدة أشكال من ترميز wire (راجع [مرجع إصدارات التسلسل](#serialization-version-reference)). ويُعد Tier 1 أبسطها: فعند ضبط إعداد الاستعلام `output_format_native_write_json_as_string = 1`، يحوّل الخادم كل قيمة JSON إلى تمثيلها النصي المُسلسَل، ويُخرج الـ column من النوع `String` مسبوقًا بوسم الحالة.

سلسلة النوع: `JSON`.

```text theme={null}
[8 bytes:  Int64 LE state prefix = 1]        ← JSONStringSerializationVersion
[per block with rows > 0]:
  [N bytes: String column encoding for num_rows JSON text values]
```

قيمة بادئة الحالة في وضع String fallback هذا هي `1`. أما القيم الأخرى فتشير إلى ترميزات مختلفة لـ `JSON`/`Object`: `0` = V1، و`2` = V2 (الترميز الافتراضي عبر بروتوكول TCP الأصلي)، و`3` = FLATTENED، و`4` = V3 (راجع [مرجع إصدارات التسلسل](#serialization-version-reference)). فإذا صادفت أداة فك الترميز هنا قيمة غير `1`، فهذا يعني أن البيانات التي تعالجها ليست بصيغة String fallback. تُقرأ البادئة في بداية كل كتلة يزيد عدد صفوفها على 0، أما دفق القيم فهو column قياسي من نوع [String](#string-type) يضم `num_rows` صفًا.

قيمة `JSON` هي `'{"a":1}'` (صف واحد):

```text theme={null}
01 00 00 00 00 00 00 00      state prefix Int64 = 1
07 7B 22 61 22 3A 31 7D      String: 7 bytes {"a":1}
```

تُرسَل القيمة في صورة نص JSON مضغوط — `{"a":1}`، مع إبقاء العدد الصحيح عددًا صحيحًا. ولأن هذا النص مجرد قيمة من النوع `String`، يتلقى العميل JSON لتمريره كما هو دون تفسير محتواه، لكنه لا يستعيد المسارات الفردية وأنواع ClickHouse الخاصة بها؛ فالحفاظ الدقيق على نوع كل مسار يتطلب استخدام ترميز Tier 2 الموضح أدناه.

#### Variant(T1, T2, ...)

اتحاد مُميَّز: يحمل كل صف قيمةً من نوع واحد فقط من الأنواع البديلة، أو NULL. ويحمل كل صف **مميّزًا عامًا** بحجم بايت واحد يحدد نوعه، ثم تُخزَّن القيم الخاصة بكل نوع بشكل متراص، بحيث يُخصَّص لكل نوع بديل تتابع واحد متصل.

سلسلة النوع: `Variant(T1, T2, ...)`. يُوحِّد الخادم الترتيب إلى صيغته القياسية (إذ تُرتَّب الأنواع البديلة حسب الاسم)، ولذلك تأتي سلسلة النوع المستقبَلة وقد سُردت فيها الأنواع وفق **ترتيب المميّز العام**: فالمميّز `0` يختار النوع الأول في القائمة، و`1` يختار النوع الثاني، وهكذا. أما `255` (`NULL_DISCRIMINATOR`) فيعني أن قيمة الصف NULL. لا تكون عناصر Variant من النوع `Nullable` أبدًا، لأن تمثيل NULL من مهام المميّز. أمثلة: `Variant(String, UInt64)`، `Variant(Array(UInt8), String)`.

تحمل بادئة الحالة وضع المميّزات بصيغة `UInt64 LE`: القيمة `0` = BASIC (يُكتب مميّز كل صف كما هو)، والقيمة `1` = COMPACT (ترميز الحبيبات بطول التتابع). يستخدم الخادم افتراضيًا الوضع BASIC عبر البروتوكول الأصلي (`use_compact_variant_discriminators_serialization = false`)، ولا يتناول هذا المستند سوى الوضع BASIC.

```text theme={null}
[per block with rows > 0]:
  [8 bytes:  UInt64 LE discriminators mode = 0]    ← state prefix, repeated at the start of every non-empty block;
                                                     followed by each variant element's own state prefix
                                                     (empty for leaf types)
  [num_rows bytes: UInt8 discriminators]           ← one global discriminator per row; 255 = NULL
  [for each variant type i, in declared order]:
    [values for the rows whose discriminator == i] ← dense encoding in type i; count = #rows selecting i
```

لإعادة البناء، اجتز المميّزات من اليسار إلى اليمين مع الاحتفاظ بعدّاد تراكمي لكل نوع. يأخذ الصف `r` ذو المميّز `d` (≠ 255) القيمة الموجودة عند الفهرس `counter[d]` من سلسلة قيم النوع البديل `d`، ثم يُزاد `counter[d]` بمقدار واحد. أما الصفوف ذات المميّز `255` فهي NULL ولا تستهلك أي قيمة من أي سلسلة، ولذلك يساوي مجموع العدّادات الخاصة بالأنواع عددَ الصفوف غير NULL.

تُقرأ بادئة الحالة (وهي قيمة `UInt64` الخاصة بالوضع) في بداية كل كتلة يزيد عدد صفوفها على 0؛ أما كتل الترويسة والكتل الفارغة فلا تُصدر شيئًا. ويكون كل مميّز غير NULL أصغر من عدد الأنواع البديلة، ويُفكّ ترميز النوع البديل `i` لعدد `count[i]` من الصفوف بالضبط.

> [!NOTE]
>
> عناصر Variant التي تكون هي نفسها ذات حالة (`LowCardinality` و`Variant` و`Dynamic` و`JSON`) تُصدر بادئة الحالة الخاصة بها في مرحلة بادئة الحالة الخاصة بكل عنصر، بعد قيمة `UInt64` الخاصة بالوضع. أما الأنواع الطرفية والأنواع المركّبة البسيطة (`Array` و`Tuple` و`Map` المكوّنة من أنواع طرفية) فبادئات حالتها فارغة، ويمكن تركيبها بحرية.

`Variant(String, UInt64)` بالقيم `[42, 'hi', NULL]` (يضع الترتيب القياسي `String` قبل `UInt64`، ولذلك فإن المميّز 0 = String، و1 = UInt64):

```text theme={null}
00 00 00 00 00 00 00 00      state prefix: UInt64 discriminators mode = 0 (BASIC)
01 00 FF                     discriminators (3 rows): 1 (UInt64), 0 (String), 255 (NULL)
02 68 69                     String run (1 value): len=2 "hi"
2A 00 00 00 00 00 00 00      UInt64 run (1 value): 42
```

إعادة البناء: الصف 0 = UInt64 run\[0] = `42`؛ الصف 1 = String run\[0] = `"hi"`؛ الصف 2 = NULL.

يؤدي دفق المميّزات دور الفهرس؛ إذ يسحب كل مميّز غير NULL القيمة التالية من التسلسل الكثيف الخاص بنوعه، في حين لا يستهلك `255` (NULL) أي قيمة. وبالطريقة نفسها يُعاد بناء [Dynamic](#dynamic)، الذي لا يختلف إلا في كيفية ترميز NULL:

![مميّزات تختار القيم من تسلسلات كثيفة خاصة بكل نوع](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/variant-discriminators.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=b722e6e431e0afbfd7a719d4b9711ea2)

#### Dynamic

column يُكتشف نوع قيمته في وقت التشغيل: يحتوي كل صف على قيمة من أحد أنواع مجموعةٍ تُحدَّد في وقت التشغيل، أو على NULL. وعلى خلاف `Variant`، فإن مجموعة الأنواع **ليست** جزءًا من سلسلة النوع الخاصة بالـ column — بل تُحمَل في بادئة الحالة.

سلسلة النوع: `Dynamic` أو `Dynamic(max_types=N)`. تحدّد المعلمة `max_types` الحد الأقصى لعدد الأنواع المميزة التي يتتبعها الـ column، لكنها لا تؤثر في تنسيق wire الموضّح أدناه.

لـ `Dynamic` أربعة أشكال ترميز: `V1 = 1`، و`V2 = 2`، و`FLATTENED = 3`، و`V3 = 4`. ويتوقف الشكل الذي يُصدره الخادم على القناة وعلى إعدادات الاستعلام:

* عبر `clickhouse-client` و`FORMAT Native` في HTTP، تكون قيمة revision لدى الكاتب `0` (ما لم تُرفع باستخدام `client_protocol_version`)، ولذلك يكون الشكل الافتراضي **V1**.
* عبر بروتوكول TCP الأصلي عند قيمة revision المتفاوض عليها، يكون الشكل الافتراضي **V2**. يُبقي كاتب `Native` الإحصاءات معطّلة، لذا لا تحمل حمولة `V2` الافتراضية أي إحصاءات لكل نوع، إذ تلي قائمةَ الأنواع مباشرةً بادئةُ `Variant` المتداخلة ثم البيانات. (الإحصاءات لكل نوع شأن يخص تخزين MergeTree على القرص، وليست جزءًا من تنسيق wire الخاص بـ Native.)
* يتجاوز إعداد الاستعلام `output_format_native_use_flattened_dynamic_and_json_serialization = 1` كلتا الحالتين، ويُصدر **FLATTENED (الإصدار 3)** بصرف النظر عن قيمة revision.

> [!NOTE]
>
> **النطاق**
>
> تقتصر هذه الصفحة على توصيف التخطيط **`FLATTENED`**. أما التخطيطات الثنائية غير المسطّحة `V1`/`V2`/`V3` فهي التمثيل الداخلي/المخزّن على القرص (قوائم أنواع مُرمَّزة ثنائيًا، وإحصاءات لكل نوع)، و**لا** تُوصَف هنا. ويجب على العميل الذي يريد فك ترميز `Dynamic` استنادًا إلى هذه الصفحة أن يطلب `FLATTENED` عبر ضبط `output_format_native_use_flattened_dynamic_and_json_serialization = 1`؛ إذ يفترض التخطيط أدناه تفعيل هذا الإعداد. ولأن بايت الإصدار يتصدّر البادئة، تستطيع أداة فك الترميز اكتشاف الترميز الفعلي الذي تلقّته، ورفض `V1`/`V2`/`V3` إن كانت لا تدعم سوى `FLATTENED`.

تخطيط **FLATTENED (الإصدار 3)** الذي يُفعّله ذلك الإعداد:

```text theme={null}
[per block with rows > 0]:
  [8 bytes:  UInt64 LE version = 3]                ← state prefix, repeated at the start of every non-empty block
  [VarUInt num_types]                              ← number of runtime types
  [num_types × type]                               ← type names, in wire order; each a String, or a binary
                                                     type encoding when output_format_native_encode_types_in_binary_format = 1,
                                                     except for a secondary query (see the type field note)
  [per type: its own state prefix]                 ← empty for leaf types; + indexes-type prefix (empty, integer)
  [num_rows × discriminator]                       ← width by num_types (UInt8 if ≤ 255, else UInt16/32/64);
                                                     NULL discriminator = num_types (one past the last type)
  [for each type i, in wire order]:
    [values for the rows whose discriminator == i] ← dense encoding in type i
```

عرض المميّز هو أصغر عدد صحيح بلا إشارة يتّسع لفهرسة `num_types` من الأنواع إضافةً إلى خانة NULL — أي `UInt8` عندما يكون `num_types ≤ 255`، ثم `UInt16`، ثم `UInt32`، ثم `UInt64`. وتُمثَّل NULL بقيمة المميّز `num_types` نفسها، بخلاف `Variant` حيث تُمثَّل NULL بالقيمة الثابتة `255`. أما إعادة البناء فتتم بالمرور الكثيف نفسه المتّبع في `Variant`: احتفظ بعدّاد لكل نوع، فيأخذ الصف `r` ذو المميّز `d` (≠ `num_types`) القيمة `counter[d]` من تتابع قيم النوع `d`.

تُقرأ بادئة الحالة (الإصدار + قائمة الأنواع) في بداية كل كتلة يكون عدد صفوفها > 0؛ أما كتل الترويسة والكتل الفارغة فلا تُصدر شيئًا.

> [!NOTE]
>
> **عدد مشوّه**
>
> تُقرأ قيمة `num_types` من الدفق قبل أي أسماء أنواع. ويجب على أداة فك الترميز أن تعاملها على أنها غير موثوقة، وألّا تستخدمها مباشرةً لتحديد حجم تخصيص الذاكرة — سواء كان العدد قريبًا من `SIZE_MAX` (مما قد يسبب تجاوزًا في العمليات الحسابية الوسيطة أو يُطلق استثناءً من غير نوع `DB::Exception`)، أو كان عددًا كبيرًا لكنه قابل للتمثيل مثل `100000000` (وهو أقل بكثير من `max_size()` الخاص بالمتجه، لكنه سيخصّص جيجابايتات من الذاكرة قبل قراءة اسم نوع واحد). يقرأ ClickHouse قائمة الأنواع عنصرًا تلو الآخر، ولا يستخدم سوى تلميح تخصيص مسبق محدود السقف، لذا تُرفض قيمة `num_types` التالفة إما بالخطأ `INCORRECT_DATA` ("`Dynamic` column has too many types"، عندما يتجاوز العدد ما تستطيع الحاوية استيعابه)، أو كخطأ قراءة عادي حين تنفد عناصر الأنواع من الدفق — ولا تُرفض أبدًا بسبب نفاد الذاكرة.
>
> لكن **لا** تقيّد قيمة `num_types` **المسطّحة** بالحد `ColumnDynamic::MAX_DYNAMIC_TYPES_LIMIT` (وقيمته `254` في ClickHouse): إذ تضم قائمة الأنواع المسطّحة *كل* نوع وقت تشغيل مميّز، بما في ذلك الأنواع التي فاضت إلى المتغيّر المشترك، لذا قد تسرد كتلة مسطّحة صالحة عددًا يفوق ذلك بكثير دون أن يكون ذلك خطأً. ولا ينطبق الحد `MAX_DYNAMIC_TYPES_LIMIT` إلا على العدد `num_dynamic_types` في البادئات غير المسطّحة `V1`/`V2`/`V3`، وهو عدد خانات المتغيّرات العادية المقيّد بهذا الحد (يتحقق منه ClickHouse هناك قبل إضافة `+ 1` الخاصة بالمتغيّر المشترك).

> [!NOTE]
>
> **الدفقات المقتطعة**
>
> تحتوي سلسلة المميّزات على `num_rows` عنصرًا بالضبط، ويجب على أداة فك الترميز التحقق من أنها قرأت هذا العدد كاملًا بدلًا من قبول ما يتضمنه الدفق أيًّا كان. فالسلسلة الأقصر لا تتعارض مع أي شيء يليها: إذ تُشتق عدّادات القيم لكل نوع من المميّزات المقروءة فعليًا، فتبقى متّسقة مع سلاسل القيم، كما أن السلسلة المكوّنة بالكامل من وسوم NULL لا تتطلب أي بايتات قيم على الإطلاق. ولهذا يرفض ClickHouse سلسلة المميّزات القصيرة بالخطأ `INCORRECT_DATA` ("Mismatch in flattened `Dynamic` column: N rows are expected, but the indexes stream contains only M rows") قبل أي فحص خاص بكل نوع، لأن الـ column المُعاد بناؤه سيُستخدم حينها على أنه يضم `num_rows` من الصفوف بغض النظر عن حجمه الفعلي.

> [!NOTE]
>
> تحمل أنواع وقت التشغيل ذات التسلسل الحافظ للحالة (`LowCardinality` و`Variant` و`Dynamic` و`JSON`) بادئات حالة متداخلة بعد قائمة أسماء الأنواع.

تتبع قائمة أنواع وقت التشغيل عادةً التوحيد القياسي الخاص بـ `Variant` — إذ تُكتب خانات المتغيّرات العادية بترتيب `DataTypeVariant` (حسب اسم النوع)، فلا يطابق ترتيب الإرسال ترتيب الإدراج. غير أنها **ليست دائمًا** مرتّبة ترتيبًا شاملًا: فالأنواع التي فاضت إلى المتغيّر المشترك (كما في حالة `Dynamic(max_types=N)` مثلًا) تُضاف بعد الخانات العادية بحسب ترتيب ظهورها الأول، مما قد يُخلّ بترتيب أسماء الأنواع في ذيل القائمة. لذلك يجب على أداة فك الترميز أن تعتمد قائمة الأنواع المُرسلة مرجعًا لتعيين المميّزات، وألّا تعيد ترتيبها بنفسها. أما ما لا يجوز أن تحتويه القائمة فهو نوع مكرّر أو `Nothing`: فالعنصران المتساويان يُعيَّنان إلى مميّز واحد، و`Nothing` ليس متغيّرًا أصلًا، وفي كلتا الحالتين لا يبقى لجزء من القائمة موضع يُعاد بناؤه فيه. ويرفض ClickHouse كلتا الحالتين بالخطأ `INCORRECT_DATA`. فمثلًا، في الصفوف `[42::UInt64, "hi", NULL]` يكون النوعان هما `String` و`UInt64`، ويسبق `"String"` القيمة `"UInt64"` في الترتيب، لذا تكون المميّزات `0` = String و`1` = UInt64 و`2` = NULL:

```text theme={null}
03 00 00 00 00 00 00 00      state prefix: UInt64 version = 3 (FLATTENED)
02                           VarUInt num_types = 2
06 53 74 72 69 6E 67         type[0] = "String"
06 55 49 6E 74 36 34         type[1] = "UInt64"
01 00 02                     discriminators (3 rows): 1 (UInt64), 0 (String), 2 (NULL)
02 68 69                     String run (type[0], 1 value): len=2 "hi"
2A 00 00 00 00 00 00 00      UInt64 run (type[1], 1 value): 42
```

بعد إعادة البناء: الصف 0 = UInt64 run\[0] = `42`؛ الصف 1 = String run\[0] = `"hi"`؛ الصف 2 = NULL. تتبع السلاسل الخاصة بكل نوع ترتيبَ التسلسل الثنائي نفسه المتبع في قائمة الأنواع (`String` قبل `UInt64`).

#### JSON (Tier 2: FLATTENED Object)

هذا هو الترميز الأغنى لـ JSON: فبدلًا من تسطيح كل قيمة وتحويلها إلى نص (Tier 1)، يُقسَّم الـ column إلى columns فرعية، column فرعي لكل مسار JSON. ويُختار هذا الترميز عبر **عدم** طلب الحل البديل الخاص بـ Tier 1 (`output_format_native_write_json_as_string = 0`) مع تفعيل علامة التسلسل المسطّح (`output_format_native_use_flattened_dynamic_and_json_serialization = 1`)؛ وعندها يُصدر الخادم التسلسل **بالإصدار 3**.

هناك نوعان من المسارات:

* **المسارات محددة النوع**: تُعرَّف في سلسلة النوع، مثل `JSON(a UInt32, b String)`، ويُفك ترميزها وفق النوع المُعرَّف لها. ويُحاط اسم المسار الذي يحتوي على نقاط بعلامات backtick في سلسلة النوع.
* **المسارات الديناميكية**: تُكتشف أثناء وقت التشغيل، ويُفك ترميز كل منها بوصفه column من نوع [Dynamic](#dynamic).

في وضع FLATTENED **لا يوجد column shared-data** (إذ يخص مخزن الفائض هذا ترميزات Object غير المسطّحة V2/V3). وكل مسار هو column كامل يضم `num_rows` من القيم.

```text theme={null}
[per block with rows > 0]:
  -- prefix phase (repeated at the start of every non-empty block):
  [8 bytes:  UInt64 LE version = 3]                ← state prefix
  [VarUInt num_dynamic_paths]
  [num_dynamic_paths × String]                     ← dynamic path names, in wire order
  [per typed path: its column's state prefix]      ← empty for leaf types
  [per dynamic path: a Dynamic state prefix]       ← version + type list (see Dynamic)
  -- data phase:
  [for each typed path:   its column's data]       ← num_rows values in the declared type
  [for each dynamic path: its Dynamic data]        ← num_rows values (discriminators + runs)
```

لاحظ البنية ثنائية المراحل: تأتي **جميع** بادئات الحالة للمسارات أولاً، ثم **جميع** بيانات المسارات. ومن ثَمّ تكون بادئة `Dynamic` الخاصة بالمسار الديناميكي (في مرحلة البادئة) منفصلةً عن بياناته (في مرحلة البيانات). تُقرأ بادئة الحالة في بداية كل كتلة يزيد عدد صفوفها على 0، ويحتوي كل column مسار (مُحدَّد النوع أو ديناميكي) على `num_rows` قيمة بالضبط. يُجمَّع كائن الصف `r` بقراءة قيمة كل مسار عند الفهرس `r`؛ أما المسار الديناميكي الذي يكون مميّز `Dynamic` الخاص به NULL في ذلك الصف فلا يُضيف أي مفتاح. يجب أن تكون أسماء المسارات الديناميكية فريدة، ولا يجوز أن يكرر أيٌّ منها مسارًا صرّحت به سلسلة النوع مسبقًا على أنه مُحدَّد النوع — إذ يؤدي كلا الأمرين إلى وجود قيمتين تحت مفتاح واحد في الكائن المُجمَّع، ولذلك يرفض ClickHouse الحالتين كلتيهما بالخطأ `INCORRECT_DATA`. وينطبق الأمر نفسه على قائمة المسارات الديناميكية في الترميزات غير المسطّحة.

> [!NOTE]
>
> **أعداد مشوّهة**
>
> يُقرأ `num_dynamic_paths` في تخطيط FLATTENED الموثَّق هنا — وكذلك عدد المسارات الديناميكية في الترميزين غير المسطّحين `V1`/`V2` — من الدفق قبل أسماء المسارات. (لا يوجد حقل مستقل للمسارات المسطّحة في بادئات الترميزات غير المسطّحة: فهي لا تحمل سوى عدد المسارات الديناميكية، إضافةً إلى قيمة `max_dynamic_paths` في `V1` تُقرأ ثم تُهمَل.) وكما هو الحال مع [`Dynamic`](#dynamic)، يجب على أداة فك الترميز أن تعدّ هذه الأعداد غير موثوقة، وألا تعتمد عليها مباشرةً في تحديد حجم أي تخصيص للذاكرة — سواء كان العدد من فئة `SIZE_MAX` أو عددًا كبيرًا لكنه قابل للتمثيل. يقرأ ClickHouse أسماء المسارات إدخالًا تلو الآخر، مكتفيًا بتلميح تخصيص مسبق ذي حد أقصى، لذا يُرفض العدد التالف إما بالخطأ `INCORRECT_DATA` ("JSON/Object column has too many paths"، عندما يتجاوز ما تستطيع الحاوية استيعابه) أو بخطأ قراءة عادي عند نفاد أسماء المسارات من الدفق.

> [!NOTE]
>
> **دفقات مقتطعة**
>
> تُعدّ قيم `num_rows` لكل column مسار شرطًا إلزاميًا لا حدًا أعلى: يجب على أداة فك الترميز أن تتحقق من مطابقة كل column مسار، مُحدَّد النوع كان أو ديناميكيًا، لعدد صفوف الكتلة، بدلًا من الوثوق بأن الدفق سينتهي في موضعه الصحيح. فليس للكائن المُجمَّع حجم مستقل يمكن المقارنة به — إذ إن عدد صفوفه هو ببساطة ما تنتجه عملية إلغاء التسطيح — لذا يتعذّر لاحقًا اكتشاف column المسار الناقص، وتتجاوز قراءةُ الصف `r` منه نهايتَه. يرفضه ClickHouse بالخطأ `INCORRECT_DATA` ("Unexpected size of typed path / flattened path *p* in flattened Object column"). وفي حالة المسار الديناميكي، يأتي هذا التحقق إضافةً إلى التحقق المكافئ داخل column [`Dynamic`](#dynamic) الخاص به.

قيمة `JSON` هي `{"a": 42, "b": "hi"}` (صف واحد، وكلا المسارين ديناميكيان). يُستنتج نوع العدد الصحيح في JSON على أنه `Int64`:

```text theme={null}
03 00 00 00 00 00 00 00      version = 3 (Object)
02                           num_dynamic_paths = 2
01 61                        path "a"
01 62                        path "b"
03 00 00 00 00 00 00 00 01 05 49 6E 74 36 34      "a" Dynamic prefix: version 3, 1 type, "Int64"
03 00 00 00 00 00 00 00 01 06 53 74 72 69 6E 67   "b" Dynamic prefix: version 3, 1 type, "String"
00 2A 00 00 00 00 00 00 00   "a" data: discriminator 0, Int64 42
00 02 68 69                  "b" data: discriminator 0, String "hi"
```

#### JSON غير المسطّح (V1/V2)

ترميزات `Object` غير المسطّحة هي ما يُرسله الخادم عبر الشبكة عندما تكون علامة التسطيح معطّلة: إذ يُستخدم `V1` عبر `clickhouse-client` / HTTP `FORMAT Native` (revision `0`)، ويُستخدم `V2` عبر بروتوكول TCP الأصلي. (أما `V3`، وهو الترميز الثالث غير المسطّح، فلا يُكتب إلا في أجزاء بيانات MergeTree، ويُرفض عند نقله عبر الشبكة.) تتضمن هذه الترميزات عمود بيانات مشتركة (shared-data)، و**لا** تتناولها هذه الصفحة. لاحظ أنها **لا** تتضمن إحصاءات لكل مسار عند النقل بصيغة Native: إذ يُبقي `NativeWriter` الإحصاءات معطّلة، فلا تحتوي بادئة البنية الخاصة بـ `Object` على قسم للإحصاءات، وتليها مباشرةً بايتات البادئات والبيانات الخاصة بالمسارات محددة النوع (typed) والديناميكية (dynamic) والبيانات المشتركة (shared-data). ولا تظهر الإحصاءات إلا في مسارات MergeTree على القرص التي تُفعّلها. ولفك ترميز عمود `JSON` اعتمادًا على هذه الصفحة، يجب على العميل اختيار أحد المستويات الموثّقة: عيّن `output_format_native_write_json_as_string = 1` لاستخدام [الرجوع إلى String](#json-tier-1-string-fallback)، أو `output_format_native_use_flattened_dynamic_and_json_serialization = 1` (مع `output_format_native_write_json_as_string = 0`) لاستخدام تخطيط [FLATTENED Object](#json-tier-2-flattened-object).

## إطار الضغط

يستطيع ClickHouse ضغط بيانات الأعمدة في دفق `Native` باستخدام صيغة إطار داخلية. و[تخطيط الإطار](#frame-format) الموضح أدناه **مستقل عن وسيلة النقل**، إذ تظهر الإطارات نفسها في بروتوكول TCP الأصلي وعبر HTTP على حد سواء، غير أن طريقة طلب الضغط وما يحيط بالإطارات يختلفان باختلاف وسيلة النقل.

* **بروتوكول TCP الأصلي.** الضغط اختياري ويُفعَّل لكل استعلام على حدة عبر العلامة `compression` في [حزمة الاستعلام](https://clickhouse.com/docs/ar/resources/develop-contribute/native-protocol/protocol.md#query). وعند تفعيله، يُغلَّف متن كل حزمة من الحزم `Data` و`Totals` و`Extremes` و`Log` و`ProfileEvents`، أي البايتات التي تلي سلسلة `table_name`، بصيغة الإطار. أما غلاف الحزمة نفسه، المؤلف من رمز نوع الحزمة وسلسلة `table_name`، فهو **غير** مضغوط، إذ يكتبهما الخادم في الدفق الخام. وكل ما يُصدره `NativeWriter` يدخل في الدفق المضغوط، ولذا تكون البادئة `BlockInfo` أول ما يرد داخل الإطار، إلى جانب الأبعاد والأعمدة. ومن ثَمّ يجب على العميل فك ضغط الإطار قبل أن يتمكن من قراءة `BlockInfo`.
* **HTTP.** يُغلِّف `SELECT ... FORMAT Native&compress=1` دفق بايتات `FORMAT Native` بأكمله بالإطارات نفسها (إذ يستخدم الخادم `CompressedWriteBuffer` الداخلي ذاته)، ويتوقع `?decompress=1` الإطارات نفسها في متن *إدخال* بصيغة `Native`، فيفك ترميزها عبر `CompressedReadBuffer` المقابل. ولا وجود في هذا المسار لنوع حزمة TCP ولا لـ `table_name` ولا لغلاف الحزمة، فالحمولة المضغوطة بأكملها ليست سوى كتل `Native` مؤطَّرة (ولا تظهر البادئة `BlockInfo` إلا إذا كانت قيمة revision المتفاوض عليها أكبر من `0`، تمامًا كما في التخطيط غير المضغوط أعلاه). ويختلف هذا التأطير الداخلي `compress`/`decompress` عن ضغط النقل في HTTP (`Content-Encoding: gzip`/`zstd`، المُفعَّل عبر `enable_http_compression`)، الذي يُغلِّف الاستجابة على مستوى طبقة HTTP ولا علاقة له بصيغة الإطار الموضحة أدناه.

وعليه، فإن العميل الذي لم ينفّذ سوى تخطيط `FORMAT Native` غير المضغوط لا بد له من إضافة طبقة الإطار هذه كي يتمكن من قراءة استجابة `Native` مضغوطة عبر HTTP، أو من إرسال متن طلب مع `decompress=1`.

### صيغة الإطار

```text theme={null}
[16 bytes: CityHash128 checksum over the 9-byte header + compressed body]
[1 byte:   method]                 ← 0x82 = LZ4, 0x90 = ZSTD, 0x02 = NONE
[4 bytes:  compressed_size LE u32] ← INCLUDES the 9-byte header, EXCLUDES the 16-byte checksum
[4 bytes:  uncompressed_size LE u32]
[N bytes:  compressed body]        ← N = compressed_size - 9
```

يبلغ الحجم الإجمالي للإطار `16 + compressed_size` = `16 + 9 + body_size` = `25 + body_size`. لاحظ وجود نطاقين مختلفين: تغطي قيمة التحقق الترويسة البالغة 9 بايتات إضافةً إلى المتن، بينما يشمل `compressed_size` الترويسة والمتن **دون** قيمة التحقق نفسها:

![حقول إطار الضغط وحدود الحجم المضغوط](https://mintcdn.com/private-7c7dfe99/kA9td9fgr8TRWXKS/images/interfaces/native-format/compression-frame.webp?fit=max\&auto=format\&n=kA9td9fgr8TRWXKS\&q=85\&s=1d0d4d1a7615bd7bf38eee8504fc7296)

### قيم بايت الطريقة

هذه المرمِّزات الثلاثة هي التي يُنتجها الخادم عند تأطير `Native` للدفق بأكمله: يستخدم إخراج HTTP مع `compress=1` المرمِّز الافتراضي للخادم (`ZSTD(3)`)، بينما يستخدم بروتوكول TCP الأصلي أحد المرمِّزات `LZ4` أو `ZSTD` أو `NONE` بحسب قيمة `network_compression_method`. ولا يحتاج أي عميل `Native` عام إلا إلى إنتاج هذه المرمِّزات واستهلاكها.

| البايت | الطريقة | ترميز المتن |
| - | - | - |
| `0x02` | NONE | المتن هو البايتات الخام (دون ضغط). ومع ذلك يُصدَر الإطار، ويتحقق المُستقبِل من قيمة التحقق. |
| `0x82` | LZ4 | المتن بـ**صيغة كتلة LZ4** — *وليس* بصيغة إطار LZ4. ولا يتضمن رقمًا سحريًا. |
| `0x90` | ZSTD | المتن دفق zstd خام بإطار واحد (الرقم السحري القياسي لـ zstd جزء من المتن). |

يُرمِّز بايت الطريقة أيضًا [المرمِّزات على مستوى العمود](https://clickhouse.com/docs/ar/reference/statements/create/table/codec.md). تُطبَّق هذه المرمِّزات على كل عمود على حدة في مسارات MergeTree على القرص، لا على تأطير الدفق بأكمله، غير أن مسار إدخال HTTP مع `decompress=1` يأخذ المرمِّز من بايت الطريقة في كل إطار، ولذا قد يظهر أيٌّ من هذه البايتات في الإدخال بصورة مشروعة. لذلك يجب على أي أداة فك الترميز متوافقة أن تتعرّف على النطاق المُخصَّص بأكمله، وأن ترفض أي بايت لا تدعمه بدلًا من أن تُسيء قراءة المتن. أما متون هذه المرمِّزات فخاصة بكل مرمِّز وتقع خارج نطاق عقد الإطار العام هذا:

| البايت | الطريقة |
| - | - |
| `0x91` | `Multiple` (مرمِّز مُركَّب يغلّف سلسلة من المرمِّزات المتداخلة) |
| `0x92` | `Delta` |
| `0x93` | `T64` |
| `0x94` | `DoubleDelta` |
| `0x95` | `Gorilla` |
| `0x96` | `AES_128_GCM_SIV` (تشفير) |
| `0x97` | `AES_256_GCM_SIV` (تشفير) |
| `0x98` | `FPC` |
| `0x9a` | `GCD` |
| `0x9c` | `ALP` |
| `0x9d` | `SZ3` |
| `0x9e` | `Quantized` |
| `0x9f` | `ZXC` |

يخزّن `DoubleDelta` (`0x94`) الفروق من الدرجة الثانية على هيئة بتات بادئة/إشارة/مقدار بترتيب البت الأعلى أولًا (MSB-first). ويحافظ فك الترميز القائم على الكلمات على هذا الترميز وعلى عدد العناصر والحشو في البايت الأخير.

`0x9c` (`ALP`) مرمِّز **تجريبي (beta)** بلا فقدان لـ `Float32` و`Float64` (ضغط تكيّفي بلا فقدان للأعداد ذات الفاصلة العائمة). لا يمكن إنشاء جدول باستخدام `CODEC(ALP)` إلا عند تفعيل الإعداد `enable_alp_codec`، لكن بايت الطريقة يُقبَل دائمًا عند فك الضغط لتظل البيانات المكتوبة سابقًا قابلة للقراءة. أما `0x9d` (`SZ3`) فهو مرمِّز **تجريبي** *مع فقدان* ومحدود الخطأ لـ `Float32` و`Float64` و`Array` من هذين النوعين. لا يمكن إنشاء جدول باستخدام `CODEC(SZ3)` إلا عند تفعيل الإعداد `enable_sz3_codec`، لكن بايت الطريقة يُقبَل دائمًا عند فك الضغط لتظل البيانات المكتوبة سابقًا قابلة للقراءة. أما `0x9f` (`ZXC`) فهو مرمِّز LZ **تجريبي** غير متناظر: بطيء في الضغط لكنه سريع جدًا في فك الضغط، بنسبة ضغط تقع بين `LZ4` و`ZSTD`. لا يمكن إنشاء جدول باستخدام `CODEC(ZXC)` إلا عند تفعيل الإعداد `enable_zxc_codec`، لكن بايت الطريقة يُقبَل دائمًا عند فك الضغط لتظل البيانات المكتوبة سابقًا قابلة للقراءة. وقد خُصِّص البايتان `0x99` (`DeflateQpl`) و`0x9b` (`ZSTD_QPL`) لمرمِّزات أُزيلت لاحقًا؛ وهما محجوزان ولا يُعاد استخدامهما.

`0x9e` (`Quantized`) مرمِّز أعمدة **تجريبي** لأعمدة المتجهات الكثيفة (`Array(Float32)` وما شابهها). وهو، مثل `NONE`، مرمِّز تمريري — إذ يُخزَّن المتن بدقته الكاملة كما هو — لكن وجوده يُلحق تسلسلًا يكتب دفقًا مرافقًا مُكمَّمًا ومدمجًا يُستخدم لتسريع البحث المتجهي. لا يمكن إنشاء جدول باستخدام `CODEC(Quantized(...))` إلا عند تفعيل الإعداد `enable_quantized_codec`، ويُقبَل بايت الطريقة دائمًا عند فك الضغط.

### قيمة التحقق

يستخدم ClickHouse الإصدار CityHash v1.0.2 (الإصدار التاريخي)، **وليس** إصدار Google CityHash الحديث؛ إذ يُنتج كلٌّ منهما مخرجات مختلفة عن الآخر.

تُحسب قيمة التحقق على بايتات الترويسة التسعة (method + compressed\_size + uncompressed\_size) إضافةً إلى بايتات المتن البالغ عددها N، أي كل ما يقع بين قيمة التحقق ونهاية الإطار. تمثّل البايتات الثمانية الأولى من ناتج CityHash128 البالغ طوله 16 بايتًا النصفَ المنخفض (LE)، بينما تمثّل البايتات الثمانية التالية النصفَ المرتفع (LE). تعيد أداة فك الترميز حساب CityHash128 على الترويسة والمتن المستلمَين، ثم تقارن النتيجة بالبايتات الستة عشر الأولى؛ وأي عدم تطابق يُعدّ تلفًا في البيانات، فيفشل فك الترميز.

### حدود كل كتلة

الحمولة المضغوطة لكتلة هي **دفق من إطار واحد أو أكثر**، وليست بالضرورة إطارًا واحدًا. يكتب المُرسِل الكتلة المُسلسَلة عبر `CompressedWriteBuffer` الذي يُصدر إطارًا كلما امتلأ مخزنه المؤقت الداخلي (≈1 MB، `DBMS_DEFAULT_BUFFER_SIZE`)، ثم إطارًا أخيرًا عند تفريغ الكتلة. وعليه، تشغل الكتلة الصغيرة إطارًا واحدًا، بينما تشغل الكتلة الكبيرة عدة إطارات متتالية.

يسري هذا الثابت في اتجاه واحد فقط: فبما أن المُرسِل يُفرّغ المخزن المؤقت المضغوط في نهاية كل كتلة، **فإن نهاية كل كتلة تتطابق مع حدّ إطار** — غير أن العكس لا يصح. فحدّ الإطار الوسيط، الذي يُصدَر عند امتلاء المخزن المؤقت أثناء الكتلة، يقع في *منتصف* الكتلة ولا يُعدّ حدًّا لها. لذا يجب على أداة فك الترميز أن تعتمد على أبعاد الكتلة نفسها (`num_columns`/`num_rows`) لتحديد موضع انتهائها، ولا يجوز لها أن تفترض أن كل إطار يمثّل كتلة كاملة.

يستقبل المُستقبِل الإطارات على هيئة دفق: يقرأ 16 + 9 بايت، ثم يقرأ `compressed_size - 9` بايت بالضبط من المتن، ويفك ضغطها إلى `uncompressed_size` بايت بالضبط، ثم يمرّر هذه البايتات إلى أداة فك ترميز الكتل؛ وعندما تحتاج أداة فك الترميز إلى بيانات تتجاوز ما يحويه الإطار الحالي، يسحب الإطار التالي. ولأن المُرسِل يُفرّغ البيانات عند نهاية كل كتلة، يصبح المخزن المؤقت للإطار فارغًا بعد اكتمال فك ترميز الكتلة، وتبدأ الكتلة التالية في إطار جديد.

في بروتوكول TCP الأصلي، يُكتب غلاف الحزمة — أي قيمة VarUInt التي تحدد نوع الحزمة وسلسلة `table_name` من النوع string — في الدفق **الخام**، خارج الحمولة المضغوطة؛ ولا يُؤطَّر سوى متن الكتلة (BlockInfo + الأعمدة). أما مسار `compress`/`decompress` عبر HTTP فلا يتضمن مثل هذا الغلاف، إذ يتكوّن الدفق بأكمله من كتل مؤطَّرة.

### التفاوض

في بروتوكول TCP الأصلي، يُحدَّد الضغط لكل استعلام على حدة لا لكل اتصال. إذ يطلب الحقل `compression: bool` في حزمة الاستعلام الضغطَ لذلك الاستعلام وحده. فيلبّي الخادم الطلب ويُرسل متون `Data`/`Totals`/`Extremes`/`Log`/`ProfileEvents` مضغوطةً طوال مدة الاستعلام (أما `Log`/`ProfileEvents` فبدءًا من الإصدار v54481 فقط). كما يتوقع أن تكون كتل Data *الصادرة* من العميل — الجداول الخارجية، ووسم نهاية البيانات الفارغ، وصفوف INSERT — مؤطَّرةً بالطريقة نفسها. وقد تختلف الاستعلامات اللاحقة على الاتصال نفسه في ذلك.

أما عبر HTTP فلا توجد حزمة الاستعلام: إذ تختار معلمة الاستعلام `compress=1` إخراجًا مؤطَّرًا لذلك الطلب، بينما تُشير `decompress=1` إلى أن متن الطلب مؤطَّر. ويُكتب إخراج `compress=1` باستخدام المرمِّز الافتراضي للخادم (`ZSTD(3)`) لا باستخدام `network_compression_method`؛ في حين يستخلص قارئ `decompress=1` المرمِّزَ من بايت الطريقة في كل إطار، ولذا يُقبل أي مرمِّز في الإدخال.

> [!NOTE]
>
> عند تفعيل الضغط، قد يمرّر الخادم الأعمدة أيضًا عبر مسار التنظيم المتوازي للكتل / `ColumnBLOB` (`PARALLEL_BLOCK_MARSHALLING`، الإصدار v54478) في الكتل التي تحتوي على أكثر من صف واحد. لذا يجب على أي تنفيذ يضغط بيانات INSERT أن يكون مستعدًا للتعامل مع هذا المسار (أو أن يعطّله صراحةً) تجنبًا لاختلال تزامن الدفق.

## مسرد المصطلحات

**كتلة** — وحدة تبادل البيانات في صيغة Native، وهي كتلة ذاتية الوصف من الصفوف المخزّنة بشكل عمودي. راجع [بنية الكتلة والعمود](#block-and-column-structure).

**BlockInfo** — ترويسة البيانات الوصفية التي تسبق الكتلة في مسار حزم Data عبر TCP (وتُكتب متى كانت revision الاتصال أكبر من صفر). وهي سلسلة من الحقول الموسومة بمعرّفات حقول، ويتوقف وجود كلٍّ منها على الـ revision. لا يتضمنها تنسيق الإخراج `Native`، إذ يُجري التسلسل عند revision `0`. راجع [BlockInfo](#blockinfo).

**متن العمود** — بايتات العمود التي تحمل القيم الفعلية، وتأتي بعد ترويسة العمود (الاسم، والنوع، وبايت has\_custom\_serialization). ويختلف تخطيطه باختلاف النوع. راجع [تخطيط العمود على الشبكة](#column-wire-layout).

**النوع المركّب** — نوع مبني من نوع داخلي واحد أو أكثر، ويُرمَّز على هيئة عدة تدفقات لكل عمود. صيغة النقل الخاصة به مستقرة ولا تخضع لإصدارات. راجع [الأنواع المركّبة](#composite-types).

**القاموس (LowCardinality)** — مصفوفة القيم الفريدة التي يشير إليها عمود `LowCardinality(T)` عبر فهارس عددية صحيحة. راجع [LowCardinality](#lowcardinality).

**الكتلة الفارغة** — كتلة تكون فيها `num_columns = 0` و`num_rows = 0`. تُستخدم قيمةً حارسة: فهي وسم لنهاية الإدخال من جهة العميل، ووسم لحدود الدفق من جهة الخادم. راجع [بدائل الكتلة](#block-variants).

**كتلة الترويسة** — كتلة تكون فيها `num_columns > 0` و`num_rows = 0`، يرسلها الخادم أولَ حزمة Data في استجابة الاستعلام، وتُعلن عن مخطط النتيجة. راجع [بدائل الكتلة](#block-variants).

**النوع الداخلي** — النوع الذي يغلّفه النوع المركّب. فالنوع الداخلي لـ `Array(UInt32)` هو `UInt32`، والنوع الداخلي لـ `Nullable(T)` هو `T`.

**دفق الإزاحات** — مصفوفة UInt64 من مواضع النهاية التراكمية، تستخدمها `Array` و`Map` و`Nested` لتحديد حدود العناصر في كل صف. راجع [Array](#array).

**القيمة البديلة المؤقتة** — البايتات المكتوبة في مواضع القيم الفارغة ضمن دفق القيم لعمود `Nullable(T)`. تقرؤها أداة فك الترميز لتتقدّم في الدفق، لكنها تتجاهل محتواها. راجع [Nullable](#nullable).

**كتلة النتيجة** — كتلة تكون فيها `num_rows > 0`، وتحمل صفوفاً فعلية من نتيجة الاستعلام. راجع [بدائل الكتلة](#block-variants).

**كتلة المخطط** — مرادف لكتلة الترويسة، يُستخدم عند وصف مرحلة INSERT، حيث تُبلغ كتلة المخطط العميلَ بأشكال الأعمدة المتوقعة.

**إصدار التسلسل** — رقم إصدار خاص بكل نوع يُنقل عبر الشبكة، تستخدمه الأنواع ذات الإصدارات للإعلان عن بديل الترميز الذي يليه. ويختلف عن إصدار البروتوكول. راجع [إصدار التسلسل: المفهوم](#serialization-version-concept).

**بادئة الحالة** — البايتات التي تسبق حمولة الكتلة في الأنواع ذات الإصدارات. تحمل إصدار التسلسل و(في حالة LowCardinality) البيانات الوصفية للقاموس الخاصة بكل كتلة. تُرسَل في بداية كل كتلة يزيد عدد صفوفها على 0، ولا يُحتفظ بها من كتلة إلى أخرى.

**الدفق** — سلسلة متصلة من البايتات داخل متن العمود، ترمّز مكوّناً فرعياً منطقياً واحداً (خريطة القيم الفارغة، أو مصفوفة الإزاحات، أو دفق القيم). وتضمّ الأنواع متعددة التدفقات دفقين أو أكثر متتاليين لكل عمود.
