clickhouse-c — это C-клиент, состоящий только из заголовочных файлов, для ClickHouse собственного протокола.
Исходный код и справочная информация по каждому заголовочному файлу доступны в репозитории GitHub.
В отличие от более высокоуровневых клиентов, он намеренно берёт на себя минимум. Основной заголовочный файл декодирует и
кодирует блоки формата Native через предоставленный вами callback ввода-вывода. Управление
сокетом, TLS-контекстом, аллокатором, retries и пулом соединений остаётся на вашей стороне. Благодаря этому библиотека достаточно мала для
встраивания: подключение одного лишь clickhouse.h не добавляет зависимостей на этапе линковки, кроме libc.
Эта библиотека находится в активной разработке. Версия v1 декодирует основные типы ClickHouse.
Сообщайте об ограничениях или недостающей функциональности через трекер issues.
Однако имейте в виду, что часть функциональности в этой библиотеке отсутствует намеренно.
Чего библиотека не делает
- HTTP-протокол. Напрямую оберните libcurl для HTTP-интерфейса.
- DNS-разрешение, переключение конечной точки при отказе, пул соединений, повторные попытки и задержка.
- Жизненный цикл TLS-контекста. Backend OpenSSL использует уже подключённый вами
SSL. - Потоки. Каждый
chc_clientпо своей архитектуре однопоточный. - Асинхронный I/O внутри библиотеки. Блокирующий клиент вызывает
chc_io.readсинхронно. Для клиента на основе цикла событий, который сам не выполняет I/O, используйте клиент без I/O.
Как устроена библиотека
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, повторным подключением или 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 освобождает блок или исключение пакета’а — чтобы вместо этого забрать владение, сначала
обнулите эти поля в пакете.
Чтение данных столбцов
chc_column_layout;
по ней выбирается способ обработки, а объявленный тип возвращает chc_block_column_type. Составные структуры могут быть вложенными, поэтому
чтение Nullable(Array(String)) означает, что нужно снять обёртку Nullable, обойти смещения массива, а затем
выделить срезы строковых данных.
Пример чтения обычных числовых, строковых столбцов и столбцов с типом Nullable:
CHC_COL_FIXED при передаче имеют порядок байтов little-endian; на хостах с порядком
байтов big-endian вам нужно самостоятельно переставлять байты в многобайтовых целых числах. Смещения и ключи LowCardinality уже приводятся к порядку байтов хоста при декодировании.
UUID представляются как две половины UInt64 в little-endian, IPv4 — как 4-байтовое little-endian-целое число, а IPv6 —
в сетевом порядке байтов. Тики DateTime64 — в UTC; часовой пояс в типе — это лишь метаданные.
При приёме данных от недоверенного peer вызывайте chc_column_validate для каждого столбца, прежде чем обходить
его. chc_block_read не проверяет межполевые инварианты, такие как смещения массивов и
ключи LowCardinality, поэтому без такой проверки поддельный блок может прочитать данные за пределами границ внутренних столбцов.
Вставка данных
chc_build_*, добавьте их в chc_block_builder, а затем передайте его в
chc_client_send_data. chc_block_builder использует хранилище, предоставленное вызывающей стороной, и сохраняет указатели, а не
копирует данные, поэтому хранилище, деревья столбцов, типы, имена и slabs должны существовать дольше, чем длится отправка. INSERT
отправляет запрос, ожидает заголовочный блок от сервера, отправляет один или несколько блоков данных, а затем отправляет пустой
блок, чтобы завершить поток.
chc_build_fixed принимает n_rows * elem_size байт в формате little-endian; chc_build_string принимает накопительные
исключающие конечные смещения в порядке байт хоста для упакованного slab. Вспомогательные функции возвращают узлы столбцов по
значению. Вкладывайте их в соответствии с типом: например, передайте фиксированный или строковый узел в
chc_build_nullable, затем передайте результат в chc_build_array и добавьте корневой узел массива. Tuple,
LowCardinality, Map и Geo-столбцы используют одно и то же дерево: Map — это Array(Tuple(K, V)).
Все столбцы в блоке должны иметь одинаковое количество строк на верхнем уровне. writer проверяет дерево на соответствие
разобранному типу ClickHouse, но вызывающая сторона должна выделить хранилище chc_block_col для каждого append.
Вы также можете напрямую добавить декодированный столбец из chc_block_column, чтобы повторно закодировать его, или вызвать
chc_block_write_cols с массивом chc_block_col, чтобы обойтись без builder. Использование builder через
chc_client_send_data, а не через низкоуровневый chc_block_write, позволяет client задавать параметры блока
на основе согласованной ревизии и применять сжатие.
Сжатие
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 (async) клиент
clickhouse-async.h — это ioless-вариант TCP-клиента для циклов событий. Он вообще не работает с
сокетом: вы передаёте полученные байты и считываете байты, которые клиент хочет отправить, самостоятельно управляя 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 данные, прочитанные из сокета. Отправка никогда не блокируется и не создает
обратного давления, поэтому следите за длиной pending-out и прекращайте отправку, когда она становится слишком большой.
Рабочий драйвер 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_build_string;
модуль записи добавляет префикс, необходимый для разобранного типа.
Variant, Dynamic, AggregateFunction пока не декодируются и возвращают CHC_ERR_TYPE;
в качестве обходного варианта приводите их к String на стороне server.