clickhouse-c — это header-only C-клиент для ClickHouse собственного протокола.
Исходный код и справочник по каждому заголовочному файлу доступны в репозитории GitHub.
В отличие от более высокоуровневых клиентов, он намеренно берет на себя минимум. Основной заголовочный файл декодирует и
кодирует блоки формата Native через переданный вами I/O-колбэк. За сокет, TLS-контекст,
аллокатор, повторные попытки и пул соединений отвечаете вы. Благодаря этому библиотека достаточно компактна для
встраивания: одно только подключение clickhouse.h не добавляет зависимостей на этапе компоновки, кроме libc.
Эта библиотека находится в активной разработке. Версия v1 декодирует основные типы ClickHouse.
Сообщайте об ограничениях или недостающей функциональности через issue tracker.
Однако имейте в виду, что часть функциональности в этой библиотеке отсутствует намеренно.
Чего библиотека не делает
- HTTP-протокол. Используйте libcurl напрямую для HTTP-интерфейса.
- Разрешение DNS-имён, переключение конечных точек при отказе, пул соединений, повторные попытки и задержка между ними.
- Управление жизненным циклом TLS-контекста. Реализация на OpenSSL использует
SSL, к которому вы уже подключились. - Многопоточность. Каждый
chc_clientпо замыслу однопоточный. - Асинхронный I/O внутри библиотеки. Блокирующий клиент синхронно вызывает
chc_io.read. Для клиента с циклом событий, который сам не выполняет I/O, используйте клиент без I/O.
Как организована библиотека
clickhouse-c поставляется в виде плоского набора заголовочных файлов. Каждый заголовок содержит и объявления, и реализацию,
защищённые сигнальным макросом. Выбирайте заголовки, которые нужны вашей сборке.
| Header | Назначение | Флаги линковки |
|---|---|---|
clickhouse.h | Ядро: типы, ошибки, аллокатор, vtable I/O, парсер имён типов, средство чтения блоков и средство записи | — |
clickhouse-client.h | Цикл обработки TCP-пакетов: Hello, Query, Data, EndOfStream, Exception, Progress, Pong | — |
clickhouse-async.h | Клиент без I/O: тот же цикл пакетов, управляемый вызывающей стороной через передачу байтов, без socket | — |
clickhouse-compression.h | Структура сжатого фрейма, CityHash128, диспетчеризация кодеков и адаптеры LZ4/ZSTD | -llz4 -lzstd |
clickhouse-posix-io.h | I/O backend поверх блокирующих read(2)/write(2) | — |
clickhouse-openssl.h | I/O 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,
переключением при отказе, повторным подключением или пулом соединений — это обязанность вызывающего кода.
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 освобождает блок или исключение пакета — чтобы вместо этого взять владение на себя, сначала
обнулите эти поля в пакете.
Чтение данных столбцов
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 байт в формате 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 в словаре — значение по умолчанию |
CHC_COL_FIXED при передаче имеют порядок little-endian; на хостах с big-endian вы сами выполняете байтовую перестановку
для многобайтовых целых чисел. Смещения и ключи LowCardinality уже приводятся к порядку байтов хоста при декодировании.
UUID — это две little-endian половины UInt64, IPv4 — 4-байтовое little-endian целое число, а IPv6 использует
network byte order. Тики DateTime64 отсчитываются в UTC — timezone в типе служит лишь как metadata.
При приёме данных от недоверенного peer вызывайте chc_column_validate для каждого столбца перед обходом
его содержимого. chc_block_read не проверяет инварианты между полями, такие как смещения массивов и
ключи 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 принимает накопительные конечные смещения exclusive в порядке байтов хоста для
упакованного slab. Передача построителя блоков через chc_client_send_data, а не через более низкоуровневый
chc_block_write, позволяет клиенту задавать параметры блока в соответствии с согласованной ревизией и применять
сжатие.
Сжатие
chc_client_opts. Клиент распаковывает входящие
пакеты данных и сжимает исходящие. В заголовочном файле сжатия предусмотрены адаптеры 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, который опрашивается между операциями чтения, а также
deadline чтения через chc_openssl_io_set_deadline / chc_posix_io_set_deadline.
Клиент без I/O (async)
clickhouse-async.h — это вариант TCP-клиента без I/O для циклов событий. Он никогда не обращается к
сокету: вы передаёте полученные байты и забираете байты, которые он хочет отправить, самостоятельно управляя epoll,
io_uring или WaitLatchOrSocket. Параметры, типы пакетов и построитель блоков — те же, что и у
блокирующего клиента.
chc_async_client_init не выполняет I/O и не может блокироваться. После этого рукопожатие работает как
возобновляемая машина состояний, как и каждая операция отправки и приёма. Когда разбор выходит за пределы переданных вами байтов,
вызов возвращает CHC_WOULD_BLOCK вместо блокировки — передайте больше входящих байтов и вызовите его снова, и
parser продолжит работу с середины блока.
pump передаёт байты в обоих направлениях. Для исходящего трафика chc_async_pending_out возвращает указатель и длину
для байтов в очереди; после того как сокет примет часть данных, вызовите chc_async_consume_out с этим количеством —
частичная запись допустима. Для входящего трафика передавайте данные, прочитанные из сокета, в chc_async_submit. Отправка никогда не блокируется и не создаёт
обратного давления, поэтому следите за длиной очереди исходящих данных и прекращайте отправку, когда она становится слишком большой.
Рабочий драйвер liburing находится в
test/test_async_uring.c.
Память и аллокатор
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при строковой сериализации (см. ниже)
JSON и Object('json') декодируются, только если запрос задаёт output_format_native_write_json_as_string=1.
Каждая строка поступает как отдельный JSON-документ в столбце CHC_COL_STRING, поэтому его читают строковые аксессоры;
построитель записывает ту же структуру с помощью chc_block_builder_append_json_string. Любая другая версия JSON-
сериализации возвращает CHC_ERR_TYPE с указанием настройки.
Variant, Dynamic, AggregateFunction пока не декодируются и возвращают CHC_ERR_TYPE;
в качестве резервного варианта приведите их к String на стороне сервера.