> ## 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.

> Client C header-only pour le protocole natif de ClickHouse, conçu pour être embarqué.

# Client C ClickHouse

`clickhouse-c` est un client C header-only pour le [protocole natif](/fr/concepts/features/interfaces/tcp) de ClickHouse.
Le code source et la référence de chaque en-tête se trouvent dans le [dépôt GitHub](https://github.com/ClickHouse/clickhouse-c).

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](/fr/reference/formats/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.

<Note>
  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](https://github.com/ClickHouse/clickhouse-c/issues).
  Gardez toutefois à l’esprit que l’absence de certaines fonctionnalités est intentionnelle.
</Note>

<div id="non-goals">
  ## Ce que la bibliothèque ne fait pas
</div>

Ce sont des non-objectifs délibérés. Gérez-les dans votre application ou avec une bibliothèque sœur :

* Protocole HTTP. Encapsulez directement libcurl pour l’[interface HTTP](/fr/concepts/features/interfaces/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 `SSL` déjà connecté.
* Gestion des threads. Chaque `chc_client` est mono-thread par conception.
* E/S asynchrones au sein de la bibliothèque. Le client bloquant appelle `chc_io.read` de manière synchrone. Pour un
  client à boucle d’événements qui n’effectue lui-même aucune E/S, utilisez le [client ioless](#async-client).

<div id="headers">
  ## Organisation de la bibliothèque
</div>

`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`](https://github.com/ClickHouse/clickhouse-c/blob/main/doc/clickhouse.md)                         | Cœur : types, erreurs, allocateur, vtable d'E/S, parseur de noms de type, lecture et écriture de blocs | —                  |
| [`clickhouse-client.h`](https://github.com/ClickHouse/clickhouse-c/blob/main/doc/clickhouse-client.md)           | Boucle de paquets TCP : Hello, Query, Data, EndOfStream, Exception, Progress, Pong                     | —                  |
| [`clickhouse-async.h`](https://github.com/ClickHouse/clickhouse-c/blob/main/doc/clickhouse-async.md)             | Client ioless : la même boucle de paquets, pilotée par l'appelant via l'envoi d'octets, sans socket    | —                  |
| [`clickhouse-compression.h`](https://github.com/ClickHouse/clickhouse-c/blob/main/doc/clickhouse-compression.md) | Organisation des frames compressées, CityHash128, répartition des codecs et adaptateurs `LZ4`/`ZSTD`   | `-llz4 -lzstd`     |
| [`clickhouse-posix-io.h`](https://github.com/ClickHouse/clickhouse-c/blob/main/doc/clickhouse-posix-io.md)       | Backend d'E/S reposant sur `read(2)`/`write(2)` bloquants                                              | —                  |
| [`clickhouse-openssl.h`](https://github.com/ClickHouse/clickhouse-c/blob/main/doc/clickhouse-openssl.md)         | Backend d'E/S reposant sur `SSL_read`/`SSL_write`                                                      | `-lssl -lcrypto`   |

<div id="server-setting">
  ## Paramètre du serveur requis
</div>

Le décodeur lit des noms de type lisibles sur le wire ; ils doivent donc être encodés en texte. ClickHouse
les écrit en texte par défaut, mais forcez ce paramètre dans vos requêtes afin qu’un profil serveur ou de session
qui le définit en binaire ne puisse pas compromettre le décodage :

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

<div id="adding-to-project">
  ## L’intégrer à votre projet
</div>

Il n’y a aucun paquet à installer, vous devez donc ajouter les fichiers d’en-tête à votre arborescence via un sous-module Git ou une copie.
Une seule unité de traduction définit `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.

```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"
```

Définissez `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.

<div id="connecting-over-tcp">
  ## Connexion via TCP
</div>

Pour communiquer avec un serveur ClickHouse, configurez vous-même le socket, encapsulez-le dans un `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.

```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);
```

Chaque `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.

<div id="running-a-query">
  ## Exécuter une requête
</div>

Envoyez la requête, puis consommez les paquets jusqu’à `CHC_PKT_END_OF_STREAM`. Utilisez `chc_client_send_query_ex` pour
ajouter le [paramètre serveur requis](#server-setting) ; `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.

```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;
}
```

Les exceptions du serveur arrivent sous forme de paquets `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.

<div id="reading-column-data">
  ## Lecture des données de colonnes
</div>

Les blocs sont orientés colonnes. Chaque colonne a une disposition physique, renvoyée par `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                               |

Un lecteur pour les colonnes numériques simples, String et 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;
    }
}
```

Les données `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.

<div id="inserting-data">
  ## Insertion de données
</div>

Construisez un bloc avec `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.

```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` 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.

<div id="compression">
  ## Compression
</div>

Indiquez un mode de compression et un codec renseigné dans `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.

```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,
};
```

Pour utiliser une bibliothèque de compression pour laquelle le projet ne fournit pas de liaison, définissez vous-même un `chc_codec` ;
la vtable est déclarée dans `clickhouse-compression.h`.

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

`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.

```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](/fr/products/cloud/getting-started/intro) et les autres déploiements où TLS est activé utilisent le protocole natif sur
le port 9440. Les deux backends acceptent une fonction de rappel `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`.

<div id="async-client">
  ## Client ioless (async)
</div>

`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.

```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;
}
```

Votre `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`](https://github.com/ClickHouse/clickhouse-c/blob/main/test/test_async_uring.c).

<div id="allocator">
  ## Mémoire et l’allocateur
</div>

Chaque point d’entrée accepte une vtable `chc_alloc`, de sorte que l’allocation s’appuie sur le mécanisme utilisé par le système hôte.

```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;
```

Définissez `CHC_PROVIDE_STDLIB_ALLOC` avant d’inclure `clickhouse.h` et appelez `chc_alloc_stdlib()` pour utiliser un
allocateur standard basé sur `malloc`.

<div id="errors">
  ## Erreurs et exceptions du serveur
</div>

Les fonctions renvoient `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.

```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;
```

Les erreurs de requête côté serveur ne sont pas des erreurs `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.

<div id="supported-types">
  ## Types de données pris en charge
</div>

Le lecteur de blocs décode :

* `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)`, qui est décodé comme son `T` interne
* `JSON` et `Object('json')`, sous forme de colonnes `String` avec 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.
