clickhouse-c هو عميل C يعتمد فقط على ملفات الترويسة لبروتوكول ClickHouse الأصلي.
يوجد المصدر والمرجع الخاص بكل ترويسة في مستودع GitHub.
وعلى خلاف العملاء الأعلى مستوى، فهو يتعمد ألا ينجز الكثير نيابةً عنك. تتولى الترويسة الأساسية فك ترميز كتل تنسيق Native وترميزها
عبر callback للإدخال/الإخراج توفّره أنت. وأنت من يتولى إدارة
socket وTLS context ومخصص وretries وتجميع الاتصالات. وهذا ما يجعله صغيرًا بما يكفي
للتضمين: فمجرد تضمين clickhouse.h لا يضيف أي dependencies وقت الربط سوى libc.
هذه المكتبة قيد التطوير النشط. يفك الإصدار v1 ترميز أنواع ClickHouse الأساسية.
أبلغ عن القيود أو الوظائف غير المتوفرة عبر متعقب المشكلات.
لكن تجدر الإشارة إلى أن افتقار هذه المكتبة إلى بعض الوظائف هو أمر مقصود في التصميم.
ما لا تفعله المكتبة
- بروتوكول HTTP. استخدم libcurl مباشرةً لتغليف واجهة HTTP.
- حل أسماء DNS، والتبديل التلقائي عند تعطل نقطة النهاية، وتجميع الاتصالات، وإعادة المحاولة، والتراجع التدريجي.
- دورة حياة سياق TLS. تستخدم الواجهة الخلفية لـ OpenSSL كائن
SSLتكون قد أنشأت اتصالًا به بالفعل. - تعدد الخيوط. كل
chc_clientأحادي الخيط بحكم التصميم. - عمليات I/O غير المتزامنة داخل المكتبة. يستدعي العميل الحاجب
chc_io.readبشكل متزامن. وبالنسبة إلى عميل حلقة أحداث لا ينفّذ أي عمليات I/O بنفسه، فاستخدم عميل ioless.
كيف تُنَّظم المكتبة
clickhouse-c كمجموعة مسطحة من ملفات الترويسة. ويضم كل ملف ترويسة التصريحات والتنفيذ معًا،
ويحميه ماكرو حارس. اختر ملفات الترويسة التي تحتاجها عملية البناء لديك.
إعداد الخادم المطلوب
إضافته إلى مشروعك
CHC_IMPLEMENTATION وتضمين التنفيذ؛
أما كل الوحدات الأخرى فتضمّن ملفات الترويسة نفسها للتصريحات فقط.
CHC_PROVIDE_STDLIB_ALLOC قبل تضمين clickhouse.h لاستخدام chc_alloc_stdlib.
عرّف CHC_NO_LZ4 أو CHC_NO_ZSTD مع clickhouse-compression.h لإزالة الاعتماديات على lz4/zstd.
الاتصال عبر TCP
chc_io وتمريره إلى
chc_client_init، الذي يُجري مصافحة Hello بشكل متزامن. ولا توفّر المكتبة أي حلّ لأسماء DNS،
أو failover، أو إعادة اتصال، أو تجميع الاتصالات — فهذه كلها تقع على عاتق الجهة المستدعية.
chc_client أحادي الخيط ويغلف اتصالًا واحدًا. تستدعي المكتبة دوال رد النداء chc_io
بشكل متزامن؛ أما ما تنفذه هذه الدوال في الخلفية (epoll, io_uring,
WaitLatchOrSocket) فيعود إليك.
تشغيل استعلام
CHC_PKT_END_OF_STREAM. استخدم chc_client_send_query_ex لإرفاق إعداد الخادم المطلوب؛ أما chc_client_send_query وحده فيرسل
قائمة إعدادات فارغة ويرث الإعدادات الافتراضية التي يعتمدها الخادم.
CHC_PKT_EXCEPTION، وليس على شكل قيمة إرجاع غير OK من
chc_client_recv_packet. ولا تُرجِع قيمة غير OK إلا حالات الإخفاق على مستوى النقل. تكون أول حزمة CHC_PKT_DATA
في النتيجة كتلة ترويسة تصف المخطط من دون أي صفوف؛ وتليها كتل البيانات.
تقوم chc_packet_clear بتحرير كتلة الحزمة أو الاستثناء — عيّن هذين الحقلين في الحزمة إلى null أولًا
لتتولى الملكية بدلًا من ذلك.
قراءة بيانات الأعمدة
chc_column_layout،
وتُجري dispatch بناءً عليه؛ أما نوعه المصرَّح به فيأتي من chc_block_column_type. ويمكن أن
تكون التخطيطات المركبة متداخلة، لذا فإن قراءة Nullable(Array(String)) تعني فكّ Nullable،
واجتياز إزاحات المصفوفة، ثم تقطيع بيانات السلسلة النصية.
قارئ للأعمدة الرقمية العادية، والسلاسل النصية، والأعمدة
Nullable:
CHC_COL_FIXED بتنسيق little-endian في تنسيق النقل؛ وعلى المضيفات ذات ترتيب big-endian، عليك تبديل بايتات الأعداد الصحيحة متعددة البايتات بنفسك. وتكون الإزاحات ومفاتيح LowCardinality قد حُوِّلت بالفعل إلى ترتيب المضيف وقت فك الترميز.
وتتكون معرّفات UUID من نصفين UInt64 بتنسيق little-endian، وIPv4 هو عدد صحيح little-endian من 4 بايتات، أما IPv6 فهو
بترتيب بايتات الشبكة. وتكون قيم tick في DateTime64 وفق UTC — والمنطقة الزمنية في النوع ليست سوى بيانات وصفية.
عند إدخال البيانات من نظير غير موثوق، استدعِ chc_column_validate على كل عمود قبل المرور
عليه. لا يتحقق chc_block_read من الشروط الثابتة بين الحقول، مثل إزاحات المصفوفات
ومفاتيح LowCardinality، لذا قد تتسبب كتلة مزورة، بخلاف ذلك، في القراءة خارج حدود الأعمدة الداخلية.
إدراج البيانات
chc_build_*، ثم ألحِقها بـ chc_block_builder، وبعد ذلك مرّره إلى
chc_client_send_data. يستخدم المنشئ مساحة تخزين يوفّرها المستدعي، ويسجّل المؤشرات بدلًا من
نسخها، لذلك يجب أن تظل مساحة التخزين وأشجار الأعمدة والأنواع والأسماء وslabs قائمة طوال مدة الإرسال. ترسل عملية INSERT
الاستعلام، وتنتظر كتلة الترويسة من الخادم، ثم ترسل كتلة بيانات واحدة أو أكثر، ثم ترسل كتلة
فارغة لإنهاء الدفق.
chc_build_fixed عددًا من البايتات مقداره n_rows * elem_size بترتيب little-endian؛ بينما يأخذ chc_build_string
إزاحات نهايات تراكمية exclusive بترتيب بايتات المضيف فوق slab packed. تُرجع الدوال المساعدة عُقد الأعمدة
بالقيمة. عشّقها بما يطابق النوع: على سبيل المثال، مرِّر عقدة ثابتة أو نصية إلى
chc_build_nullable، ثم مرِّر النتيجة إلى chc_build_array، وألحِق جذر المصفوفة. تستخدم أعمدة Tuple،
وLowCardinality، وMap، وGeo البنية الشجرية نفسها: فـ Map هو Array(Tuple(K, V)).
يجب أن يكون لجميع الأعمدة في block واحد عدد الصفوف نفسه على المستوى الأعلى. يتحقق الكاتب من الشجرة مقارنةً
بنوع ClickHouse المُحلَّل، لكن يجب على المستدعي تحديد حجم مساحة التخزين chc_block_col لكل عملية إلحاق.
يمكنك أيضًا إلحاق عمود مفكوك الترميز من chc_block_column مباشرةً لإعادة ترميزه، أو استدعاء
chc_block_write_cols باستخدام مصفوفة chc_block_col لتجاوز أداة الإنشاء. إن تمرير أداة الإنشاء عبر
chc_client_send_data بدلًا من chc_block_write منخفض المستوى يتيح للعميل ضبط خيارات block
استنادًا إلى revision المتفَق عليه وتطبيق الضغط.
الضغط
codec مُهيّأً في chc_client_opts. يفكّ العميل ضغط حزم Data الواردة
ويضغط الحزم الصادرة. توفّر ترويسة الضغط مواءمي LZ4 وZSTD؛
ولا يملأ كل استدعاء تهيئة إلا خاناته الخاصة، لذا استدعِ كليهما لدعم أيٍّ منهما.
chc_codec بنفسك؛
ويُصرَّح عن الـ vtable في clickhouse-compression.h.
TLS
clickhouse-openssl.h واجهة chc_io خلفية فوق SSL_read/SSL_write. أنت من يتولى إدارة OpenSSL:
فالمكتبة لا تُنشئ مطلقًا SSL_CTX، ولا تتحقق من الشهادات، ولا تضبط SNI، ولا تستدعي SSL_connect /
SSL_shutdown. وعند استدعاء chc_io.read، يجب أن تكون المصافحة قد اكتملت.
callback اختيارية باسم check_cancel، ويجري استطلاعها بين عمليات القراءة، كما تقبلان
مهلة للقراءة عبر chc_openssl_io_set_deadline / chc_posix_io_set_deadline.
عميل Ioless (غير متزامن)
clickhouse-async.h هو إصدار ioless من عميل TCP المخصّص لحلقات الأحداث. وهو لا يتعامل مطلقًا مع
socket: فأنت تمرّر إليه البايتات التي استلمتها، وتستخرج البايتات التي يريد إرسالها، بينما تتولى بنفسك تشغيل epoll،
وio_uring، أو WaitLatchOrSocket. وتظل الخيارات، وأنواع الحزم، ومُنشئ الكتل، هي نفسها كما في
العميل الحاجب.
لا يُجري chc_async_client_init أي عمليات I/O ولا يمكن أن يحجب التنفيذ. ثم تُنفَّذ المصافحة بعد ذلك
بوصفها آلة حالات قابلة للاستئناف، وكذلك الحال مع كل عملية إرسال واستقبال. وعندما يتجاوز التحليل البايتات التي
مرّرتها، يُرجع الاستدعاء CHC_WOULD_BLOCK بدلًا من الحجب — مرّر مزيدًا من البايتات الواردة واستدعِه مرة
أخرى، فيستأنف المُحلّل من منتصف الكتلة.
pump لديك بنقل البايتات في كلا الاتجاهين. في الاتجاه الصادر، يعيد chc_async_pending_out مؤشّرًا وطولًا
إلى البايتات الموجودة في قائمة الانتظار؛ وبعد أن يقبل المقبس جزءًا منها، استدعِ chc_async_consume_out بهذا العدد،
ولا بأس بكتابة جزئية. في الاتجاه الوارد، مرّر البيانات المقروءة من المقبس إلى chc_async_submit. لا تُحظر عمليات الإرسال
مطلقًا ولا تطبّق ضغطًا عكسيًا، لذا راقب طول البيانات الصادرة المعلّقة وتوقّف عن إصدار عمليات إرسال جديدة عندما
يكبر هذا الطول أكثر من اللازم.
يوجد برنامج تشغيل liburing فعّال في
test/test_async_uring.c.
الذاكرة والمخصّص
vtable لـ chc_alloc، لذا يعتمد التخصيص على الآلية التي يستخدمها المضيف.
CHC_PROVIDE_STDLIB_ALLOC قبل تضمين clickhouse.h، واستدعِ chc_alloc_stdlib() للحصول على
مُخصِّص قياسي يستند إلى malloc.
الأخطاء واستثناءات الخادم
CHC_OK (0) أو رمز CHC_ERR_* غير صفري. تكون قيمة الإرجاع هي الرمز؛ بينما يحمل
chc_err المخصّص على مكدس المستدعي الرسالة المقروءة بشريًا. ولا تُخصِّص المكتبة أي خطأ على كومة الذاكرة
مطلقًا.
chc_err. فهي تصل عبر تدفّق الحزم على شكل
CHC_PKT_EXCEPTION، وتحمل code وdisplay_text وstack_trace الخاصة بالخادم. اقتصر
التحقق من chc_err على إخفاقات النقل والبروتوكول وفك الترميز.
أنواع البيانات المدعومة
Int8–Int256,UInt8–UInt256Float32,Float64,BFloat16BoolDecimal32,Decimal64,Decimal128,Decimal256Date,Date32,DateTime,DateTime64,Time,Time64String,FixedString(N)UUID,IPv4,IPv6Enum8,Enum16Nullable(T),Array(T),Tuple(...),Map(K, V),Nested(...)LowCardinality(T)IntervalQBit(...)Point,Ring,Polygon,MultiPolygonSimpleAggregateFunction(f, T)، ويُفك ترميزها على أنها القيمة الداخليةTJSONوObject('json')، كأعمدةStringباستخدام تسلسل String (انظر أدناه)
JSON و Object('json') باستخدام تسلسل String؛ عيّن
output_format_native_write_json_as_string=1 في الاستعلام. يصل كل صف مدعوم
كمستند JSON واحد في عمود CHC_COL_STRING. أنشئ البنية نفسها باستخدام chc_build_string;
ويُخرج الكاتب البادئة المطلوبة وفقًا للنوع المُحلَّل.
لا يُفك ترميز Variant و Dynamic و AggregateFunction بعد، وتُرجِع CHC_ERR_TYPE;
حوّلها إلى String على جانب الخادم كحل احتياطي.