> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-mintlify-fbfa8bee.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> 面向 ClickHouse 原生协议的仅头文件 C 客户端，专为嵌入而设计。

# ClickHouse C 客户端

`clickhouse-c` 是一个面向 ClickHouse [原生协议](/zh/concepts/features/interfaces/tcp)的仅头文件 C 客户端。
其源代码以及各头文件的参考文档位于 [GitHub 仓库](https://github.com/ClickHouse/clickhouse-c)。

与更高层的客户端不同，它是有意只提供尽可能少的封装。核心头文件会通过你提供的 I/O 回调，对 [Native](/zh/reference/formats/Native) 格式的块进行解码和编码。套接字、TLS 上下文、内存分配器、重试机制以及连接池都由你自行管理。因此它足够小，便于嵌入：仅包含 `clickhouse.h` 即可，除 libc 外不会引入任何链接时依赖项。

<Note>
  该库仍在积极开发中。v1 可解码 ClickHouse 的核心类型。
  如有局限或缺失的功能，请通过 [issue tracker](https://github.com/ClickHouse/clickhouse-c/issues) 反馈。
  但也请理解，该库有意在设计上省略了部分功能。
</Note>

<div id="non-goals">
  ## 该库不做什么
</div>

以下是刻意不纳入该库范围的内容。请在你的应用程序中处理，或借助配套库实现：

* HTTP 协议。直接封装 libcurl 以使用 [HTTP interface](/zh/concepts/features/interfaces/http)。
* DNS 解析、端点故障转移、连接池、重试和退避。
* TLS 上下文生命周期。OpenSSL 后端使用的是你已建立连接的 `SSL`。
* 线程。每个 `chc_client` 按设计都是单线程的。
* 库内部的异步 I/O。阻塞式 client 会同步调用 `chc_io.read`。如果需要一个
  自身不执行任何 I/O 的事件循环 client，请使用 [ioless client](#async-client)。

<div id="headers">
  ## 库的组织方式
</div>

`clickhouse-c` 以一组扁平的头文件形式发布。每个头文件同时包含声明和实现，
并由哨兵宏保护。请按构建需要选择相应的头文件。

| Header                                                                                                           | Purpose                                                        | Link flags       |
| ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ---------------- |
| [`clickhouse.h`](https://github.com/ClickHouse/clickhouse-c/blob/main/doc/clickhouse.md)                         | Core：类型、错误、内存分配器、I/O vtable、类型名解析器、块读取器和写入器                    | —                |
| [`clickhouse-client.h`](https://github.com/ClickHouse/clickhouse-c/blob/main/doc/clickhouse-client.md)           | TCP 数据包循环：Hello、Query、Data、EndOfStream、Exception、Progress、Pong | —                |
| [`clickhouse-async.h`](https://github.com/ClickHouse/clickhouse-c/blob/main/doc/clickhouse-async.md)             | 无 I/O 客户端：由调用方提交字节驱动的相同数据包循环，不使用套接字                            | —                |
| [`clickhouse-compression.h`](https://github.com/ClickHouse/clickhouse-c/blob/main/doc/clickhouse-compression.md) | 压缩帧布局、CityHash128、编解码器分发，以及 `LZ4`/`ZSTD` 适配器                   | `-llz4 -lzstd`   |
| [`clickhouse-posix-io.h`](https://github.com/ClickHouse/clickhouse-c/blob/main/doc/clickhouse-posix-io.md)       | 基于阻塞 `read(2)`/`write(2)` 的 I/O 后端                             | —                |
| [`clickhouse-openssl.h`](https://github.com/ClickHouse/clickhouse-c/blob/main/doc/clickhouse-openssl.md)         | 基于 `SSL_read`/`SSL_write` 的 I/O 后端                             | `-lssl -lcrypto` |

<div id="server-setting">
  ## 必需的服务器设置
</div>

解码器会从 wire 中读取可打印的类型名称，因此它们必须以文本形式编码。ClickHouse
默认会以文本形式写入它们，但请在查询中显式固定此设置，以免服务器或 session profile
将其设为二进制后导致解码失败：

```plaintext theme={null}
output_format_native_encode_types_in_binary_format = 0
```

<div id="adding-to-project">
  ## 将其添加到你的项目中
</div>

无需安装任何软件包，因此你应通过 Git 子模块或直接拷贝的方式，将这些头文件纳入代码树中。
必须且只能有一个编译单元定义 `CHC_IMPLEMENTATION` 并包含实现；
其他所有编译单元都只包含相同的头文件，以获得声明。

```c theme={null}
/* clickhouse_impl.c */
#define CHC_IMPLEMENTATION
#include "clickhouse.h"
#include "clickhouse-posix-io.h"
#include "clickhouse-client.h"
#include "clickhouse-compression.h"
```

```c theme={null}
/* every other TU */
#include "clickhouse.h"
#include "clickhouse-client.h"
```

在包含 `clickhouse.h` 之前定义 `CHC_PROVIDE_STDLIB_ALLOC`，即可使用 `chc_alloc_stdlib`。
为 `clickhouse-compression.h` 定义 `CHC_NO_LZ4` 或 `CHC_NO_ZSTD`，即可去除对 lz4/zstd 的依赖项。

<div id="connecting-over-tcp">
  ## 通过 TCP 连接
</div>

要与 ClickHouse server 通信，需要自行建立套接字，将其封装为 `chc_io`，然后将其传给
`chc_client_init`，它会同步执行 Hello 握手。该库不负责 DNS、
故障转移、重连或连接池化——这些都需要由调用方处理。

```c theme={null}
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`) ，则由你决定。

<div id="running-a-query">
  ## 运行查询
</div>

发送查询后，持续接收数据包，直到 `CHC_PKT_END_OF_STREAM`。使用 `chc_client_send_query_ex` 可
附带[所需的服务器级设置](#server-setting)；而基础版 `chc_client_send_query` 会发送一个
空的设置列表，并继承服务器的默认设置。

```c theme={null}
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` 数据包的形式到达，而不是作为
`chc_client_recv_packet` 的非 OK 返回值。只有传输层故障才会返回非 OK。结果中的第一个 `CHC_PKT_DATA`
数据包是一个描述 schema 且包含零行的头部块；后面跟着的是数据块。
`chc_packet_clear` 会释放该数据包的块或异常——如果要改为自行接管其所有权，请先将数据包中的这些字段设为 NULL。

<div id="reading-column-data">
  ## 读取列数据
</div>

块采用列式布局。每一列都有一个由 `chc_column_layout` 返回的物理布局，
你需要根据它进行分派处理；其声明类型来自 `chc_block_column_type`。复合布局可以嵌套，因此
读取 `Nullable(Array(String))` 意味着先拆开 Nullable，遍历数组偏移量，然后
切分字符串数据。

| 布局                        | 访问器                                                                                                               |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `CHC_COL_FIXED`           | `chc_column_fixed_data(c, &elem_size)` — `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 = `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 列的读取器：

```c theme={null}
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` 数据在线上传输时采用 小端字节序；在 big-endian 主机上，你需要自行对多字节整数进行字节交换。
偏移量和 LowCardinality 键在解码时就已经转换为主机字节序。
UUID 由两个 小端字节序 的 `UInt64` 半部分组成，IPv4 是一个 4 字节的 小端字节序 整数，而 IPv6 采用 network byte order。
`DateTime64` tick 以 UTC 为准——类型中的 timezone 仅是元数据。

从不受信任的对端摄取数据时，在遍历每一列之前都要调用 `chc_column_validate` 进行校验。
`chc_block_read` 不会验证跨字段约束，例如数组偏移量和 LowCardinality 键，因此伪造的块可能会导致读取越过内部列的边界。

<div id="inserting-data">
  ## 插入数据
</div>

使用 `chc_block_builder` 构建一个块，然后将其交给 `chc_client_send_data`。该构建器记录的是
指针而不是复制数据，因此各列的 slab 必须在发送完成前一直有效。一次 INSERT 会先发送查询，
等待服务器的头部块，再发送一个或多个数据块，最后发送一个空块来
终止 stream。

```c theme={null}
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_builder *bb = NULL;
chc_block_builder_init(&bb, &al, &err);

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

/* 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_block_builder_append_string(bb, "message", 7, offsets, bytes, 3, &err);

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_block_builder_destroy(bb);
chc_type_destroy(u64, &al);
```

`chc_block_builder_append_fixed` 接受 `n_rows * elem_size` 个小端字节序字节；
`chc_block_builder_append_string` 接受基于主机字节序、作用于紧凑 slab 的累计结束偏移量 (不包含终点) 。让 builder 经由 `chc_client_send_data` 而不是更底层的
`chc_block_write` 发送，可使 client 根据协商后的 revision 设置块选项并应用
压缩。

<div id="compression">
  ## 压缩
</div>

在 `chc_client_opts` 中传入压缩模式和一个已填充的 编解码器。客户端会解压传入的
Data 包，并压缩传出的数据包。压缩请求头内置了 `LZ4` 和 `ZSTD` 适配器；
每个 init 只会填充各自的槽位，因此若要同时支持两者，请把它们都调用一次。

```c theme={null}
#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` 中声明。

<div id="tls">
  ## TLS
</div>

`clickhouse-openssl.h` 基于 `SSL_read`/`SSL_write` 提供了一个 `chc_io` 后端。OpenSSL 需要由你自行驱动：
该库不会创建 `SSL_CTX`、验证证书、设置 SNI，或调用 `SSL_connect` /
`SSL_shutdown`。在 `chc_io.read` 触发之前，握手必须已经完成。

```c theme={null}
#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](/zh/products/cloud/getting-started/intro) 和其他启用 TLS 的部署使用 9440
端口上的原生协议。两个后端都接受可选的 `check_cancel` 回调，该回调会在两次读取之间轮询；并且可通过 `chc_openssl_io_set_deadline` / `chc_posix_io_set_deadline` 设置读取截止时间。

<div id="async-client">
  ## Ioless (异步) 客户端
</div>

`clickhouse-async.h` 是面向事件循环的 TCP 客户端 ioless 变体。它完全不会操作
套接字：你提交已接收的字节，取出它需要发送的字节，并自行驱动 `epoll`、
`io_uring` 或 `WaitLatchOrSocket`。其选项、数据包类型和块构建器与阻塞式客户端
相同。

`chc_async_client_init` 不执行 I/O，也不会阻塞。之后的握手会以可恢复的
状态机方式运行，每次发送和接收也都是如此。当解析超出你已提交的字节时，
调用会返回 `CHC_WOULD_BLOCK`，而不是发生阻塞——提交更多入站字节后再次调用，
parser 会从块中间继续解析。

```c theme={null}
#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` 会在两个方向上传输字节。对于 Outbound，`chc_async_pending_out` 会返回一个指向已排队字节的指针及其长度；当套接字接收了其中一部分后，用该数量调用 `chc_async_consume_out`，部分写入也是可以的。对于 Inbound，将从套接字读取的数据传给 `chc_async_submit`。发送操作永远不会阻塞，也不会施加背压，因此要留意待发送数据的长度，并在其增长过大时停止继续发送。

一个可用的 liburing driver 位于
[`test/test_async_uring.c`](https://github.com/ClickHouse/clickhouse-c/blob/main/test/test_async_uring.c)。

<div id="allocator">
  ## 内存与分配器
</div>

每个入口点都接受一个 `chc_alloc` vtable，因此内存分配会遵循宿主所采用的机制。

```c theme={null}
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;
```

在包含 `clickhouse.h` 之前定义 `CHC_PROVIDE_STDLIB_ALLOC`，并调用 `chc_alloc_stdlib()` 以使用
基于标准 `malloc` 的分配器。

<div id="errors">
  ## 错误和服务器异常
</div>

函数返回 `CHC_OK` (0) 或非零的 `CHC_ERR_*` 代码。该代码即返回值；由
调用方在栈上分配的 `chc_err` 保存便于人类阅读的错误消息。该库绝不会在堆上分配
错误对象。

```c theme={null}
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` 的形式在数据包 stream 中传输，并携带 server 的 `code`、`display_text` 和 `stack_trace`。请仅将
`chc_err` 检查用于传输、协议和解码失败。

<div id="supported-types">
  ## 支持的数据类型
</div>

块读取器可解码：

* `Int8`–`Int256`, `UInt8`–`UInt256`
* `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` 列 (见下文)

只有在查询设置了 `output_format_native_write_json_as_string=1` 时，`JSON` 和 `Object('json')` 才会被解码。
每一行都会以 `CHC_COL_STRING` 列中的一个 JSON 文档形式传入，因此可由字符串访问器读取；
构建器则使用 `chc_block_builder_append_json_string` 写入相同的形态。任何其他 JSON
序列化版本都会返回 `CHC_ERR_TYPE`，并注明该设置。

`Variant`、`Dynamic`、`AggregateFunction` 目前尚不能解码，返回 `CHC_ERR_TYPE`；
可作为回退方案，在服务端将其 CAST 为 `String`。
