INSERT، وبيانات القياس عن بُعد الخاصة بالتنفيذ، وإشارات الأخطاء. وعليه يعتمد عميل سطر الأوامر، ومشغّل C++، ومعظم المشغّلات الأصلية التابعة لجهات خارجية.
تتناول هذه الصفحة البروتوكول نفسه: تأطير الحزم، وآلة حالات الاتصال، والتفاوض على الإصدار، ومتن كل رسالة من غير نوع Block. أما البايتات داخل حزم عائلة Data (أي Block وأعمدتها وترميزات كل نوع) فهي مسألة مستقلة، موثّقة في مواصفة صيغة Native.
المواصفة المرافقةهذه الصفحة جزء من مواصفتين متكاملتين، وتُنشر جنبًا إلى جنب مع المواصفة المرافقة صيغة Native. وتتقاسم المواصفتان العمل بحدود واضحة: تختص هذه الصفحة بطبقة الحزم والنقل، بينما تختص مواصفة صيغة Native بالبايتات داخل حزم عائلة
Data.BlockInfo، ولذا فإن بايتًا واحدًا في غير موضعه يُخلّ بتزامن كل ما يليه. وهو كذلك بروتوكول ذو حالة، يعالج فيه كل اتصال TCP استعلامًا واحدًا في كل مرة، فلا يدعم تعدد الإرسال. أما الأعداد الصحيحة ثابتة العرض فتُخزَّن بترتيب little-endian.
نظرة عامة
تبدأ كل رسالة في تنسيق النقل برمز نوع الحزمة بصيغة
VarUInt، يليه متن تتحدد بنيته وفقًا لهذا الرمز ولإصدار البروتوكول المتفق عليه.
يمر الاتصال بثلاث مراحل: مصافحة تُجرى مرة واحدة، ثم أي عدد من عمليات تبادل Ping أو Query، ثم الإغلاق:
ينقل بروتوكول TCP الأصلي البيانات الجدولية دائمًا بصيغة Native، بصرف النظر عن أي عبارة FORMAT في استعلام SQL. أما إعادة تحويلها إلى RowBinary أو CSV أو JSON وغيرها فهي مهمة العميل، ويتولاها بعد فك ترميز كتل Native. (أما واجهة HTTP فتعتمد مسار شيفرة مختلفًا يلتزم فعلًا بعبارة FORMAT، إلا أن HTTP يقع خارج نطاق هذا المستند.)
الأمان
أمان النقل (TLS)
يعمل TLS في طبقة النقل، أي في مستوى أدنى من البروتوكول. وعند تفعيله يُشفَّر تدفق TCP بأكمله، وتظل رسائل البروتوكول متطابقة بايتًا ببايت، سواء استُخدم TLS أم لم يُستخدم.المصادقة
تتم المصادقة أثناء المصافحة، ضمن رسالةClientHello. ويُرسَل الحقلان user وpassword على هيئة سلاسل نصية بنص صريح، لذا فإن التشفير على مستوى النقل (TLS) هو ما يحمي بيانات الاعتماد أثناء انتقالها عبر الشبكة.
يفسّر الخادم الحقل user الفارغ على أنه مستخدم الجلسة الافتراضي المُهيّأ لديه، أي قيمة إعداد الخادم default_session_user (وقيمته الافتراضية default)، ويمكن استبدال هذه القيمة لكل مستمع على حدة في القسم protocols. وإذا هُيّئ الخادم بمستخدم جلسة افتراضي فارغ، أو كان إصداره أقدم من 26.8، فسيُرفض الحقل user الفارغ ويُطلَق استثناء. لاحظ أن clickhouse-client لا يرسل اسم مستخدم فارغًا أبدًا، إذ يستعيض عنه بالقيمة default من جهة العميل.
تتوفر مصادقة SSH القائمة على التحدي والاستجابة بدءًا من إصدار البروتوكول 54466 — راجع مصادقة SSH القائمة على التحدي والاستجابة.
السر المشترك بين الخوادم
عند تنفيذ الاستعلامات الموزعة، تتحقق الخوادم من هوية بعضها بعضًا بإثبات معرفتها بـ secret مشترك، دون إرسال الـ secret نفسه في تنسيق النقل. تحمل كل رسالة Query قيمةauth_hash بطول 32 بايت محسوبة بخوارزمية SHA-256 في الحقل 4 من Query. وتُحسب هذه القيمة من ملح (salt) وقيمة nonce والـ secret المُهيأ ونص الاستعلام، ثم يعيد الخادم المستقبِل حسابها ويقارنها بالقيمة المستلمة. ولا تتوفر هذه الآلية إلا عند تفعيل الميزة INTERSERVER_SECRET (v54441). أما العملاء الخارجيون فيرسلون دائمًا سلسلة فارغة في هذا الحقل. راجع المصادقة بين الخوادم.
إدارة الإصدارات وبوابات الميزات
التفاوض على الإصدار
يُعلن كلٌّ من العميل والخادم أثناء المصافحة عن أعلى إصدار من البروتوكول يدعمه. ويكون الإصدار المتفق عليه هو الأدنى بين الإصدارين:بوابات الميزات
تُعرَّف كل ميزة بإصدار البروتوكول الذي أُضيفت فيه، وتكون نشطة متى كان الإصدار المتفق عليه أكبر من ذلك الرقم أو مساويًا له.جدول الميزات
غلاف الحزمة
تتشارك كل رسالة في تنسيق النقل البنيةَ الخارجية نفسها، في كلا الاتجاهين:VarUInt وليس بايتًا ثابت العرض. ففي القيم الأقل من 128 يُنتج VarUInt البايت المفرد نفسه، لكن يجب على التطبيقات استخدام ترميز VarUInt كي تبقى متوافقة إذا بلغت أنواع الحزم المستقبلية القيمة 128 أو تجاوزتها.
لا يوثّق مرجع الرسائل سوى المتن في كل حزمة، أي البايتات التي تلي رمز نوع الحزمة. ويبدأ ترقيم الحقول من 1 عند أول حقل في المتن.
التأطير المجزأ (v54470+)
عند التفاوض على الميزةCHUNKED_PROTOCOL (راجع المصافحة)، تُغلَّف كل حزمة في تنسيق النقل بتأطير مجزأ. ويتم هذا التغليف لكل اتجاه على حدة: إذ يُتفاوَض على اتجاه العميل←الخادم (client→server) واتجاه الخادم←العميل (server→client) كلٌّ على حدة، وقد ينتهي كلٌّ منهما إلى وضع مختلف (مجزأ أو غير مؤطَّر).
تخطيط wire لكل حزمة:
VarUInt الخاص بنوع الحزمة داخل الدفق المقسّم إلى مقاطع: فهو البايت الأول من حمولة الحزمة (البايت الأول من المقطع الأول)، وليس بايتًا منفصلًا يُرسَل قبل التأطير. تتكوّن حمولة المقطع لكل حزمة من [VarUInt packet_type_code][message body] كاملًا كما هو موضح في غلاف الحزمة. وإذا أبقى العميل نوع الحزمة خارج الدفق المقسّم إلى مقاطع، فسيقرأ الطرف الآخر بايت النوع هذا على أنه البايت الأول من حجم المقطع u32، مما يُفقد الاتصال تزامنه.
قد تتوزّع الحزمة الواحدة على عدة مقاطع إذا امتلأ المخزن المؤقت لدى الكاتب في منتصف الحزمة، وقد يقع التقسيم في أي موضع، حتى داخل VarUInt الخاص بنوع الحزمة. يدمج القارئ حمولات المقاطع، ويعامل الأصفار الأربعة الختامية (4 بايت) بوصفها حدًّا شفافًا للحزمة، أي أنه يستهلكها دون أن يمرّرها إلى أي جهة تقرأ متن الحزم.
تُغلَّف الحزم التي لا متن لها كذلك: فالحزمة المكوّنة من بايت واحد مثل Ping أو Pong تصبح [u32 size = 1][0x04][u32 0] بمجرد الاتفاق على التقسيم إلى مقاطع. وأي وصف لـ “بايت واحد في تنسيق النقل” في موضع آخر من هذه الصفحة يشير إلى الشكل السابق للتقسيم إلى مقاطع.
التفاوض. تحمل كلٌّ من ServerHello وAddendum حقلين من النوع String، حقلًا لكل اتجاه، وتُؤخذ قيمهما من {"chunked", "notchunked", "chunked_optional", "notchunked_optional"}:
- القيمتان
chunked/notchunkedصارمتان: أي أن الطرف المعني يشترط هذا الوضع تحديدًا. - البدائل
_optionalمرنة: إذ تقبل أي وضع يختاره الطرف الآخر.
من جهة العميل، يُطابَق تفضيل الإرسال (SEND) لدى العميل مع تفضيل الاستقبال (RECV) لدى الخادم، والعكس صحيح.
التوقيت. تنتقل سلاسل التفاوض عبر قناة النقل دون تأطير: ClientHello ← ServerHello (تفضيلات الخادم) ← Addendum (القيم التي تفاوض عليها العميل). ويسري التحوّل إلى التأطير على كل بايت يُرسَل بعد تفريغ Addendum. أما Addendum نفسها وClientHello وServerHello فتُرسَل دائمًا دون تأطير.
دورة حياة الاتصال
يكون الاتصال في أي لحظة في حالة واحدة فقط من أربع حالات:HANDSHAKE أو READY أو READING_RESPONSE أو منتهيًا. ولأن البروتوكول لا يدعم تعدد الإرسال، فإن العميل الذي يرسل طلبًا جديدًا قبل تصريف الاستجابة الواردة السابقة يتسبب في تداخل البايتات في تنسيق النقل، مما يُفسد الدفق.
الحالات
يمتد المسار الناجح في خط مستقيم نحو الأسفل —HANDSHAKE → READY → READING_RESPONSE → READY — مع الحلقة الذاتية Ping/Pong، بينما تصبّ جميع حواف الفشل في المصبّ الوحيد Terminated.
مرحلة المصافحة
تتولى هذه المرحلة المصادقة والتفاوض على إصدار البروتوكول، وتحدث مرة واحدة فقط لكل اتصال، قبل أي عملية أخرى. عند بدء هذه المرحلة، يكون اتصال TCP قد فُتح للتو دون تبادل أي رسائل بعد. وتسير العملية على النحو التالي:-
يرسل العميل
ClientHelloمتضمنًا أعلى إصدار يدعمه من البروتوكول. -
يقرأ العميل الاستجابة ويوجّهها بحسب نوع الحزمة:
-
إذا كان
negotiated_version ≥ 54458(ميزةADDENDUM)، يرسل العميلAddendum. ويستند هذا القرار إلى الإصدار المتفق عليه، لا إلى الإصدار الذي أعلنه العميل.
READY؛ وعند حدوث أي error يُنهى الاتصال.
مرحلة Ping
فحصٌ لحيوية الاتصال على مستوى التطبيق، مستقلٌّ عن آلية TCP keepalive. ويؤكد نجاح تبادل Ping/Pong ذهابًا وإيابًا أن اتصال TCP لا يزال قائمًا في كلا الاتجاهين وأن الخادم يستجيب. ورسالة Ping عديمة الحالة (stateless) ولا ترتبط بأي استعلام، ولذلك تكون رسائل Ping المتتالية مستقلةً بعضها عن بعض. بدءًا من الحالةREADY، يسير التدفق على النحو التالي:
-
يرسل العميل
Ping. -
يقرأ العميل الاستجابة:
مرحلة الاستعلام
يُرسل العميل عبارة SQL، فيبثّ الخادم في المقابل كتل النتائج وبيانات القياس عن بُعد الخاصة بالتنفيذ. وتتألف الاستجابة من سلسلة من الحزم تنتهي بحزمة واحدة فقط، إماEndOfStream أو Exception.
بدءًا من الحالة READY، يسير التدفق على النحو التالي:
عند حدوث خطأ في أي مرحلة، يرسل الخادم Exception بدلًا من EndOfStream، فيُنهى الاستعلام.
-
يرسل العميل
Queryمعquery_idفريد (عادةً UUID). -
يرسل العميل الجداول الخارجية إن وُجدت، ثم وسم Data الفارغ. تحتوي حزمة Data الفارغة على
table_name = ""وnum_columns = 0وnum_rows = 0. لا يبدأ الخادم تنفيذ الاستعلام إلا بعد استلام هذا الوسم. يمكن تقسيم الجدول الخارجي على عدة حزمDataتحملtable_nameنفسه؛ إذ تُثبّت أولى هذه الحزم مخطط الجدول، ويجب أن تحمل كل حزمة لاحقة بالاسم نفسه أسماء الأعمدة وأنواعها ذاتها وبالترتيب ذاته، وإلا رفض الخادم الاستعلام بالخطأINCORRECT_DATA(راجع Data). -
ينتقل العميل إلى الحالة
READING_RESPONSEويُفرغ مخزن الكتابة المؤقت الخاص به. -
يقرأ العميل حزم الاستجابة في حلقة، ويعالج كل حزمة بحسب نوعها:
EndOfStream أو Exception جرت معالجته، يعود الاتصال إلى READY. أما انتهاك البروتوكول أو خطأ الإدخال/الإخراج فيؤدي إلى إنهاء الاتصال.
كثيرًا ما تقع التطبيقات الجديدة في خطأ بسبب حالة
num_rows == 0. فالكتلة الخالية من الصفوف هي وسم حدّ أو ترويسة مخطط، وليست إشارة إلى نهاية الدفق. ولا تنتهي الاستجابة إلا بـ EndOfStream أو Exception.مرحلة INSERT
مرحلة INSERT هي مرحلة الاستعلام مضافًا إليها تبادلان إضافيان. يرسل العميل عبارةINSERT، فيردّ الخادم بـ كتلة المخطط التي تصف جدول الهدف، ثم يبثّ العميل حزم Data التي تحمل الصفوف، تليها حزمة Data فارغة بوصفها وسمًا للنهاية، وأخيرًا ينهي الخادم التبادل بـ EndOfStream أو Exception.
انطلاقًا من الحالة READY، تكون عبارة SQL عبارة INSERT بالصيغة INSERT INTO <table> [(<cols>)] VALUES، دون أي قيمة حرفية مضمّنة من نوع VALUES (...)، لأن بيانات الصفوف تنتقل عبر حزم Data. ويسير التدفق على النحو التالي:
- يرسل العميل
Queryمع تعيينbodyإلى استعلام INSERT بلغة SQL. - يرسل العميل أي جداول خارجية (وهو أمر نادر مع INSERT). وخلافًا لما يحدث في مرحلة الاستعلام، فإنه لا يرسل هنا وسم Data فارغًا. إذ تُرسَل حزمة
Queryالخاصة بـINSERTمع وجود بيانات معلّقة، ولذلك تُؤجَّل الكتلة الفارغة الدالة على نهاية البيانات إلى الخطوة 5؛ فلو أُرسلت قبل كتلة المخطط، لقرأها الخادم على أنها نهاية دفق الصفوف، فيُنهي عملية INSERT دون أي صفوف، ثم يحلّل أول حزمة صفوف فعلية على أنها حزمة شاردة في المستوى الأعلى. - يُصرِّف العميل حزم البيانات الوصفية (TableColumns وProgress وProfileInfo وLog وProfileEvents) إلى أن يقرأ حزمة Data الخاصة بالمخطط، وهي Block تحتوي على 0 صفوف لكنها تتضمن البنية الكاملة للأعمدة (الأسماء والأنواع). وتمثّل كتلة المخطط العقد المُلزِم: إذ يجب أن تطابق الصفوف التي يرسلها العميل بعد ذلك بنية هذه الأعمدة.
- يرسل العميل كتلة (أو كتل) البيانات. ولكل كتلة يكتب
VarUInt(ClientPacket::Data = 2)، ثمString("")لاسم الجدول الخارجي الفارغ، ثم الـ Block. ويجب أن تتوافق أنواع الأعمدة مع أعمدة كتلة المخطط بحسب الموضع. - يرسل العميل علامة إنهاء المدخلات: حزمة Data تحتوي على Block فارغة (0 أعمدة، 0 صفوف).
- يُصرِّف العميل دفق الاستجابة حتى
EndOfStream(في حال النجاح) أوException(في حال الفشل).
async_insert = 1، يضع الخادم الصفوف في قائمة انتظار ثم يُفرّغها ضمن دفعة. وعند الإصدار المتفق عليه ≥ 54484 (PROGRESS_IN_ASYNC_INSERT)، يُرسل الخادم فور اكتمال التفريغ حزمة Progress إضافية، تليها مباشرةً ProfileEvents الخاصة بعملية الإدراج، ثم EndOfStream. أما في الإصدارات الأدنى من 54484 فلا يرسل الخادم حزمة Progress الختامية هذه. وهذه الحزمة هي Progress عادية؛ ولأن الخادم يعيد تعيين مسار تنفيذ الاستعلام قبل احتساب أعداد الكتابة، فإن الزيادة لا تحمل عمليًا سوى الوقت المنقضي، بينما تصل إحصاءات الصفوف والبايتات المكتوبة إلى العميل عبر ProfileEvents المرافقة. ولا يحتاج العميل الذي يُصرِّف أصلًا حزم Progress المتداخلة في الخطوة 6 إلا إلى قبول حزمة إضافية واحدة.
يعود الاتصال إلى الحالة READY عند تلقّي EndOfStream أو Exception تمت معالجته. أما انتهاكات البروتوكول وأخطاء الإدخال/الإخراج فتُنهي الاتصال.
مرجع الرسائل
تُسرد الحقول بحسب ترتيب تسلسلها في البيانات المُرسلة. ويستخدم العمودType القيم التالية:
VarUInt— عدد صحيح بلا إشارة متغير الطول (راجع VarUInt).String— بايتات مسبوقة بطولها مُرمَّزًا بصيغة VarUInt (راجع String).UInt8وInt32وما شابهها — أعداد صحيحة ثابتة العرض بترتيب little-endian.Bool— بايت واحد قيمته0x00أو0x01.
Role الجهة التي تستخدم كل حقل:
- client — يضبطه العملاء الخارجيون.
- inter-server — لا يُعتدّ به إلا في الاتصال بين الخوادم، ويكتب فيه العملاء الخارجيون القيمة الافتراضية.
- universal — يستخدمه الطرفان.
ClientHello (نوع الحزمة 0)
Client → Server. أول رسالة تُرسَل بعد فتح اتصال TCP.ServerHello (نوع الحزمة 0)
Server → Client. الرد على ClientHello عند نجاح المصادقة.
Rule — عنصر من عناصر
password_complexity_rules:
تعكس هذه القائمة إعداد سياسة كلمات المرور الذي ضبطه مشغّل الخادم، وهي استرشادية فقط؛ إذ لا يفرض الخادم هذه القواعد أثناء المصافحة. ويمكن للعميل الذي يتيح تغيير كلمة المرور أو تعيينها أن يستخدم هذه القواعد لتنبيه المستخدم إلى الأخطاء قبل إرسال كلمة مرور غير مستوفية للشروط إلى الخادم.
للحدّ من استهلاك الموارد في مواجهة خادم خبيث أو سيئ التهيئة، اجعل الحد الأقصى لقيمة
count بعد فك ترميزها 256 مدخلًا، والحد الأقصى لكل من pattern وmessage من النوع String 4096 بايت. وتُعد القيمة 0 لـ count (دون أزواج تالية) الحالة الشائعة في الخوادم التي لم تُهيَّأ فيها سياسة لكلمات المرور.الملحق (Addendum) (بلا نوع حزمة)
Client → Server، ومشروط بـADDENDUM (v54458). يُرسَل فور اكتمال تبادل المصافحة. وهو ليس نوع حزمة مستقلًا، إذ تُرسَل الحقول في تنسيق النقل بصيغتها الخام، دون أن يسبقها بايت نوع الحزمة.
لا يبدأ العمل بالتأطير المقسَّم إلى مقاطع إلا بعد تفريغ هذا الملحق، أما الملحق نفسه فيُرسَل دون تأطير.
Ping (نوع الحزمة 4)
Client → Server. بلا متن — تتألف الحزمة من بايت واحد0x04 قبل تطبيق التأطير المجزأ؛ وعند التفاوض على التجزئة يصبح هذا البايت حمولةً من بايت واحد داخل مقطع (انظر التأطير المجزأ).
Pong (نوع الحزمة 4)
Server → Client. بلا متن — تتكوّن الحزمة من بايت واحد0x04 قبل تطبيق التأطير المجزأ؛ وعند التفاوض على التجزئة يصبح هذا البايت حمولةَ مقطعٍ بطول بايت واحد (راجع التأطير المجزأ).
Exception (نوع الحزمة 2)
من الخادم إلى العميل. تُرسَل عندما يصادف الخادم خطأً في أي مرحلة.Query (نوع الحزمة 1)
Client → Server.ClientInfo (مُضمَّن في Query)
Client → Server، مُضمَّن في متن Query (الحقل 2). يتوقف توفّره علىCLIENT_INFO (v54032). (يتوقف توفّر بعض الحقول داخل ClientInfo على إصدارات لاحقة، كما هو موضح عند كل حقل أدناه.)
تخطيط يعتمد على الواجهة (الحقول 7–12)تمثّل الحقول 7–12 أعلاه فرع TCP. وعندما لا تكون قيمة
query_interface (الحقل 6) هي TCP، تُستبدل هذه الحقول بتخطيط wire مختلف — فالأمر لا يقتصر على حذف حقول اختيارية، ولذلك يجب أن تتفرّع أداة فك الترميز بحسب قيمة الحقل 6.query_interface = 2(HTTP): تُكتب بدلًا منها معلومات طلب HTTP التي يمرّرها الخادم —http_method(UInt8)، وhttp_user_agent(String)، ثمforwarded_for(String، مشروط بـX_FORWARDED_FOR_IN_CLIENT_INFOv54443)، وhttp_referer(String، مشروط بـREFERER_IN_CLIENT_INFOv54447)، وأخيرًاhttp_handler_name(String) وhttp_request_url(String) (كلاهما مشروط بـHTTP_HANDLER_IN_CLIENT_INFOv54490، وبهذا الترتيب). ولا تظهر الحقولos_user/client_hostname/client_name/version_*/protocol_version. يحمل الحقلان الأخيران اسم معالج HTTP المطابِق المعرّف بلغة SQL وعنوان URL الخاص بالطلب، كي يُنقلا إلى المقاطع البعيدة (ويكونان فارغين في استعلام HTTP لا يمرّ عبر معالج).- أي واجهة أخرى: لا يُكتب أيٌّ من حقول TCP (7–12) ولا أيٌّ من حقول HTTP، ويتابع الدفق مباشرةً بالحقل
quota_key.
quota_key (الحقل 13) وdistributed_depth (الحقل 14) في جميع الواجهات، ثم يُكتب version_patch (الحقل 15) في حالة TCP فقط.تبرز أهمية هذا الفرع أساسًا في حركة البيانات بين الخوادم، حين يمرّر الخادم المُبادِر استعلامًا وصل في الأصل عبر HTTP. فأداة فك الترميز التي تقرأ حقول TCP دائمًا ستُخطئ في قراءة هذه الحزم، إذ ستعامل http_method أو http_user_agent على أنه quota_key.المصادقة بين الخوادم
الحقل 4 في حزمة Query (auth_hash) ليس هو secret الخاص بالعنقود المشترك في تنسيق النقل؛ فإرسال القيمة السرية كما هي سيؤدي إلى فشل المصادقة وتسريب القيمة معًا. وبدلًا من ذلك، يُثبت الخادم الذي يعمل بوصفه عميلًا بين الخوادم أنه يعرف القيمة السرية عبر تجزئة SHA-256 مُملَّحة:
- الدخول في وضع الاتصال بين الخوادم. يُعلن الخادم المُتصِل ذلك داخل
ClientHello: إذ يحمل الحقلuserوسم الاتصال بين الخوادم، ويكونpasswordفارغًا. ثم يُضيف سلسلتين نصيتين أخريين، هما اسم العنقود وقيمةsaltبطول 32 بايت تُولَّد حديثًا (encodeSHA256لقيمة عشوائية)، مباشرةً بعد الحقلينuser/passwordضمن حزمةClientHelloنفسها. يقرأ الخادم هاتين السلسلتين قبل أن يُرسلServerHello، لذا يجب على العميل كتابتهما مُسبقًا؛ أما انتظارServerHelloأولًا فيُسبِّب حالة جمود (deadlock)، لأن الخادم يظل متوقفًا في انتظار قراءتهما. - الحصول على القيمة العابرة (nonce). تحمل
ServerHelloقيمة nonce من النوعUInt64بطول 8 بايت عند التفاوض علىINTERSERVER_SECRET_V2(v54462). - حساب التجزئة. في كل حزمة Query ليست من نوع
InitialQuery، يكتب العميلencodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles + current_roles)في الحقل 4، وهو ملخص (digest) بطول 32 بايت. (تُمثَّلnonceبصيغتها النصية العشرية، ولا تُدرَج إلا عند التفاوض على إصدار ≥ v54462؛ ولا يُضافexternal_rolesإلا عند التفاوض علىINTERSERVER_EXTERNALLY_GRANTED_ROLES(v54472)؛ أماcurrent_rolesفهي قائمة أسماء الأدوار المُسلسَلة، أي[VarUInt count][String]*countدون بايت الحضور، ولا تُضاف إلا عند التفاوض علىINTERSERVER_CURRENT_ROLES(v54488) وكانت القائمة موجودة.) أما في حالةInitialQuery، أو إذا لم تتم تهيئة secret الخاص بالعنقود، فيكتب العميل سلسلة فارغة بدلًا من ذلك. - التحقق. يقرأ الخادم الحقل 4 بحد أقصى 32 بايت، ثم يُعيد حساب سلسلة الربط نفسها باستخدام نسخته الخاصة من secret الخاص بالعنقود؛ ويُرفَض الاتصال إذا اختلف الملخصان.
auth_hash فارغة.
الإعداد
يُرمَّز مضمَّنًا داخل قائمة الإعدادات في متن Query (حزمة Query، الحقل 3). وتكون القائمة موجودة دائمًا أيًّا كان الإصدار المتفق عليه، وتنتهي بإعداد ذي مفتاح فارغ، أي بقيمةVarUInt 0 واحدة لا تليها أي علامات أو قيمة. ولا يتوقف على الإصدار المتفق عليه سوى ترميز كل إعداد على حدة، ويتحكم في ذلك SETTINGS_SERIALIZED_AS_STRINGS (v54429).
v54429+ (STRINGS_WITH_FLAGS): يتألف كل إعداد من الثلاثية الموضحة أدناه:
لا يُرسَل الحقلان 2 و3 عندما يكون
key فارغًا.
ما قبل 54429 (BINARY): يأتي كل إعداد على الصورة [String key][type-specific binary value]، إذ لا يُكتب الحقل flags، وتُرمَّز القيمة بالصيغة الثنائية الأصلية للإعداد (كعدد صحيح ثابت العرض أو string مسبوق بطوله مثلًا) بدلًا من string عشري أو نصي. وتنتهي القائمة كذلك بـ key فارغ. ويجب على العميل الذي يستهدف إصدارًا متفقًا عليه أقل من 54429 أن يقرأ هذه الصيغة الثنائية ويكتبها، لا الثلاثية المذكورة أعلاه. (ويُستثنى من ذلك الإعدادات المخصصة التي يعرّفها المستخدم، إذ تحمل دائمًا flags وقيمة من نوع string في كلا الترميزين.)
يجمع الحقل flags ما يلي:
0x01: Important: يؤثر الإعداد في نتائج الاستعلام، ويجب ألا تتجاهله الأطراف الأقدم دون تنبيه.0x02: Custom: إعداد مخصص يعرّفه المستخدم.0x0c: حقل مستوى من بتّين، وليس علامة مستقلة:0x00= Production، و0x04= Obsolete، و0x08= Experimental، و0x0c= Beta. اقرأ البتّين معًا (flags & 0x0c)، فالاكتفاء بالاختبارflags & 0x04سيصنّف Beta (0x0c) خطأً على أنه Obsolete.0x80: HotReload (إعادة تحميل الإعداد دون إعادة التشغيل؛ مُعرَّف في enum العلامات، ويَرِد غالبًا في إعدادات التنسيق).
المعلمة
معلمات الاستعلام، وتُستخدم في الاستعلامات ذات المعلمات مثلSELECT {x:UInt64}. تُرمَّز بالطريقة نفسها التي يُرمَّز بها الإعداد مع تعيين العلامة Custom (0x02)، وتُنهى كذلك بمفتاح فارغ.
قيمة المعلمة هي تمثيل SQL للقيمة، وليست قيمة حرفية خامًا. يجب تمرير المعلمات من النوع String محاطةً مسبقًا بعلامات اقتباس مفردة (فمثلًا، قيمة
{name:String} هي 'Alice' وليست Alice)، وإلا فسيرفضها محلل القيم في الخادم.Data (نوع الحزمة 1 خادم→عميل، نوع الحزمة 2 عميل→خادم)
تُستخدم في كلا الاتجاهين، وتحمل كتل النتائج، وبيانات INSERT، والجداول الخارجية، ووسوم نهاية البيانات. تنسيق wire متماثل في الاتجاهين، إذ يتضمن كلاهما بادئةtable_name قبل الـ Block، ولا يختلفان إلا في بايت نوع الحزمة.
وسم نهاية البيانات هو حزمة كتلتها فارغة، أي تحتوي على
0 أعمدة و0 صفوف، بصرف النظر عن قيمة table_name. ولا يعدّ الخادم حزمة Data المرسلة من العميل حزمةَ إنهاء إلا إذا كانت الكتلة المفككة الترميز فارغة (block.empty())؛ أما الحزمة التي تحمل table_name = "" مع كتلة غير فارغة فهي حزمة صفوف عادية وليست حزمة إنهاء. وعليه، يتألف دفق صفوف INSERT من سلسلة من كتل Data غير الفارغة، تليها كتلة Data فارغة واحدة تُنهيه.
تجد بدائل الكتلة ومعانيها موثّقة في قسم بدائل الكتلة.
Progress (نوع الحزمة 3)
Server → Client. تُرسَل بشكل دوري أثناء تنفيذ الاستعلام. جميع الحقول من النوع VarUInt، وتحمل كل حزمة الزيادات منذ حزمةProgress السابقة، لا الإجماليات التراكمية. فقبل الإرسال، يقرأ الخادم عدّاداته ويعيد ضبطها إلى الصفر بشكل ذري، ثم يحسب elapsed_ns على أنه الفارق الزمني منذ آخر إرسال. لذلك يجب على العميل تجميع الحزم المتتالية محليًا للحصول على الإجماليات الجارية؛ فالتعامل مع الحزمة على أنها قيمة مطلقة يجعل مؤشر التقدم يتراجع إلى الخلف أو يُظهر عددًا أقل من الفعلي بمجرد وصول أكثر من حزمة واحدة.
ProfileInfo (نوع الحزمة 6)
Server → Client. تُرسَل مرة واحدة لكل استعلام، قرب نهاية التنفيذ.Totals (نوع الحزمة 7)
Server → Client. تُرسَل مع الاستعلامات التي تتضمنWITH TOTALS. وتنسيق wire الخاص بها مطابق تمامًا لتنسيق Data: قيمة string باسم table_name (فارغة دائمًا) يليها Block، ولا يختلف عنها إلا في بايت نوع الحزمة.
Extremes (نوع الحزمة 8)
Server → Client. تُرسَل عند تفعيل الإعدادextremes. تنسيق wire مطابق لتنسيق Data. تحتوي الكتلة على صفّين بالضبط: يتضمن الصف 0 القيمة الدنيا لكل عمود، ويتضمن الصف 1 القيمة القصوى.
Log (نوع الحزمة 10)
Server → Client. تُرسَل عندما تكون للاستعلام قائمة انتظار سجلات نشطة (عبر الإعدادsend_logs_level؛ راجع log streaming).
تستخدم الغلاف نفسه وتنسيق المحتوى نفسه المستخدمَين في Data. تحتوي الكتلة على قيمة ثابتة num_columns = 8 وعلى مخطط محدد مسبقًا. يمثّل كل سطر سجل صفًا واحدًا يمتد عبر الأعمدة الثمانية جميعها، وقد تحمل حزمة Log واحدة صفوفًا عديدة.
ProfileEvents (نوع الحزمة 14)
Server → Client. تحمل عدّادات الأداء الخاصة بكل استعلام. تستخدم الغلاف وتنسيق المحتوى نفسيهما المستخدمَين في Data. تحتوي الكتلة على عدد ثابت من الأعمدةnum_columns = 6 وعلى مخطط محدد مسبقًا، ويمثّل كل حدث صفًا واحدًا.
نوع العنصر في العمود
value ليس ثابتًا في جميع الحزم، إذ تُرسل الخوادم الأقدم UInt64، بينما تُرسل الخوادم الأحدث Int64. لذا اقرأ سلسلة نوع العمود من ترويسة الكتلة بدلًا من افتراض عرض محدد.TableColumns (نوع الحزمة 11)
Server → Client، ومشروط بـCOLUMN_DEFAULTS_METADATA (v54410). يرسل الخادم هذه الحزمة قبل كتلة المخطط الخاصة بعملية INSERT لنقل البيانات الوصفية للقيم الافتراضية للأعمدة، ولكن فقط عندما يكون الإصدار المتفق عليه ≥ 54410 و يكون الإعداد input_format_defaults_for_omitted_fields مفعّلاً. في الإصدارات الأقل من 54410 لا تُرسَل الحزمة أبداً، لذا يجب على العميل الأقدم ألا ينتظرها، إذ تصل كتلة المخطط Data مباشرةً. أما العميل ذو الإصدار v54410 فما فوق فيجب أن يكون مستعداً لكلا الاحتمالين: حزمة TableColumns اختيارية تليها كتلة المخطط.
متن مضغوط في الإصدار v54481 فما فوقعند الإصدار المتفق عليه ≥ 54481 (
COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS) يكتب الخادم كلا الحقلين عبر مسار الإخراج نفسه القابل للضغط اختيارياً، لذا عندما يتضمن الاستعلام compression = true يقع متن TableColumns بأكمله (external_table + columns_description) داخل إطار الضغط، ويقرؤه العميل عبر الدفق المقابل بعد فك ضغطه. أما عندما لا يستخدم الاستعلام الضغط، فيُنقل المتن في تنسيق النقل غير مضغوط تماماً كما يوضح الجدول أعلاه. وتبرز أهمية ذلك في استجابات مخطط INSERT: فالعميل الذي يغيّر طريقة معالجة الضغط لحزمتي Log وProfileEvents دون TableColumns سيخطئ في قراءة الاستجابة عند تفعيل ضغط الاستعلام.TimezoneUpdate (نوع الحزمة 17)
Server → Client، ومشروطة بـTIMEZONE_UPDATES (v54464). تُرسَل في موضع واحد فقط: مُهيِّئ دالة الجدول input (أي استعلام بالصيغة INSERT INTO <table> SELECT ... FROM input('<structure>')، الذي تتدفق فيه الصفوف من العميل). فور إرسال الخادم كتلة Data الخاصة بمخطط الإدخال (راجع مرحلة INSERT)، يُرسل الخادم TimezoneUpdate حاملةً القيمة الحالية لـ session_timezone في سياق الاستعلام، كي يحلّل العميل الصفوف التي يوشك على إرسالها وفق المنطقة الزمنية نفسها. ولا يُرسل الخادم هذه الحزمة مطلقاً عند أي تغيير لـ SET session_timezone في أثناء تنفيذ الاستعلام، ولا لإبلاغ العميل بكيفية تنسيق كتل النتائج اللاحقة.
تصل الحزمة مرة واحدة فقط، مباشرةً بعد كتلة مخطط الإدخال وقبل أن يبدأ العميل في إرسال كتل الصفوف. ويجب على أداة فك الترميز التي تتجاهل
TimezoneUpdate أن تقرأ قيمة String التي تليها رغم ذلك، حفاظاً على محاذاة البيانات المتسلسلة.
مصادقة SSH بآلية التحدي والاستجابة (أنواع الحزم 11 و12 و18)
تتوقف هذه الآلية علىSSH_AUTHENTICATION (v54466)، ولا تُفعَّل إلا عند طلبها صراحةً. يدخل الاتصال في مسار SSH عندما ترسل ClientHello القيمة user = " SSH KEY AUTHENTICATION " + <real_user> (مع المسافتين في البداية والنهاية) والقيمة password = "". يقرأ الخادم البادئة، ثم يزيلها لاستعادة اسم المستخدم الحقيقي، وينتقل إلى آلية التحدي والاستجابة.
يحلّ هذا المسار محل المصادقة بكلمة المرور، ويجري تبادل التحدي والاستجابة قبل ServerHello، إذ يؤجّل الخادم ردّ Hello إلى أن تنجح المصادقة:
- يرسل العميل ClientHello مع بادئة وسم SSH وكلمة مرور فارغة.
-
يرسل العميل
SSHChallengeRequest(الحزمة 11). لم يُرسل الخادم ServerHello بعد، فهو يعالج المصادقة أولًا ويبقى متوقفًا هنا بانتظار هذه الحزمة. -
يردّ الخادم بالحزمة
SSHChallengeالتي تحمل بايتات عشوائية (الحزمة 18). -
يبني العميل السلسلة المراد توقيعها ويوقّع عليها لا على التحدي الخام، ثم يرسل
SSHChallengeResponse(الحزمة 12) مرفقةً بالتوقيع. الرسالة الموقَّعة هي ناتج دمج أربعة أجزاء بايتًا ببايت، دون أي فواصل، وبهذا الترتيب تحديدًا: -
يتحقق الخادم من التوقيع باستخدام المفتاح العام المسجّل للمستخدم، بعد أن يعيد بناء السلسلة نفسها
decimal(protocol_version) + default_database + user + challenge. عند النجاح يرسلServerHello، وهو الرد نفسه المتّبع في مسار كلمة المرور، وتمضي المصافحة على نحو طبيعي (Addendum وغيرها)؛ أما عند الفشل فيُعيدExceptionويُنهي الاتصال. وأي عميل يكتفي بتوقيع بايتات التحدي الخام ستفشل مصادقته.
هذا عكس ما يحدث في مصافحة كلمة المرور، حيث تأتي رسالة ServerHello مباشرةً بعد رسالة ClientHello. أما عند استخدام SSH auth، فيُحجَب ServerHello إلى أن يتم التحقق من التوقيع، ولذلك تُدمَج آلية SSH challenge-response ضمن المصافحة قبل ظهور أي ServerHello.
MergeTreeAllRangesAnnouncementResponse (نوع الحزمة 14)
من العميل إلى الخادم، للتواصل بين الخوادم فقط. مشروط بـparallel_replicas_protocol_version ≥ 8 (راجع VERSIONED_PARALLEL_REPLICAS_PROTOCOL). لا يرسل العملاء الخارجيون هذه الحزمة إطلاقًا.
عندما يكون إصدار parallel-replicas المتفاوض عليه ≥ 8، تتغير دورة الطلب/الاستجابة لدى العقدة البادئة (initiator) في التعامل مع حزمة MergeTreeAllRangesAnnouncement الواردة من العقدة التابعة (follower) (نوع الحزمة 15، في اتجاه الخادم←العميل):
- تفتح العقدة التابعة مسار تنفيذ القراءة الخاص بها، ثم ترسل
MergeTreeAllRangesAnnouncementإلى العقدة البادئة. - لا ترد العقدة البادئة بـ
MergeTreeAllRangesAnnouncementResponseإلا إذا كانت قيمةmodeفي الإعلان غيرDefault(أيWithOrder = 1أوReverseOrder = 2، وكلتاهما تُستخدمان في عمليات القراءة المتوازية المرتّبة). أما إذا كانتmode = Default = 0فلا ترد العقدة البادئة، ولا تنتظر العقدة التابعة أي رد، لأن الوضعDefaultيوزّع النطاقات مع كلMergeTreeReadTaskRequestولا يحتاج إلى قائمة أجزاء البيانات مسبقًا. - تنتظر العقدة التابعة الاستجابة (متى كانت متوقعة) قبل إرسال أول
MergeTreeReadTaskRequest(حزمة الخادم16، تُرسل من العقدة التابعة إلى العقدة البادئة، فترد الأخيرة بـMergeTreeReadTaskResponse، حزمة العميل10)، وتستخدم قائمة أجزاء البيانات المُعادة لقصر إنشاء المصادر على أجزاء البيانات التي يملكها الدفق#split_iالخاص بها دون غيرها.
8، يكون الإعلان من نوع fire-and-forget (يُرسَل دون انتظار تأكيد) أيًّا كان الوضع، وتُنشئ العقدة التابعة المصادر على جميع أجزاء البيانات المعروفة محليًا (السلوك القديم).
المتن
متن RangesInDataPartsDescription
متن RangesInDataPartDescription
متن MergeTreePartInfo
متن MarkRanges
مرجع أنواع الحزم
Client → Server
من الخادم إلى العميل
QueryPlan
QueryPlan هي حزمة عميل مخصصة للاتصال بين الخوادم فقط. تأتي بعد حزمة Query تكون قيمة stage فيها QueryPlan (7)، وتحتوي على دفق واحد لخطة مُسلسَلة.
قبل الإصدار
10، لا يتضمن التنسيق الحقلين 2 و3. لذا يجب على العقدة البادئة التي تستخدم قيمة غير افتراضية لأيٍّ من حدّي التنفيذ ألا ترسل هذا التنسيق الأقدم، بل ترسل بدلًا منه استعلام SQL الأصلي، ليعيد المستقبِل الأقدم بناء الحدود من إعدادات الاستعلام.
التهيئة
يتناول هذا القسم معاملات الضبط التي تحدد سلوك اتصالات البروتوكول الأصلي:- إعدادات طبقة النقل — خيارات مقبس TCP ومهلات الانتظار، وهي تؤثر في سلوك اتصال TCP نفسه.
- إعدادات طبقة التطبيق — معاملات ضبط خاصة بكل استعلام تُنقل ضمن قائمة الإعدادات في حزمة Query، وتؤثر فيما يرسله الخادم في تنسيق النقل أو في طريقة تأطيره.
- إعدادات خارج النطاق — إعدادات كثيرًا ما تُخلط بإعدادات البروتوكول، لكنها في الواقع تتحكم في تنفيذ SQL أو في التخزين.
إعدادات طبقة النقل
خيارات المقبس
المهلات
تتداخل المهلات على النحو التالي:
حدود الاتصالات
يمكن للاتصال الذي يرسل الاستعلامات بانتظام أن يبقى مفتوحًا إلى أجل غير مسمى، إذ لا تُغلق سوى الاتصالات الخاملة بعد ساعة، ولا يوجد حد أقصى افتراضي لعمر الاتصال.
إعدادات طبقة التطبيق
تُرسَل هذه الإعدادات مع كل استعلام ضمن قائمة الإعدادات في حزمة Query، وتُغيّر ما يرسله الخادم في تنسيق النقل أو طريقة تأطيره.الضغط
تتحكم علامة
compression في حزمة Query (الحقل 6) في تفعيل الضغط وتعطيله، بينما تحدد هذه الإعدادات المرمِّز المستخدم عند تفعيله.
دفق السجلات
عند ضبط
send_logs_level على أي قيمة غير "none"، يُرسل الخادم حزم Log أثناء تنفيذ الاستعلام.
الإبلاغ عن التقدّم
هذه القيمة حدٌّ أدنى مستهدف، وليست حدًّا أقصى صارمًا؛ إذ قد يرسل الخادم حزم Progress بوتيرة أقل حين لا يُنجز الاستعلام العمل بسرعة كافية.
غلاف النتيجة
INSERT غير المتزامن
التتبع الموزع
إعدادات خارج النطاق
يُخلط أحيانًا بين هذه الإعدادات وإعدادات مستوى البروتوكول، غير أنها تتحكم في تنفيذ SQL أو التخزين أو استخدام CPU، لا في سلوك صيغة النقل. ولا يحتاج تطبيق البروتوكول إلى معالجتها معالجةً خاصة.max_threads— درجة التوازي أثناء تنفيذ الاستعلام.max_memory_usage— الحد الأقصى للذاكرة لكل استعلام.max_block_size،preferred_block_size_bytes— تحديد أحجام الكتل داخليًا أثناء معالجة الاستعلام؛ أما كتل صيغة النقل فمستقلة عنها.compile_expressions— التصريف الفوري (JIT)؛ ويقتصر أثره على CPU.async_insert_max_data_size— المخزن المؤقت لقائمة الانتظار على جانب الخادم.- جميع إعدادات
input_format_*وoutput_format_*باستثناء عائلةinput_format_native_*/output_format_native_*— فالإعدادات غير الخاصة بـnativeتختار صيغًا أخرى أو تضبطها (عبر HTTP مثلًا)، ولا تغيّر كتلDataفي البروتوكول الأصلي.
*_native_* فهي الاستثناء: إذ تغيّر البايتات داخل كتل Data في البروتوكول الأصلي، ولذا يجب على تطبيق البروتوكول أن يأخذها في الحسبان. فالإعداد output_format_native_encode_types_in_binary_format يحوّل حقل type الخاص بالعمود من سلسلة نصية إلى ترميز ثنائي للنوع (باستثناء الاستعلام الثانوي، راجع تخطيط صيغة النقل للعمود)، والإعداد output_format_native_write_json_as_string يُخرج أعمدة JSON بنوع String، والإعداد output_format_native_use_flattened_dynamic_and_json_serialization يختار تخطيط Dynamic/JSON المسطّح (FLATTENED). ولأن هذه الإعدادات تؤثر في متن الكتلة لا في الغلاف الخارجي للحزمة، فهي موصوفة في مواصفة التنسيق الأصلي — راجع تخطيط صيغة النقل للعمود والأنواع ذات الإصدارات.
مسرد المصطلحات
Cancel — حزمة يرسلها العميل (من النوع 3) لإلغاء استعلام قيد التنفيذ. لا تتناول هذه الصفحة مواصفاتها بالتفصيل. وسم نهاية بيانات العميل — حزمة Data فارغة (0 أعمدة، 0 صفوف) يرسلها العميل لإغلاق دفق الإدخال. ويختلف موضعها باختلاف نوع الاستعلام:- الاستعلام العادي (
SELECTوغيره): يُرسَل بعد حزمة Query وأي حزم Data خاصة بالجداول الخارجية، للإشارة إلى “انتهاء البيانات الخارجية”. عندئذٍ يبدأ الخادم بالتنفيذ. INSERT: لا يرسل العميل وسمًا قبل المخطط، إذ يرسل الخادم كتلة المخطط أولًا، ثم يبث العميل كتل Data الخاصة بالصفوف، وبعد ذلك فقط يرسل حزمة Data الفارغة لإنهاء دفق الصفوف. أما إرسال وسم فارغ قبل كتلة المخطط فسيُفسَّر على أنه نهاية فورية للصفوف، مما يؤدي إلى فقدان البيانات.
min(client_version, server_version)، ويُحسب أثناء المصافحة، ويحدد الميزات النشطة طوال عمر الاتصال.
الحزمة — رسالة منقولة تتألف من رمز نوع الحزمة بصيغة VarUInt يليه متن تعتمد صيغته على النوع. راجع غلاف الحزمة.
رمز نوع الحزمة — قيمة VarUInt في بداية الحزمة تحدد صيغتها. والقيم المخصصة حاليًا هي من 0 إلى 18. راجع مرجع أنواع الحزم.
دفق الاستجابة — تسلسل الحزم التي يرسلها الخادم أثناء تنفيذ الاستعلام. طوله غير محدد، وينتهي بحزمة واحدة فقط: إما EndOfStream (عند النجاح) أو Exception (عند الفشل). راجع مرحلة الاستعلام.
كتلة المخطط — كتلة الترويسة (Block تحتوي على أعمدة دون أي صفوف) التي يرسلها الخادم أثناء مرحلة INSERT للإعلان عن بنية الأعمدة المتوقعة قبل أن يرسل العميل البيانات.
قائمة الإعدادات — تسلسل من الصفوف (key, flags, value) في متن Query، ينتهي بمفتاح فارغ، ويحمل تهيئة طبقة التطبيق الخاصة بكل استعلام. راجع إعداد.
المرحلة — حقل VarUInt في حزمة Query (الحقل 5) يحدد إلى أي مدى ينفّذ الخادم الاستعلام. يرسل العملاء الخارجيون عادةً القيمة 2 (Complete)، بينما تستخدم الاستعلامات الموزعة وخطط تنفيذ الاستعلام المسلسلة القيم الأعلى. راجع الحقل 5 في Query للاطلاع على المجموعة الكاملة من القيم المنقولة.
المُنهي — حزمة تُنهي دفقًا. تنتهي استجابة Query بـ EndOfStream (عند النجاح) أو Exception (عند الفشل)، بينما ينتهي دفق إدخال العميل بوسم Data الفارغ.