clickhouse-c est un client C header-only pour le protocole natif de ClickHouse.
Le code source et la référence de chaque en-tête se trouvent dans le dépôt GitHub.
Contrairement aux clients de plus haut niveau, il en fait volontairement très peu pour vous. L’en-tête principal décode et
encode des blocs au format Native via un callback d’E/S que vous fournissez. Vous gérez
vous-même le socket, le contexte TLS, l’allocateur, les reprises sur erreur et le pool de connexions. Cela le rend suffisamment petit pour être
embarqué : inclure uniquement clickhouse.h n’ajoute aucune dépendance à l’édition de liens autre que libc.
Cette bibliothèque est en développement actif. La v1 décode les principaux types de ClickHouse.
Signalez les limitations ou les fonctionnalités manquantes via le suivi des issues.
Gardez toutefois à l’esprit que l’absence de certaines fonctionnalités est intentionnelle.
Ce que la bibliothèque ne fait pas
- Protocole HTTP. Encapsulez directement libcurl pour l’interface HTTP.
- Résolution DNS, basculement d’endpoint, pool de connexions, réessais et backoff.
- Cycle de vie du contexte TLS. Le backend OpenSSL utilise un
SSLdéjà connecté. - Gestion des threads. Chaque
chc_clientest mono-thread par conception. - E/S asynchrones au sein de la bibliothèque. Le client bloquant appelle
chc_io.readde manière synchrone. Pour un client à boucle d’événements qui n’effectue lui-même aucune E/S, utilisez le client ioless.
Organisation de la bibliothèque
clickhouse-c est fourni sous la forme d’un ensemble plat d’en-têtes. Chaque en-tête contient à la fois les déclarations et l’implémentation,
protégées par une macro sentinelle. Choisissez les en-têtes nécessaires à votre compilation.
| En-tête | Rôle | Options de liaison |
|---|---|---|
clickhouse.h | Cœur : types, erreurs, allocateur, vtable d’E/S, parseur de noms de type, lecture et écriture de blocs | — |
clickhouse-client.h | Boucle de paquets TCP : Hello, Query, Data, EndOfStream, Exception, Progress, Pong | — |
clickhouse-async.h | Client ioless : la même boucle de paquets, pilotée par l’appelant via l’envoi d’octets, sans socket | — |
clickhouse-compression.h | Organisation des frames compressées, CityHash128, répartition des codecs et adaptateurs LZ4/ZSTD | -llz4 -lzstd |
clickhouse-posix-io.h | Backend d’E/S reposant sur read(2)/write(2) bloquants | — |
clickhouse-openssl.h | Backend d’E/S reposant sur SSL_read/SSL_write | -lssl -lcrypto |
Paramètre du serveur requis
L’intégrer à votre projet
CHC_IMPLEMENTATION et y intègre l’implémentation ;
toutes les autres unités incluent les mêmes fichiers d’en-tête uniquement pour les déclarations.
CHC_PROVIDE_STDLIB_ALLOC avant d’inclure clickhouse.h pour utiliser chc_alloc_stdlib.
Définissez CHC_NO_LZ4 ou CHC_NO_ZSTD pour clickhouse-compression.h afin de supprimer les dépendances à lz4/zstd.
Connexion via TCP
chc_io, puis transmettez-le à chc_client_init, qui exécute la négociation initiale Hello de façon synchrone. La bibliothèque ne prend en charge ni le DNS, ni le failover, ni la reconnexion, ni le pool de connexions — cela relève de l’appelant.
chc_client est mono-thread et encapsule une connexion. La bibliothèque appelle les callbacks chc_io
de manière synchrone ; ce que ces callbacks font en interne (epoll, io_uring,
WaitLatchOrSocket) vous appartient.
Exécuter une requête
CHC_PKT_END_OF_STREAM. Utilisez chc_client_send_query_ex pour
ajouter le paramètre serveur requis ; chc_client_send_query, sans argument supplémentaire, envoie une
liste des paramètres vide et hérite des valeurs par défaut du serveur.
CHC_PKT_EXCEPTION, et non via un retour non-OK de
chc_client_recv_packet. Seules les défaillances au niveau du transport renvoient une valeur non-OK. Le premier paquet CHC_PKT_DATA
d’un résultat est un bloc d’en-tête qui décrit le schéma avec zéro ligne ; les blocs de données suivent.
chc_packet_clear libère le bloc ou l’exception du paquet — mettez d’abord ces champs du paquet à null pour
en prendre possession à la place.
Lecture des données de colonnes
chc_column_layout, sur
laquelle vous aiguillez le traitement ; son type déclaré provient de chc_block_column_type. Les dispositions composites sont imbriquées, donc
lire un Nullable(Array(String)) consiste à déballer le Nullable, à parcourir les offsets du tableau, puis
à découper les données de chaîne.
| Disposition | Accesseurs |
|---|---|
CHC_COL_FIXED | chc_column_fixed_data(c, &elem_size) — n_rows * elem_size octets en little-endian |
CHC_COL_STRING | chc_column_string_data(c), chc_column_string_offsets(c) — offsets[i] est la fin exclusive de la ligne i dans l’ordre des octets de l’hôte ; la ligne 0 commence à 0 |
CHC_COL_NULLABLE | chc_column_null_map(c) (un octet par ligne, 1 = NULL), chc_column_nullable_inner(c) |
CHC_COL_ARRAY | chc_column_array_offsets(c) (fins cumulées), chc_column_array_values(c) ; Map se décode comme Array(Tuple(K, V)) |
CHC_COL_TUPLE | chc_column_tuple_arity(c), chc_column_tuple_child(c, i) — chaque élément enfant a le même nombre de lignes |
CHC_COL_LOW_CARDINALITY | chc_column_lc_key_size(c) (1/2/4/8), chc_column_lc_keys(c), chc_column_lc_dict(c) ; l’entrée 0 du dictionnaire est la valeur par défaut |
CHC_COL_FIXED sont en little-endian sur le wire ; sur les hôtes big-endian, vous
devez vous-même permuter les octets des entiers multi-octets. Les offsets et les clés LowCardinality sont déjà convertis dans l’ordre de l’hôte au moment du décodage.
Les UUIDs se composent de deux moitiés UInt64 little-endian, IPv4 est un entier little-endian sur 4 octets, et IPv6 est en
network byte order. Les ticks DateTime64 sont en UTC — le fuseau horaire dans le type n’est qu’une métadonnée.
Lors de l’ingestion depuis un pair non fiable, appelez chc_column_validate sur chaque colonne avant de la parcourir.
chc_block_read ne valide pas les invariants entre champs, tels que les offsets de tableau et les
clés LowCardinality, donc un bloc falsifié pourrait sinon lire au-delà des limites de la colonne interne.
Insertion de données
chc_block_builder, puis transmettez-le à chc_client_send_data. Le builder enregistre
des pointeurs au lieu de copier les données ; les zones mémoire des colonnes doivent donc rester valides après l’envoi. Un INSERT envoie la requête,
attend le bloc d’en-tête du serveur, envoie un ou plusieurs blocs de données, puis envoie un bloc vide pour
terminer le flux.
chc_block_builder_append_fixed prend n_rows * elem_size octets little-endian ;
chc_block_builder_append_string prend des offsets de fin cumulatifs exclusifs, dans l’ordre des octets de l’hôte, sur un
slab compact. Faire passer le builder par chc_client_send_data plutôt que par la fonction de plus bas niveau
chc_block_write permet au client de définir les options du bloc à partir de la révision négociée et d’appliquer la
compression.
Compression
chc_client_opts. Le client décompresse les
paquets Data entrants et compresse les paquets sortants. L’en-tête de compression fournit des adaptateurs LZ4 et ZSTD ;
chaque initialisation ne remplit que ses propres slots, appelez donc les deux pour prendre en charge l’un comme l’autre.
chc_codec ;
la vtable est déclarée dans clickhouse-compression.h.
TLS
clickhouse-openssl.h fournit un backend chc_io reposant sur SSL_read/SSL_write. C’est à vous de piloter OpenSSL :
la bibliothèque ne crée jamais de SSL_CTX, ne vérifie pas les certificats, ne configure pas le SNI et n’appelle pas SSL_connect /
SSL_shutdown. Lorsque chc_io.read est appelé, la négociation doit déjà être terminée.
check_cancel facultative, consultée entre les opérations de lecture, ainsi qu’un
délai limite de lecture via chc_openssl_io_set_deadline / chc_posix_io_set_deadline.
Client ioless (async)
clickhouse-async.h est une variante ioless du client TCP conçue pour les boucles d’événements. Il n’utilise jamais de
socket : vous lui fournissez les octets reçus et récupérez ceux qu’il souhaite envoyer, en pilotant vous-même epoll,
io_uring ou WaitLatchOrSocket. Les options, les types de paquets et le block builder sont les
mêmes que pour le client bloquant.
chc_async_client_init n’effectue aucune E/S et ne peut pas bloquer. La négociation s’exécute ensuite sous la forme d’une
machine à états reprenable, tout comme chaque envoi et chaque réception. Lorsqu’un parse dépasse les octets que vous avez fournis, l’appel
renvoie CHC_WOULD_BLOCK au lieu de bloquer — fournissez plus d’octets entrants et rappelez la
fonction, et le parser reprend au milieu du bloc.
pump fait circuler des octets dans les deux sens. En sortie, chc_async_pending_out renvoie un pointeur et une longueur
sur les octets en file d’attente ; une fois qu’une partie a été acceptée par le socket, appelez chc_async_consume_out avec ce nombre ; une
écriture partielle est acceptable. En entrée, transmettez à chc_async_submit les données lues sur le socket. Les envois ne bloquent jamais et n’exercent pas de
backpressure ; surveillez donc la taille des données en attente de sortie et cessez d’envoyer lorsque celle-ci devient trop importante.
Un pilote liburing fonctionnel se trouve dans
test/test_async_uring.c.
Mémoire et l’allocateur
chc_alloc, de sorte que l’allocation s’appuie sur le mécanisme utilisé par le système hôte.
CHC_PROVIDE_STDLIB_ALLOC avant d’inclure clickhouse.h et appelez chc_alloc_stdlib() pour utiliser un
allocateur standard basé sur malloc.
Erreurs et exceptions du serveur
CHC_OK (0) ou un code CHC_ERR_* non nul. Le code est la valeur de retour ; une
chc_err allouée sur la pile par l’appelant contient le message en clair. La bibliothèque n’alloue jamais
d’erreur sur le tas.
chc_err. Elles arrivent dans le flux de paquets sous la forme de
CHC_PKT_EXCEPTION, avec les champs code, display_text et stack_trace du serveur. Réservez la vérification de
chc_err aux erreurs de transport, de protocole et de décodage.
Types de données pris en charge
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), qui est décodé comme sonTinterneJSONetObject('json'), sous forme de colonnesStringavec la sérialisation en chaîne de caractères (voir ci-dessous)
JSON et Object('json') ne sont décodés que lorsque la requête définit output_format_native_write_json_as_string=1.
Chaque ligne arrive sous la forme d’un document JSON dans une colonne CHC_COL_STRING, que les accesseurs de chaîne peuvent donc lire ;
le builder écrit la même structure avec chc_block_builder_append_json_string. Toute autre version de sérialisation JSON
renvoie CHC_ERR_TYPE en indiquant le paramètre.
Variant, Dynamic et AggregateFunction ne sont pas encore décodés et renvoient CHC_ERR_TYPE ;
convertissez-les en String côté serveur comme solution de repli.