clickhouse-c هو عميل C قائم على الترويسات فقط لبروتوكول ClickHouse native protocol.
تتوفر الشيفرة المصدرية والمرجع الخاص بكل ترويسة في مستودع GitHub.
وعلى خلاف العملاء عالية المستوى، فهو لا يقدم لك الكثير عن قصد. تقوم الترويسة الأساسية بفك ترميز
وترميز كتل بتنسيق Native عبر دالة استدعاء I/O توفّرها أنت. وأنت من يتولى
إدارة الـ socket، وTLS context، وallocator، وretries، وconnection pooling. وهذا ما يجعله صغيرًا بما يكفي
لدمجه: إذ إن تضمين clickhouse.h وحده لا يضيف أي dependencies وقت الربط تتجاوز libc.
هذه المكتبة قيد التطوير النشط. يفك الإصدار v1 ترميز أنواع ClickHouse الأساسية.
أبلِغ عن القيود أو الوظائف غير المتوفرة عبر issue tracker.
لكن تجدر الإشارة إلى أن هذه المكتبة تفتقر عمدًا إلى بعض الوظائف.
ما الذي لا تفعله المكتبة
- بروتوكول HTTP. غلّف libcurl مباشرةً لاستخدام واجهة HTTP.
- تحليل DNS، والتبديل عند إخفاق نقطة النهاية، وتجميع الاتصالات، وإعادة المحاولة، والتراجع التدريجي.
- دورة حياة سياق TLS. تعتمد البنية الخلفية لـ OpenSSL على كائن
SSLتكون قد أنشأت اتصالًا به مسبقًا. - تعدد الخيوط. كل
chc_clientأحادي الخيط بحكم التصميم. - عمليات I/O غير المتزامنة داخل المكتبة. يستدعي العميل المتزامن
chc_io.readبشكل متزامن. ولعميل يعتمد على حلقة أحداث ولا ينفذ أي I/O بنفسه، استخدم عميل ioless.
كيفية تنظيم المكتبة
clickhouse-c على شكل مجموعة مسطّحة من ملفات الترويسة. يحتوي كل ملف ترويسة على كلٍّ من التصريحات والتنفيذ،
ويكون محميًا بماكرو حارس. اختر ملفات الترويسة التي يحتاجها build الخاص بك.
| الترويسة | الغرض | رايات الربط |
|---|---|---|
clickhouse.h | الأساسيات: الأنواع، والأخطاء، وallocator، وvtable للإدخال/الإخراج، ومحلّل أسماء الأنواع، وقارئ block، وwriter | — |
clickhouse-client.h | حلقة حزم TCP: Hello وQuery وData وEndOfStream وException وProgress وPong | — |
clickhouse-async.h | عميل ioless: حلقة الحزم نفسها، لكن يقودها المستدعي عبر تمرير البايتات، من دون socket | — |
clickhouse-compression.h | تخطيط الإطارات المضغوطة، وCityHash128، وتوجيه codec، وموائمات LZ4/ZSTD | -llz4 -lzstd |
clickhouse-posix-io.h | backend للإدخال/الإخراج فوق read(2)/write(2) بالحجب | — |
clickhouse-openssl.h | backend للإدخال/الإخراج فوق SSL_read/SSL_write | -lssl -lcrypto |
إعداد الخادم المطلوب
إضافته إلى مشروعك
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 أو إعادة اتصال أو pooling — فهذه كلها تقع على عاتق الجهة المستدعية.
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،
وتُجري التفريع بناءً عليه؛ أما النوع المصرَّح به فيأتي من chc_block_column_type. تتداخل
التخطيطات المركّبة، لذا فإن قراءة Nullable(Array(String)) تعني فكّ Nullable، وتتبع offsets
الخاصة بالمصفوفة، ثم تقطيع بيانات السلاسل النصية.
| التخطيط | وسائل الوصول |
|---|---|
CHC_COL_FIXED | chc_column_fixed_data(c, &elem_size) — n_rows * elem_size بايتًا بترتيب little-endian |
CHC_COL_STRING | chc_column_string_data(c), chc_column_string_offsets(c) — تمثل offsets[i] النهاية الحصرية للصف i بترتيب بايتات المضيف؛ ويبدأ الصف 0 من 0 |
CHC_COL_NULLABLE | chc_column_null_map(c) (بايت واحد لكل صف، 1 = NULL)، chc_column_nullable_inner(c) |
CHC_COL_ARRAY | chc_column_array_offsets(c) (نهايات تراكمية)، chc_column_array_values(c)؛ ويُفك ترميز Map على أنه Array(Tuple(K, V)) |
CHC_COL_TUPLE | chc_column_tuple_arity(c), chc_column_tuple_child(c, i) — لكل عنصر فرعي عدد الصفوف نفسه |
CHC_COL_LOW_CARDINALITY | chc_column_lc_key_size(c) (1/2/4/8)، chc_column_lc_keys(c), chc_column_lc_dict(c)؛ والخانة 0 في القاموس هي القيمة الافتراضية |
Nullable:
CHC_COL_FIXED تكون little-endian على مستوى wire؛ وعلى المضيفات ذات ترتيب big-endian يجب عليك تبديل بايتات الأعداد
الصحيحة متعددة البايتات بنفسك. تكون offsets ومفاتيح LowCardinality قد بُدّلت بالفعل إلى ترتيب المضيف وقت فك الترميز.
تتكوّن UUIDs من نصفين UInt64 بنظام little-endian، ويكون IPv4 عدداً صحيحاً من 4 بايتات بنظام little-endian، بينما يكون IPv6
بترتيب بايتات الشبكة. وتكون ticks الخاصة بـ DateTime64 بتوقيت UTC — أما timezone في النوع فهي metadata فقط.
عند استيعاب البيانات من طرف غير موثوق، استدعِ chc_column_validate على كل عمود قبل اجتيازه.
ولا يتحقق chc_block_read من الثوابت العابرة للحقول مثل offsets الخاصة بالمصفوفات ومفاتيح
LowCardinality، لذا قد تؤدي block مزوّرة بخلاف ذلك إلى القراءة خارج حدود العمود الداخلي.
إدراج البيانات
chc_block_builder، ثم مرّرها إلى chc_client_send_data. يسجّل المُنشئ
المؤشرات بدلًا من نسخ البيانات، لذا يجب أن تظل شرائح الأعمدة صالحة طوال عملية الإرسال. ترسل عملية INSERT الاستعلام،
وتنتظر كتلة الترويسة من الخادم، ثم ترسل كتلة بيانات واحدة أو أكثر، ثم ترسل كتلة فارغة
لإنهاء الدفق.
chc_block_builder_append_fixed يأخذ n_rows * elem_size بايتًا بترتيب little-endian؛
ويأخذ chc_block_builder_append_string إزاحات نهاياتٍ تراكميةً حصرية بترتيب بايتات المضيف عبر
لوح packed. إن تمرير الـ builder عبر chc_client_send_data بدلًا من
chc_block_write منخفض المستوى يتيح للعميل ضبط خيارات الكتلة استنادًا إلى revision المتفاوض عليه وتطبيق
الضغط.
الضغط
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، يجب أن تكون المصافحة قد اكتملت.
check_cancel، يُجرى استطلاعها بين عمليات القراءة، بالإضافة إلى
مهلة قراءة عبر chc_openssl_io_set_deadline / chc_posix_io_set_deadline.
عميل Ioless (غير المتزامن)
clickhouse-async.h هو إصدار ioless من عميل TCP مخصّص لحلقات الأحداث. فهو لا يتعامل مع أي
مقبس مطلقًا: إذ تمرّر البايتات التي استلمتها وتستخرج البايتات التي يريد إرسالها، بينما تتولى أنت إدارة epoll،
أو io_uring، أو WaitLatchOrSocket بنفسك. وتظل الخيارات، وأنواع الحزم، وblock builder
كما هي نفسها في العميل المتزامن.
chc_async_client_init لا يُجري أي عمليات I/O ولا يمكن أن يحجب التنفيذ. ثم تعمل المصافحة
كآلة حالات قابلة للاستئناف، وينطبق الأمر نفسه على كل عملية إرسال واستقبال. وعندما تحتاج عملية parse إلى ما يتجاوز البايتات التي مرّرتها، تُعيد
الدالة CHC_WOULD_BLOCK بدلًا من الحجب — مرّر المزيد من البايتات الواردة ثم استدعِها مرة أخرى، وعندها يستأنف
المحلّل العمل من منتصف 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)، ويُفك ترميزه على أنهTالداخليJSONوObject('json')، على شكل أعمدةStringباستخدام تسلسل String (انظر أدناه)
JSON و Object('json') إلا عندما يضبط الاستعلام output_format_native_write_json_as_string=1.
يصل كل صفّ كمستند JSON واحد في عمود CHC_COL_STRING، لذا تقرؤه accessors الخاصة بالسلاسل النصية؛
ويكتب builder البنية نفسها باستخدام chc_block_builder_append_json_string. وأي إصدار آخر من
تسلسل JSON يعيد CHC_ERR_TYPE مع ذكر اسم الإعداد.
لا يزال Variant و Dynamic و AggregateFunction غير مدعومة في فك الترميز، وتعيد CHC_ERR_TYPE؛
لذا حوّلها إلى String على جهة الخادم كحل احتياطي.