Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

عميل ClickHouse بلغة C

clickhouse-c هو عميل C يعتمد فقط على ملفات الترويسة لبروتوكول ClickHouse الأصلي. يوجد المصدر والمرجع الخاص بكل ترويسة في مستودع GitHub.

وعلى خلاف العملاء الأعلى مستوى، فهو يتعمد ألا ينجز الكثير نيابةً عنك. تتولى الترويسة الأساسية فك ترميز كتل تنسيق Native وترميزها عبر callback للإدخال/الإخراج توفّره أنت. وأنت من يتولى إدارة مقبس وسياق TLS ومخصص وretries وتجميع الاتصالات. وهذا ما يجعله صغيرًا بما يكفي للتضمين: فمجرد تضمين clickhouse.h لا يضيف أي اعتمادية وقت الربط سوى libc.

ما لا تفعله المكتبة

هذه ليست من أهدافها عن قصد. تعامل معها في تطبيقك أو باستخدام مكتبة شقيقة:

  • بروتوكول HTTP. استخدم libcurl مباشرةً لتغليف واجهة HTTP.
  • حل أسماء DNS، والتبديل التلقائي عند تعطل نقطة النهاية، وتجميع الاتصالات، وإعادة المحاولة، والتراجع التدريجي.
  • دورة حياة سياق TLS. تستخدم الواجهة الخلفية لـ OpenSSL كائن SSL تكون قد أنشأت اتصالًا به بالفعل.
  • تعدد الخيوط. كل chc_client أحادي الخيط بحكم التصميم.
  • عمليات I/O غير المتزامنة داخل المكتبة. يستدعي العميل الحاجب chc_io.read بشكل متزامن. وبالنسبة إلى عميل حلقة أحداث لا ينفّذ أي عمليات I/O بنفسه، فاستخدم عميل ioless.

كيف تُنَّظم المكتبة

يأتي clickhouse-c كمجموعة مسطحة من ملفات الترويسة. ويضم كل ملف ترويسة التصريحات والتنفيذ معًا، ويحميه ماكرو حارس. اختر ملفات الترويسة التي تحتاجها عملية البناء لديك.

الترويسة الغرض خيارات الربط
clickhouse.h الأساسيات: الأنواع، والأخطاء، والمخصِّص، وvtable للإدخال/الإخراج، ومحلّل أسماء الأنواع، وقارئ الكتل، والكاتب
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 واجهة خلفية للإدخال/الإخراج فوق read(2)/write(2) المحجوبتين
clickhouse-openssl.h واجهة خلفية للإدخال/الإخراج فوق SSL_read/SSL_write -lssl -lcrypto

إعداد الخادم المطلوب

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

output_format_native_encode_types_in_binary_format = 0

إضافته إلى مشروعك

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

/* clickhouse_impl.c */
#define CHC_IMPLEMENTATION
#include "clickhouse.h"
#include "clickhouse-posix-io.h"
#include "clickhouse-client.h"
#include "clickhouse-compression.h"
/* every other TU */
#include "clickhouse.h"
#include "clickhouse-client.h"

عرّف CHC_PROVIDE_STDLIB_ALLOC قبل تضمين clickhouse.h لاستخدام chc_alloc_stdlib. عرّف CHC_NO_LZ4 أو CHC_NO_ZSTD مع clickhouse-compression.h لإزالة الاعتماديات على lz4/zstd.

الاتصال عبر TCP

للتخاطب مع خادم ClickHouse، عليك إعداد المقبس بنفسك، ثم تغليفه داخل chc_io وتمريره إلى chc_client_init، الذي يُجري مصافحة Hello بشكل متزامن. ولا توفّر المكتبة أي حلّ لأسماء DNS، أو failover، أو إعادة اتصال، أو تجميع الاتصالات — فهذه كلها تقع على عاتق الجهة المستدعية.

int fd = socket(AF_INET, SOCK_STREAM, 0);
int one = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);

struct sockaddr_in sa = {};
sa.sin_family      = AF_INET;
sa.sin_port        = htons(9000);
sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
connect(fd, (struct sockaddr *) &sa, sizeof sa);

chc_alloc al = chc_alloc_stdlib();
chc_posix_io state;
chc_io io;
chc_posix_io_init(&state, &io, fd, NULL, NULL);

chc_client *client = NULL;
chc_client_opts opts = {
    .user     = "default",
    .password = "",
    .database = "default",
};
chc_err err = {};
if (chc_client_init(&client, &opts, &al, &io, &err) != CHC_OK) {
    fprintf(stderr, "connect: %s\n", err.msg);
    chc_client_close(client);   /* safe to call on the NULL-on-failure handle */
    return 1;
}

const chc_server_info *info = chc_client_server_info(client);
printf("connected to %s %llu.%llu.%llu\n", info->display_name,
       (unsigned long long) info->version_major,
       (unsigned long long) info->version_minor,
       (unsigned long long) info->version_patch);

كل chc_client أحادي الخيط ويغلف اتصالًا واحدًا. تستدعي المكتبة دوال رد النداء chc_io بشكل متزامن؛ أما ما تنفذه هذه الدوال في الخلفية (epoll, io_uring, WaitLatchOrSocket) فيعود إليك.

تشغيل استعلام

أرسل الاستعلام، ثم واصل استقبال الحزم حتى CHC_PKT_END_OF_STREAM. استخدم chc_client_send_query_ex لإرفاق إعداد الخادم المطلوب؛ أما chc_client_send_query وحده فيرسل قائمة إعدادات فارغة ويرث الإعدادات الافتراضية التي يعتمدها الخادم.

chc_query_setting settings[] = {
    { .name = "output_format_native_encode_types_in_binary_format", .value = "0" },
};
chc_query_opts qopts = { .settings = settings, .n_settings = 1 };

const char *sql = "SELECT number, toString(number * number) FROM numbers(5)";
if (chc_client_send_query_ex(client, sql, strlen(sql), &qopts, &err) != CHC_OK) {
    fprintf(stderr, "query: %s\n", err.msg);
    return 1;
}

for (;;) {
    chc_packet pkt = {};
    if (chc_client_recv_packet(client, &pkt, &err) != CHC_OK) {
        fprintf(stderr, "recv: %s\n", err.msg);
        break;
    }

    if (pkt.kind == CHC_PKT_DATA) {
        for (size_t r = 0; r < chc_block_n_rows(pkt.block); r++)
            for (size_t c = 0; c < chc_block_n_columns(pkt.block); c++)
                print_value(chc_block_column_type(pkt.block, c),
                            chc_block_column(pkt.block, c), r);
    } else if (pkt.kind == CHC_PKT_EXCEPTION) {
        fprintf(stderr, "server: %s\n", pkt.exception->display_text);
    }

    bool done = pkt.kind == CHC_PKT_END_OF_STREAM;
    chc_packet_clear(client, &pkt);
    if (done) break;
}

تصل استثناءات الخادم على شكل حزم 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، واجتياز إزاحات المصفوفة، ثم تقطيع بيانات السلسلة النصية.

Layout Accessors
CHC_COL_FIXED chc_column_fixed_data(c, &elem_size) — بايتات little-endian بعدد n_rows * elem_size
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 = NULLchc_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:

void print_value(const chc_type *t, const chc_column *c, size_t row)
{
    if (chc_column_layout(c) == CHC_COL_NULLABLE) {
        if (chc_column_null_map(c)[row]) { fputs("\\N", stdout); return; }
        print_value(chc_type_child(t, 0), chc_column_nullable_inner(c), row);
        return;
    }

    switch (chc_column_layout(c)) {
    case CHC_COL_FIXED: {
        /* fixed_data is a raw little-endian byte slab. memcpy into a typed
           local to avoid unaligned loads and strict-aliasing UB, then
           byte-swap on big-endian hosts. */
        size_t es;
        const uint8_t *p = chc_column_fixed_data(c, &es) + row * es;
        switch (chc_type_kind(t)) {
        case CHC_UINT64: { uint64_t v; memcpy(&v, p, sizeof v); printf("%" PRIu64, v); break; }
        case CHC_INT32:  { int32_t  v; memcpy(&v, p, sizeof v); printf("%" PRId32, v); break; }
        case CHC_FLOAT64: { double  v; memcpy(&v, p, sizeof v); printf("%g", v); break; }
        /* ... remaining numeric kinds ... */
        default: break;
        }
        break;
    }
    case CHC_COL_STRING: {
        const uint8_t  *bytes   = chc_column_string_data(c);
        const uint64_t *offsets = chc_column_string_offsets(c);
        uint64_t start = row == 0 ? 0 : offsets[row - 1];
        fwrite(bytes + start, 1, (size_t) (offsets[row] - start), stdout);
        break;
    }
    default: break;
    }
}

تكون بيانات 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 الاستعلام، وتنتظر كتلة الترويسة من الخادم، ثم ترسل كتلة بيانات واحدة أو أكثر، ثم ترسل كتلة فارغة لإنهاء الدفق.

const char *sql = "INSERT INTO greetings (id, message) VALUES";
chc_client_send_query(client, sql, strlen(sql), "", 0, &err);

/* Wait for the server's header block (schema, 0 rows). */
bool got_header = false;
while (!got_header) {
    chc_packet pkt = {};
    if (chc_client_recv_packet(client, &pkt, &err) != CHC_OK) {
        fprintf(stderr, "recv: %s\n", err.msg);
        return 1;
    }
    chc_packet_kind kind = pkt.kind;
    if (kind == CHC_PKT_DATA) got_header = true;
    else if (kind == CHC_PKT_EXCEPTION && pkt.exception)
        fprintf(stderr, "server: %s\n", pkt.exception->display_text);
    chc_packet_clear(client, &pkt);
    if (kind == CHC_PKT_EXCEPTION || kind == CHC_PKT_END_OF_STREAM) return 1;  /* no header coming */
}

chc_block_col columns[2];
chc_block_builder bb;
chc_block_builder_init(&bb, columns);

uint64_t ids[3] = { 1, 2, 3 };
chc_type *u64 = NULL;
chc_type_parse("UInt64", 6, &al, &u64, &err);
chc_column id = chc_build_fixed(ids, sizeof ids[0], 3);
chc_block_builder_append(&bb, "id", 2, u64, &id);

/* String columns: cumulative exclusive end offsets + a packed byte slab. */
uint64_t offsets[3] = { 5, 11, 20 };   /* "hello", "buenas", "goedendag" */
const uint8_t bytes[] = "hellobuenasgoedendag";
chc_type *string = NULL;
chc_type_parse("String", 6, &al, &string, &err);
chc_column message = chc_build_string(offsets, bytes, 3);
chc_block_builder_append(&bb, "message", 7, string, &message);

chc_client_send_data(client, &bb, &err);  /* the populated block */
chc_client_send_data(client, NULL, &err); /* empty block ends the INSERT */

/* Drain to EndOfStream. */
for (;;) {
    chc_packet pkt = {};
    chc_client_recv_packet(client, &pkt, &err);
    bool done = pkt.kind == CHC_PKT_END_OF_STREAM;
    chc_packet_clear(client, &pkt);
    if (done) break;
}

chc_type_destroy(u64, &al);
chc_type_destroy(string, &al);

يأخذ 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؛ ولا يملأ كل استدعاء تهيئة إلا خاناته الخاصة، لذا استدعِ كليهما لدعم أيٍّ منهما.

#include "clickhouse-compression.h"

chc_codec codec = {};
chc_lz4_codec_init(&codec);
chc_zstd_codec_init(&codec);

chc_client_opts opts = {
    .user        = "default",
    .compression = CHC_COMP_LZ4,   /* or CHC_COMP_ZSTD */
    .codec       = &codec,
};

لاستخدام مكتبة ضغط لا يوفّر المشروع لها ربطًا، املأ 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، يجب أن تكون المصافحة قد اكتملت.

#include "clickhouse-openssl.h"

SSL *ssl = /* connected, handshake complete */;
chc_openssl_io state;
chc_io io;
chc_openssl_io_init(&state, &io, ssl, NULL, NULL);
/* hand &io to chc_client_init, same as the POSIX backend */

تستخدم ClickHouse Cloud وعمليات النشر الأخرى المفعّل فيها TLS البروتوكول الأصلي على المنفذ 9440. وتقبل كلتا الجهتين الخلفيتين دالة 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 بدلًا من الحجب — مرّر مزيدًا من البايتات الواردة واستدعِه مرة أخرى، فيستأنف المُحلّل من منتصف الكتلة.

#include "clickhouse-async.h"

chc_async_client *c = NULL;
chc_client_opts opts = { .user = "default" };
chc_async_client_init(&c, &opts, &al, &err);

for (;;) {
    int rc = chc_async_handshake(c, &err);
    if (rc == CHC_OK) break;
    if (rc != CHC_WOULD_BLOCK) break;   /* hard error */
    pump(c);   /* drain pending_out to the socket; feed received bytes to chc_async_submit */
}

chc_async_send_query(c, sql, strlen(sql), "", 0, &err);

for (;;) {
    chc_packet pkt = {};
    int rc = chc_async_recv_packet(c, &pkt, &err);
    if (rc == CHC_WOULD_BLOCK) { pump(c); continue; }
    if (rc != CHC_OK) break;

    bool done = pkt.kind == CHC_PKT_END_OF_STREAM;
    if (pkt.kind == CHC_PKT_DATA && pkt.block) { /* read columns as above */ }
    chc_async_packet_clear(c, &pkt);
    if (done) break;
}

يقوم pump لديك بنقل البايتات في كلا الاتجاهين. في الاتجاه الصادر، يعيد chc_async_pending_out مؤشّرًا وطولًا إلى البايتات الموجودة في قائمة الانتظار؛ وبعد أن يقبل المقبس جزءًا منها، استدعِ chc_async_consume_out بهذا العدد، ولا بأس بكتابة جزئية. في الاتجاه الوارد، مرّر البيانات المقروءة من المقبس إلى chc_async_submit. لا تُحظر عمليات الإرسال مطلقًا ولا تطبّق ضغطًا عكسيًا، لذا راقب طول البيانات الصادرة المعلّقة وتوقّف عن إصدار عمليات إرسال جديدة عندما يكبر هذا الطول أكثر من اللازم.

يوجد برنامج تشغيل liburing فعّال في test/test_async_uring.c.

الذاكرة والمخصّص

تستقبل كل نقطة دخول vtable لـ chc_alloc، لذا يعتمد التخصيص على الآلية التي يستخدمها المضيف.

typedef struct chc_alloc {
    void *ud;
    void *(*alloc)  (void *ud, size_t bytes);
    void *(*realloc)(void *ud, void *p, size_t old_bytes, size_t new_bytes);
    void  (*free)   (void *ud, void *p, size_t bytes);
} chc_alloc;

عرِّف CHC_PROVIDE_STDLIB_ALLOC قبل تضمين clickhouse.h، واستدعِ chc_alloc_stdlib() للحصول على مُخصِّص قياسي يستند إلى malloc.

الأخطاء واستثناءات الخادم

تعيد الدالة CHC_OK (0) أو رمز CHC_ERR_* غير صفري. تكون قيمة الإرجاع هي الرمز؛ بينما يحمل chc_err المخصّص على مكدس المستدعي الرسالة المقروءة بشريًا. ولا تُخصِّص المكتبة أي خطأ على كومة الذاكرة مطلقًا.

typedef struct chc_err {
    int  server_code;           /* set when the return code is CHC_ERR_SERVER */
    char msg[CHC_ERR_MSG_LEN];  /* NUL-terminated, default 256 bytes */
    char server_name[64];       /* ClickHouse exception class, if SERVER */
} chc_err;

أخطاء الاستعلامات من جانب الخادم ليست من نوع فشل chc_err. فهي تصل عبر تدفّق الحزم على شكل CHC_PKT_EXCEPTION، وتحمل code وdisplay_text وstack_trace الخاصة بالخادم. اقتصر التحقق من chc_err على إخفاقات النقل والبروتوكول وفك الترميز.

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

يفكّ قارئ الكتل ترميز ما يلي:

  • Int8Int256, UInt8UInt256
  • Float32, Float64, BFloat16
  • Bool
  • Decimal32, Decimal64, Decimal128, Decimal256
  • Date, Date32, DateTime, DateTime64, Time, Time64
  • String, FixedString(N)
  • UUID, IPv4, IPv6
  • Enum8, Enum16
  • Nullable(T), Array(T), Tuple(...), Map(K, V), Nested(...)
  • LowCardinality(T)
  • Interval
  • QBit(...)
  • Point, Ring, Polygon, MultiPolygon
  • SimpleAggregateFunction(f, T)، ويُفك ترميزها على أنها القيمة الداخلية T
  • JSON و 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 على جانب الخادم كحل احتياطي.

Navigation