> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> Spécification du protocole TCP natif de ClickHouse : tramage des paquets, cycle de vie de la connexion, négociation de version et corps de chaque message

# Protocole natif

Le protocole natif est le protocole binaire, orienté connexion, que les clients et serveurs ClickHouse utilisent pour communiquer via TCP. Il transporte les requêtes SQL, les données de résultat, les charges utiles d'`INSERT`, la télémétrie d'exécution et les signaux d'erreur. C'est sur ce protocole que reposent le client en ligne de commande, le driver C++ et la plupart des drivers natifs tiers.

Cette page décrit le protocole lui-même : le tramage des paquets, la machine à états de la connexion, la négociation de version et le corps de chaque message autre que `Block`. Les octets contenus dans les paquets de la famille `Data` (le `Block`, ses colonnes et les encodages propres à chaque type) relèvent d'un autre sujet, documenté dans la spécification du [format Native](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md).

> [!NOTE]
>
> **Spécification complémentaire**
>
> Cette page forme un ensemble avec la spécification du [format Native](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md), publiée conjointement. Les deux spécifications se répartissent clairement les rôles : cette page couvre la couche des paquets et la couche de transport, tandis que la spécification du format Native couvre les octets contenus dans les paquets de la famille `Data`.

Quelques propriétés valent dans tous les cas. Le protocole est binaire et positionnel : il n'existe aucune balise de champ, sauf dans `BlockInfo`, si bien qu'un seul octet mal placé désynchronise tout ce qui suit. Il est avec état, et chaque connexion TCP traite une seule requête à la fois — il n'y a pas de multiplexage. Les entiers de taille fixe sont encodés en little-endian.

## Vue d'ensemble

| Propriété | Valeur |
| - | - |
| Transport | TCP, éventuellement encapsulé dans TLS |
| Ordre des octets | Little-endian pour les entiers de taille fixe |
| Encodage | Binaire et positionnel (aucune balise de champ, sauf dans `BlockInfo`) |
| Modèle de connexion | Avec état (stateful), une seule requête à la fois, sans multiplexage |
| Gestion des versions | Négociée lors du handshake ; certaines fonctionnalités sont conditionnées par la version |
| Format de données | Le [format Native](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md) pour toutes les données tabulaires |

Dans le format binaire, chaque message commence par un code de type de paquet `VarUInt`, suivi d'un corps dont la structure dépend de ce code et de la version de protocole négociée.

Une connexion se déroule en trois phases : un handshake initial unique, puis un nombre quelconque d'échanges `Ping` ou `Query`, et enfin la fermeture :

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: TCP connect

    rect rgb(220, 235, 255)
        Note over C,S: Handshake
        C->>S: ClientHello (name, version, db, user, password)
        S->>C: ServerHello (server_name, version, [timezone, display_name, ...])
        Note over C,S: negotiated_version = min(client, server)
        opt negotiated_version ≥ 54458
            C->>S: Addendum (quota_key)
        end
    end

    rect rgb(220, 245, 225)
        Note over C,S: Query phase
        C->>S: Query packet (ClientInfo, settings, params, SQL)
        C->>S: External-table Data packets (0 or more)
        C->>S: Empty Data marker — the "go" signal
        loop until EndOfStream or Exception
            S->>C: Data / Progress / Log / ProfileInfo / Totals / ...
        end
        S->>C: EndOfStream
    end
```

Le protocole TCP natif transporte toujours les données tabulaires au format Native, quelle que soit la clause `FORMAT` de la requête SQL. La conversion vers `RowBinary`, `CSV`, `JSON`, etc. incombe au client, qui l'effectue après avoir décodé les blocs Native. (L'interface HTTP emprunte un chemin de code distinct qui, lui, *respecte* la clause `FORMAT` ; HTTP n'est pas traité ici.)

## Sécurité

### Sécurité du transport (TLS)

TLS opère au niveau de la couche transport, en dessous du protocole. Lorsqu'il est activé, l'intégralité du flux TCP est chiffrée, et les messages du protocole restent identiques octet pour octet, que TLS soit utilisé ou non.

### Authentification

L'authentification a lieu pendant le handshake, dans le message [`ClientHello`](#clienthello). Les champs `user` et `password` transitent sous forme de chaînes de caractères en clair : c'est donc le chiffrement au niveau de la couche transport (TLS) qui protège les identifiants en transit.

Lorsque le champ `user` est vide, le serveur utilise l'utilisateur de session par défaut configuré, défini par le paramètre serveur `default_session_user` (dont la valeur par défaut est `default`), qui peut être surchargé pour chaque listener dans la section `protocols`. Si le serveur est configuré avec un utilisateur de session par défaut vide, ou si sa version est antérieure à 26.8, un champ `user` vide est rejeté et une exception est levée. Notez que `clickhouse-client` n'envoie jamais de nom d'utilisateur vide : il le remplace par `default` côté client.

L'authentification SSH par défi-réponse est disponible à partir de la version de protocole 54466. Voir [Authentification SSH par défi-réponse](#ssh-authentication).

### Secret interserveur

Pour l'exécution de requêtes distribuées, les serveurs s'authentifient mutuellement en prouvant qu'ils connaissent un secret partagé, sans jamais faire transiter ce secret sur le réseau. Chaque paquet Query contient un `auth_hash` SHA-256 de 32 octets dans le champ 4 de [`Query`](#query), calculé à partir d'un sel, d'un nonce, du secret configuré et de la requête ; le serveur destinataire le recalcule, puis compare les deux valeurs. Ce mécanisme dépend de la fonctionnalité `INTERSERVER_SECRET` (v54441). Les clients externes envoient toujours une chaîne vide dans ce champ. Voir [Authentification inter-serveur](#inter-server-authentication).

## Gestion des versions et activation conditionnelle des fonctionnalités

### Négociation de version

Lors du handshake, le client et le serveur déclarent chacun la version de protocole maximale qu'ils prennent en charge. La **version négociée** correspond à la plus basse des deux :

```text theme={null}
negotiated_version = min(client_version, server_version)
```

Chaque message échangé par la suite s'appuie sur la version négociée pour déterminer quels champs figurent dans le format binaire.

### Activation conditionnelle des fonctionnalités

Une fonctionnalité est identifiée par la version du protocole qui l'a introduite ; elle est **active** dès lors que la version négociée est supérieure ou égale à ce numéro.

> [!WARNING]
>
> Lorsqu'une fonctionnalité est active, ses champs **doivent** figurer dans le format binaire. Le protocole étant strictement positionnel, omettre un champ soumis à une activation conditionnelle des fonctionnalités corrompt le flux d'octets pour tous les champs suivants.

### Tableau des fonctionnalités

| Fonctionnalité | Version | Concerne | Impact sur le format wire |
| - | - | - | - |
| BLOCK\_INFO | toutes | Block | Ajoute le préfixe BlockInfo (`is_overflows`, `bucket_number`) à chaque Block. |
| CLIENT\_INFO | 54032 | Query | Ajoute le bloc ClientInfo au corps de Query. |
| TIMEZONE | 54058 | ServerHello | Ajoute le champ `timezone` à ServerHello. |
| QUOTA\_KEY\_IN\_CLIENT\_INFO | 54060 | ClientInfo | Ajoute le champ `quota_key` à ClientInfo. |
| DISPLAY\_NAME | 54372 | ServerHello | Ajoute le champ `display_name` à ServerHello. |
| VERSION\_PATCH | 54401 | ServerHello, ClientInfo | Ajoute le champ `version_patch` aux deux. |
| SERVER\_LOGS | 54406 | Log | Le serveur émet des paquets Log lorsque `send_logs_level` est défini. |
| COLUMN\_DEFAULTS\_METADATA | 54410 | TableColumns | Le serveur peut envoyer le paquet [`TableColumns`](#tablecolumns) (type 11), qui contient les métadonnées des valeurs par défaut des colonnes, avant le bloc de schéma INSERT/input. Envoyé uniquement lorsque la version négociée est ≥ 54410 **et** que `input_format_defaults_for_omitted_fields` est activé. En deçà de cette version, le paquet n'est jamais envoyé ; les clients ne doivent pas l'attendre. |
| WRITE\_CLIENT\_INFO | 54420 | Progress | Ajoute `wrote_rows` et `wrote_bytes` à Progress. (Malgré son nom, cette fonctionnalité ne conditionne **pas** le bloc ClientInfo — c'est le rôle de `CLIENT_INFO` (v54032).) |
| SETTINGS\_SERIALIZED\_AS\_STRINGS | 54429 | Query (encodage des paramètres) | Modifie la **manière** dont est encodée la liste de paramètres, toujours présente ; ne détermine **pas** si les paramètres sont envoyés. À partir de v54429, chaque paramètre est écrit sous la forme `(name, flags, value-as-string)` ; les pairs plus anciens écrivent `(name, type-specific-binary-value)` sans flags. Voir [Setting](#setting). |
| INTERSERVER\_SECRET | 54441 | Query | Ajoute le champ interserveur `auth_hash` à Query — un SHA-256 salé calculé sur le secret du cluster, et non le secret brut. Les clients externes envoient une chaîne vide. Voir [Authentification interserveur](#inter-server-authentication). |
| OPEN\_TELEMETRY | 54442 | ClientInfo | Ajoute le contexte de trace OpenTelemetry à ClientInfo. |
| DISTRIBUTED\_DEPTH | 54448 | ClientInfo | Ajoute le champ `distributed_depth` à ClientInfo. |
| INITIAL\_QUERY\_START\_TIME | 54449 | ClientInfo | Ajoute le champ `initial_time` (Int64, largeur fixe). |
| PROFILE\_EVENTS | 54451 | ProfileEvents | Le serveur émet des paquets ProfileEvents pendant l'exécution de la requête. |
| PARALLEL\_REPLICAS | 54453 | ClientInfo | Ajoute à ClientInfo les champs de coordination des réplicas parallèles. |
| CUSTOM\_SERIALIZATION | 54454 | Block (Column) | Ajoute l'octet `has_custom_serialization` après la chaîne de type de chaque colonne. |
| ADDENDUM | 54458 | Handshake | Le client envoie un addendum (`quota_key`) à l'issue du handshake. |
| PARAMETERS | 54459 | Query | Ajoute la liste des paramètres au corps de Query. |
| SERVER\_QUERY\_TIME\_IN\_PROGRESS | 54460 | Progress | Ajoute le champ `elapsed_ns` à Progress. |
| PASSWORD\_COMPLEXITY\_RULES | 54461 | ServerHello | Ajoute à ServerHello une liste de motifs regex de politique de mot de passe, accompagnés de messages lisibles. |
| INTERSERVER\_SECRET\_V2 | 54462 | ServerHello | Ajoute à ServerHello un nonce `UInt64` de 8 octets. Utilisé pour la signature des requêtes interserveurs ; les clients externes le décodent et l'ignorent. |
| TOTAL\_BYTES\_IN\_PROGRESS | 54463 | Progress | Ajoute le champ `total_bytes_to_read` (VarUInt) à Progress, entre `total_rows` et `wrote_rows`. |
| TIMEZONE\_UPDATES | 54464 | TimezoneUpdate | Ajoute le paquet serveur `TimezoneUpdate` (type 17). Corps : un unique `String` contenant le fuseau horaire de la session. Envoyé uniquement par l'initialiseur de la fonction de table `input`, juste après le bloc de schéma d'entrée, afin que le client analyse les lignes qu'il envoie selon le `session_timezone` du serveur. Voir [TimezoneUpdate](#timezoneupdate). |
| SPARSE\_SERIALIZATION | 54465 | Block (Column) | Le serveur peut définir `has_custom_serialization = 1` et émettre une colonne à encodage sparse. Format wire : un octet de kind (0x01 = SPARSE), puis un flux d'offsets VarUInt terminé par EOG, puis les valeurs non par défaut, encodées de manière dense dans le type interne. Voir [kind\_stack et encodage sparse](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#kind-stack-and-sparse-encoding). |
| SSH\_AUTHENTICATION | 54466 | Flux d'authentification | Ajoute l'authentification SSH par défi-réponse. Facultative : pour la déclencher, le client envoie un `user` de la forme `" SSH KEY AUTHENTICATION " + <real_user>` avec un mot de passe vide. Voir [Authentification SSH par défi-réponse](#ssh-authentication). |
| TABLE\_READ\_ONLY\_CHECK | 54467 | TablesStatusResponse | Ajoute un flag `is_readonly` à la ligne de chaque table dans TablesStatusResponse. Pour les clients externes qui n'émettent pas de `TablesStatusRequest`, le format wire reste inchangé. |
| SYSTEM\_KEYWORDS\_TABLE | 54468 | tables système | Le serveur alimente `system.keywords` afin que le `clickhouse-client` officiel puisse proposer l'autocomplétion des mots-clés. Aucun changement du format wire du protocole natif. |
| ROWS\_BEFORE\_AGGREGATION | 54469 | ProfileInfo | Ajoute `applied_aggregation` (Bool) et `rows_before_aggregation` (VarUInt) à la fin de ProfileInfo, dans cet ordre. |
| CHUNKED\_PROTOCOL | 54470 | Tramage de la connexion | Le corps de chaque paquet est encapsulé dans un tramage par fragments. Négocié dans l'Addendum : ServerHello transmet la préférence du serveur pour chaque sens de communication, et l'Addendum transmet le choix final du client. Voir [tramage par fragments](#chunked-framing). |
| VERSIONED\_PARALLEL\_REPLICAS\_PROTOCOL | 54471 | ServerHello, Addendum | Les deux parties échangent un `VarUInt` indiquant la version du protocole de coordination des répliques parallèles. Le champ de ServerHello est placé **immédiatement après `protocol_version`** (avant `timezone`). Le champ d'Addendum est ajouté après les strings du protocole par blocs (chunked). Valeur actuelle : `8` (`DBMS_PARALLEL_REPLICAS_PROTOCOL_VERSION`). La version `8` ajoute [`MergeTreeAllRangesAnnouncementResponse`](#mergetreeallrangesannouncementresponse) (paquet client `14`) : lorsque la version négociée des répliques parallèles est `≥ 8`, l'initiateur répond à chaque annonce d'un suiveur en mode autre que `Default` avec la liste de référence des parts pour ce flux, et le suiveur l'attend avant d'émettre des requêtes de lecture. En dessous de `8`, l'annonce est envoyée sans attendre de réponse. |
| INTERSERVER\_EXTERNALLY\_GRANTED\_ROLES | 54472 | Query | Ajoute un champ `String external_roles` au corps de Query, entre le terminateur des paramètres et le hachage du secret interserveur. Les clients externes envoient une liste de rôles vide (un seul octet `0x00`, c.-à-d. VarUInt 0 dans une enveloppe String). |
| V2\_DYNAMIC\_AND\_JSON\_SERIALIZATION | 54473 | Corps de colonne | Le serveur peut émettre une sérialisation V2 pour les types de colonne `Dynamic` et `JSON` — ce qui détermine la version de `state_prefix` utilisée. Voir [types versionnés](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#versioned-types). |
| SERVER\_SETTINGS | 54474 | ServerHello | Le serveur diffuse ses paramètres modifiés par rapport aux valeurs par défaut sous forme de liste à la fin de ServerHello, après `nonce`. Format : triplets `(key, flags, value)` terminés par une clé vide — identique à la liste des paramètres du paquet Query. |
| QUERY\_AND\_LINE\_NUMBERS | 54475 | ClientInfo | Ajoute `script_query_number` (VarUInt) et `script_line_number` (VarUInt) à la fin de ClientInfo. Utilisé par clickhouse-client pour localiser les erreurs dans les scripts à plusieurs instructions ; les clients externes envoient `0, 0`. |
| JWT\_IN\_INTERSERVER | 54476 | ClientInfo | Ajoute un UInt8 de présence du JWT + un `String jwt` facultatif à la fin de ClientInfo. Les clients externes (sans JWT) envoient l'octet `0x00`. (Orthographié `DBMS_MIN_REVISON_WITH_JWT_IN_INTERSERVER` en C++ — notez la coquille dans le nom de la constante.) |
| QUERY\_PLAN\_SERIALIZATION | 54477 | ServerHello, paquet QueryPlan | ServerHello ajoute `VarUInt query_plan_serialization_version` après les paramètres du serveur. Introduit également `ClientPacket::QueryPlan` (code `13`) pour la transmission interserveur de plans de requête préconstruits — les clients externes ne l'envoient jamais. La version `10` de sérialisation du plan de requête préfixe le plan sérialisé par `VarUInt max_threads` et `Bool concurrency_control` ; lorsque l'un ou l'autre n'a pas sa valeur par défaut, les initiateurs transmettent le SQL aux pairs de version inférieure à `10`, afin que ceux-ci construisent leur plan à partir des paramètres de requête transmis. |
| PARALLEL\_BLOCK\_MARSHALLING | 54478 | Block (Column) | Le serveur peut encapsuler les colonnes dans `ColumnBLOB` (compressées en ligne) pour un traitement parallèle. Conditionné à l'activation de la compression pour la requête ET à `rows > 1` ; sinon, le format wire habituel des colonnes s'applique. Les clients qui n'activent jamais la compression sur les paquets Query sortants ne constatent aucun changement du format wire. |
| VERSIONED\_CLUSTER\_FUNCTION\_PROTOCOL | 54479 | ServerHello | Ajoute `VarUInt cluster_function_protocol_version` à la fin de ServerHello. Utilisé pour les fonctions de table `*Cluster` (`s3Cluster`, etc.). Valeur actuelle : `8` (`DBMS_CLUSTER_PROCESSING_PROTOCOL_VERSION`) ; la version `7` est réservée à une fonctionnalité d'un dépôt privé (compaction Iceberg), et la version `8` ajoute un `read_source_index` facultatif à la charge utile interserveur des tâches de lecture du cluster (le corps de `ReadTaskResponse`, qui n'est pas spécifié ici — voir ci-dessous). Les clients externes le décodent et l'ignorent. |
| OUT\_OF\_ORDER\_BUCKETS\_IN\_AGGREGATION | 54480 | BlockInfo | Ajoute le champ 3 (`out_of_order_buckets: Vec<Int32>`) au flux à champs étiquetés de BlockInfo. Décodé sous la forme `[VarUInt count][Int32]*count`. Les clients externes ne l'émettent pas eux-mêmes ; le décodeur lit toute liste non vide envoyée par le serveur. |
| COMPRESSED\_LOGS\_PROFILE\_EVENTS\_COLUMNS | 54481 | Log, ProfileEvents, TableColumns | Le serveur peut encapsuler les corps des paquets [`Log`](#log), [`ProfileEvents`](#profileevents) et [`TableColumns`](#tablecolumns) dans la [trame de compression](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#compression-frame). À partir de cette version, ces trois corps transitent par le même chemin de sortie éventuellement compressé, qui ne devient une véritable trame de compression que lorsque la requête a `compression = true`. Les clients qui n'activent jamais la compression sur les paquets Query sortants ne constatent aucun changement du format wire. |
| REPLICATED\_SERIALIZATION | 54482 | Block (Column) | Le serveur peut émettre des colonnes avec kind\_stack `0x04 = REPLICATED` — une forme compacte de type dictionnaire pour les valeurs répétées — voir [kind\_stack et encodage sparse](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#kind-stack-and-sparse-encoding). En dessous de cette version, l'émetteur développait ces colonnes avant l'envoi. Décodé par recherche d'index (`elements[indexes[i]]` par ligne) ; les types feuilles ainsi que les types internes `Nullable`/`Array`/`Tuple`/`Map`/`Nested`/`LowCardinality` sont pris en charge. |
| NULLABLE\_SPARSE\_SERIALIZATION | 54483 | Block (Column) | Combine la sérialisation sparse avec `Nullable(T)`. En dessous de cette version, l'émetteur développait le format sparse des colonnes Nullable avant l'envoi ; à partir de v54483, les données wire sont au format sparse sur Nullable. Voir [kind\_stack et encodage sparse](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#kind-stack-and-sparse-encoding). |
| PROGRESS\_IN\_ASYNC\_INSERT | 54484 | Progress (INSERT) | Lors d'un INSERT **asynchrone** (`async_insert = 1`), une fois l'insertion vidée, le serveur envoie un paquet [`Progress`](#progress) supplémentaire, puis les `ProfileEvents` de l'insertion, avant `EndOfStream`. Conditionné à une version *négociée* ≥ 54484 ; en dessous, le serveur omet ce Progress final. Le format wire de Progress est inchangé — seule son émission est nouvelle. En pratique, l'incrément contient le temps écoulé ; les compteurs de lignes écrites sont transmis via les ProfileEvents associés. Un client qui consomme déjà les Progress entrelacés n'a besoin d'aucun changement de format : il doit simplement tolérer un paquet de plus. |
| CLIENT\_AGENT\_IN\_CLIENT\_INFO | 54485 | ClientInfo | Ajoute un `String` `client_agent` final à ClientInfo. Le client de référence détecte automatiquement un identifiant d'agent à partir de son environnement (par exemple `claude-code`, `cursor`, `gemini-cli` ou la valeur de la variable `AGENT`) ; un client externe n'ayant rien détecté envoie une string vide. Obligatoire dès que la version négociée est ≥ 54485 — son omission désynchronise le reste du paquet Query. |
| INTERNAL\_QUERY\_FLAG | 54486 | ClientInfo | Ajoute un `UInt8` `is_internal` final à ClientInfo. `1` pour une requête interne au serveur (non émise par l'utilisateur), propagé aux requêtes distantes afin que leurs lignes dans `system.query_log` soient marquées comme internes ; les clients externes envoient `0`. Obligatoire dès que la version négociée est ≥ 54486 — son omission désynchronise le reste du paquet Query. |
| INTERSERVER\_CURRENT\_ROLES | 54488 | ClientInfo | Ajoute une liste de String facultative `current_roles` à la fin de ClientInfo (`[UInt8 present]` puis, si `1`, `[VarUInt count][String]*count`) et, pour les requêtes interserveur, étend `auth_hash` à la liste sérialisée. Transporte les noms des rôles actifs (activés) de l'initiateur afin qu'un nœud secondaire applique les politiques de lignes de la même manière, au lieu de se rabattre sur les rôles par défaut de l'utilisateur. Renseigné uniquement pour les requêtes interserveur et seulement lorsque `push_external_roles_in_interserver_queries = 1` ; les clients externes envoient `0` (absent). Obligatoire dès que la version négociée est ≥ 54488 — l'omission de l'octet de présence désynchronise le reste du paquet Query. |
| CURRENT\_AGGREGATION\_VARIANT\_SELECTION\_METHOD | 54489 | Agrégation (buckets à deux niveaux) | La méthode d'agrégation pour une clé `String` unique dépend de `enable_packed_string_keys_in_aggregation`. Un pair de révision inférieure utilise toujours la méthode compactée et ne connaît pas ce paramètre ; échanger des buckets à deux niveaux avec lui placerait donc la même clé dans des buckets différents. Face à un tel pair, l'initiateur adopte un comportement sûr par défaut : il met à zéro les seuils à deux niveaux pour celui-ci et répartit lui-même ses blocs à un niveau dans les buckets. La comparaison porte sur la révision du pair plutôt que sur sa version, car la méthode peut changer au sein d'une même release. Aucun changement du format wire du protocole natif. |
| HTTP\_HANDLER\_IN\_CLIENT\_INFO | 54490 | ClientInfo | Ajoute `http_handler_name` et `http_request_url` (tous deux `String`) à la branche **HTTP** de ClientInfo, immédiatement après `http_referer`. Ils transportent le nom du handler HTTP défini en SQL correspondant et l'URL de la requête, afin que `currentHandler`/`currentRequestURL` et les colonnes `http_handler_name`/`http_request_url` de `system.query_log` restent renseignés sur les shards distants pour les requêtes de handler distribuées. Écrits uniquement lorsque `query_interface = HTTP` (strings vides pour une requête HTTP sans handler) ; obligatoires dès que la version négociée est ≥ 54490 — leur omission désynchronise le reste du paquet Query. |
| QUANTILE\_DETERMINISTIC\_SKIP\_DEGREE | 54491 | Block (Column) | Fait passer la version d'état de `AggregateFunction(quantileDeterministic, ...)` (et de ses variantes `quantiles`/`median`) de `0` à `1`, ce qui ajoute un `UInt8 skip_degree` final à chaque état sérialisé. Seule la charge utile d'état de cette fonction d'agrégation change ; le cadrage des colonnes est inchangé et toutes les autres fonctions d'agrégation conservent leur propre version. En dessous de cette révision, l'émetteur produit des états de version `0`, de sorte qu'un pair plus ancien n'est pas affecté. |
| STRING\_WITH\_SIZE\_STREAM\_SERIALIZATION | 54492 | Block (Column) | Les colonnes `String` passent d'une disposition avec préfixe de longueur par valeur à un flux séparé d'offsets d'octets cumulés (la même disposition d'offsets qu'un `Array`), envoyé tel quel : `[UInt64 × num_rows offsets][data blob]`. Conditionné à la révision négociée ; en dessous de 54492, l'émetteur utilise les préfixes de longueur par valeur. Le format wire ne comporte aucun marqueur par colonne à cet effet — les deux pairs le déduisent uniquement de la révision. Voir [Disposition des colonnes String](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#string-type). |
| UNIQ\_STATE\_VERSION\_1 | 54493 | Block (Column) | Fait passer la version d'état de `AggregateFunction(uniq, ...)` de `0` à `1`. Un état de version `1` commence par un octet de flags, suivi de l'échantillon principal dans la disposition de la version `0`. Lorsque le bit `0` des flags est positionné, vient ensuite un petit échantillon supplémentaire de hachages 64 bits complets, chacun combiné par XOR avec la constante `0x9E3779B97F4A0000` (`UInt8 skip_degree`, `VarUInt count`, puis `count` valeurs de 8 octets) — limité à 16384 valeurs au maximum, il préserve la précision de l'estimation pour des cardinalités supérieures à deux milliards, là où l'échantillon principal 32 bits se dégrade et produisait auparavant des valeurs aberrantes par dépassement. Le bit `1` indique que l'échantillon 64 bits est incomplet (l'état a absorbé un état de version `0`, dont l'échantillon 64 bits n'a jamais vu les éléments) : aucun échantillon de ce type n'est alors écrit, et l'estimation repose uniquement sur l'échantillon principal 32 bits. Les deux bits sont mutuellement exclusifs et aucun autre bit n'est défini ; un lecteur doit rejeter un octet de flags qui positionne les deux bits ou tout autre bit. Seule la charge utile d'état de cette fonction d'agrégation change ; le cadrage des colonnes est inchangé et toutes les autres fonctions d'agrégation conservent leur propre version. Les combinateurs transparents appliqués à `uniq` (`uniqIf`, `uniqArray`, `uniqForEach`, `uniqMap`, `uniqArgMin` / `uniqArgMax`, `uniqOrNull` / `uniqOrDefault`, `uniqResample`, ainsi que l'adaptateur implicite pour les arguments `Nullable`) obéissent à la même condition et transmettent la version aux états `uniq` imbriqués dans leurs charges utiles. En dessous de cette révision, l'émetteur produit des états de version `0` (l'échantillon principal uniquement), de sorte qu'un pair plus ancien n'est pas affecté. |

## Enveloppe de paquet

Chaque message transmis dans le format binaire présente la même structure externe, dans les deux sens :

```text theme={null}
[VarUInt: packet_type_code]    always encoded as VarUInt
[message body]                 format depends on packet_type_code
```

Les tableaux complets des types de paquets figurent dans la [référence des types de paquets](#packet-type-reference).

Le type de paquet est un `VarUInt`, et non un octet de taille fixe. Pour les valeurs inférieures à 128, un `VarUInt` produit le même octet unique, mais les implémentations doivent utiliser l'encodage `VarUInt` afin de rester compatibles si de futurs types de paquets atteignent ou dépassent 128.

La [référence des messages](#message-reference) décrit uniquement le **corps** de chaque paquet, c'est-à-dire les octets qui suivent le code du type de paquet. La numérotation des champs commence à 1, avec le premier champ du corps.

### Tramage par fragments (v54470+)

Lorsque la fonctionnalité `CHUNKED_PROTOCOL` est **négociée** (voir [le handshake](#handshake-phase)), chaque paquet transmis est enveloppé dans un tramage par fragments. Cet enveloppement s'applique **par sens de communication** : les sens client→serveur et serveur→client sont négociés séparément et peuvent aboutir à des modes différents (par fragments ou sans tramage).

Disposition wire de chaque paquet :

```text theme={null}
<chunk>...   one or more chunks; their payloads concatenated form the whole packet
[u32 LE = 0] zero-size terminator marking end of packet
```

Disposition wire par fragment :

```text theme={null}
[u32 LE: chunk_size]   chunk_size in [1, UINT32_MAX]
[chunk_size bytes]     packet bytes (see note below)
```

Le `VarUInt` du type de paquet se trouve **à l'intérieur** du flux fragmenté : il constitue le premier octet de la charge utile du paquet (le premier octet du premier fragment), et non un octet distinct envoyé avant le tramage. La charge utile des fragments de chaque paquet correspond à l'intégralité de `[VarUInt packet_type_code][message body]` issu de l'[enveloppe de paquet](#packet-envelope). Si un client laisse le type de paquet en dehors du flux fragmenté, le pair lit cet octet de type comme le premier octet de la taille de fragment `u32`, ce qui désynchronise la connexion.

Un même paquet peut être réparti sur plusieurs fragments si le tampon du rédacteur se remplit en cours de paquet ; la coupure peut intervenir n'importe où, y compris à l'intérieur du `VarUInt` du type de paquet. Le lecteur concatène les charges utiles des fragments et traite le zéro final de 4 octets comme une frontière de paquet transparente : il le consomme, mais ne le transmet pas au composant qui lit les corps de paquets.

Les paquets sans corps sont malgré tout encapsulés : un paquet d'un seul octet tel que `Ping` ou `Pong` devient `[u32 size = 1][0x04][u32 0]` une fois la fragmentation négociée. Toute mention d'« un seul octet dans le format binaire » ailleurs sur cette page désigne la forme antérieure à la fragmentation.

**Négociation.** ServerHello et Addendum transportent chacun deux champs `String`, un par direction, dont les valeurs sont choisies parmi `{"chunked", "notchunked", "chunked_optional", "notchunked_optional"}` :

* `chunked` / `notchunked` sont stricts : le côté concerné exige exactement ce mode.
* Les variantes `_optional` sont souples : elles acceptent le mode choisi par l'autre côté.

La valeur convenue pour chaque direction est déterminée par paire :

| Préférence du serveur | Préférence du client | Valeur convenue |
| - | - | - |
| `*_optional` | quelconque | suivre le CLIENT (son `starts_with("chunked")`) |
| quelconque | `*_optional` | suivre le SERVEUR |
| `chunked` strict | `chunked` strict | `chunked` |
| `notchunked` strict | `notchunked` strict | `notchunked` |
| incompatibilité stricte | incompatibilité stricte | **erreur de protocole** — la connexion DOIT être fermée |

Côté client, la préférence d'ENVOI du client est négociée avec la préférence de RÉCEPTION du serveur, et inversement.

**Chronologie.** Les chaînes de négociation transitent sans tramage : ClientHello → ServerHello (préférences du serveur) → Addendum (valeurs négociées par le client). Le passage au tramage s'applique à chaque octet envoyé *après* le vidage de l'Addendum. L'Addendum lui-même, le ClientHello et le ServerHello sont toujours transmis sans tramage.

## Cycle de vie de la connexion

À tout instant, une connexion se trouve dans un seul des quatre états suivants : `HANDSHAKE`, `READY`, `READING_RESPONSE` ou terminée. Le protocole ne gérant pas le multiplexage, un client qui envoie une nouvelle requête avant d'avoir entièrement lu la réponse précédente entremêle les octets transmis et corrompt le flux.

### États

```mermaid theme={null}
stateDiagram-v2
    [*] --> HANDSHAKE: TCP connect

    HANDSHAKE --> READY: handshake ok
    READY --> READING_RESPONSE: send Query
    READING_RESPONSE --> READY: EndOfStream / Exception
    READY --> READY: Ping / Pong

    HANDSHAKE --> Terminated: handshake error
    READING_RESPONSE --> Terminated: protocol violation / I/O error
    READY --> Terminated: close
    Terminated --> [*]
```

Le chemin nominal descend en ligne droite — `HANDSHAKE → READY → READING_RESPONSE → READY` — avec la boucle `Ping`/`Pong` sur elle-même, tandis que toutes les transitions d'échec convergent vers l'unique état final `Terminated`.

| État | Description |
| - | - |
| `HANDSHAKE` | État initial après l'ouverture de la connexion TCP. Seuls les messages de [handshake](#handshake-phase) sont valides. Passe à `READY` en cas de succès ; la connexion est interrompue en cas d'échec. |
| `READY` | Inactif. Le client peut envoyer un [Ping](#ping-phase), une [Query](#query-phase), ou fermer la connexion. La connexion peut rester indéfiniment dans l'état `READY` (sous réserve de `idle_connection_timeout`, voir [limites de connexion](#connection-limits)). Un paquet `Data` (2) ou `Scalar` (7) reçu dans cet état constitue une violation de protocole : le serveur met fin à la connexion sans décoder le corps du paquet, et sans envoyer au préalable de paquet `Exception`. Le corps n'étant pas lu, le client peut constater une réinitialisation TCP (reset) plutôt qu'une fermeture propre. |
| `READING_RESPONSE` | État atteint lorsque le client envoie une Query. Le client doit consommer entièrement le flux de réponse du serveur avant de revenir à `READY`. Le seul paquet client→serveur autorisé dans cet état est Cancel (non décrit sur cette page). |
| Terminated | La connexion n'est plus utilisable. Le client doit ouvrir une nouvelle connexion TCP et recommencer le handshake. |

### Phase de handshake

Authentifie le client et négocie la version du protocole. Cette phase a lieu une seule fois par connexion, avant toute autre opération.

La connexion TCP vient d'être ouverte et aucun message n'a encore été échangé. Le déroulement est le suivant :

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: ClientHello
    alt success
        S->>C: ServerHello
    else error
        S->>C: Exception — connection terminates
    end
    Note over C,S: negotiated_version = min(client, server)
    opt negotiated_version ≥ 54458
        C->>S: Addendum
    end
```

1. Le client envoie [`ClientHello`](#clienthello) avec la version maximale du protocole qu'il prend en charge.

2. Le client lit la réponse et la traite selon le type de paquet :

   | Type de paquet | Action |
   | - | - |
   | `Hello` (0) | Décoder [`ServerHello`](#serverhello). Calculer `negotiated_version = min(client_ver, server_ver)`. Passer à l'étape 3. |
   | `Exception` (2) | Décoder [`Exception`](#exception). La renvoyer comme erreur et mettre fin à la connexion. |
   | tout autre type | Violation du protocole. Mettre fin à la connexion. |

3. Si `negotiated_version ≥ 54458` (fonctionnalité `ADDENDUM`), le client envoie un [`Addendum`](#addendum). Cette décision repose sur la version **négociée**, et non sur la version déclarée par le client.

En cas de succès, la connexion passe à l'état `READY` ; en cas d'erreur, elle est interrompue.

### Phase Ping

Vérification de disponibilité au niveau applicatif, indépendante du keepalive TCP. Un aller-retour Ping/Pong réussi confirme que la connexion TCP est active dans les deux sens et que le serveur répond. Le Ping est sans état et n'est lié à aucune requête : plusieurs Pings successifs sont donc indépendants les uns des autres.

À partir de l'état `READY`, le déroulement est le suivant :

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: Ping (0x04)
    alt responsive
        S->>C: Pong (0x04)
    else error
        S->>C: Exception
    end
```

1. Le client envoie [`Ping`](#ping).
2. Le client lit la réponse :

   | Type de paquet | Action |
   | - | - |
   | `Pong` (4) | Connexion active confirmée. Retour à l'état `READY`. |
   | `Exception` (2) | Décoder le paquet [`Exception`](#exception) et le renvoyer comme erreur. |
   | tout autre type | Violation de protocole. |

### Phase de requête

Le client soumet un statement SQL ; le serveur renvoie en continu des blocs de résultats ainsi que la télémétrie d'exécution. La réponse est une séquence de paquets se terminant par exactement un `EndOfStream` ou un `Exception`.

À partir de l'état `READY`, le déroulement est le suivant :

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: Query message
    C->>S: External-table Data packets (0 or more)
    C->>S: Empty Data marker — end-of-client-data (required)
    S->>C: Data — header block (N cols, 0 rows)
    loop until EndOfStream or Exception
        S->>C: Progress / Log (interleaved)
        S->>C: Data — result block (N cols, M rows)
        S->>C: Totals / Extremes (aggregation queries)
        S->>C: ProfileInfo / ProfileEvents (profiling)
    end
    S->>C: Data — empty block (boundary marker)
    S->>C: Progress — final updates
    S->>C: EndOfStream — authoritative end of query
```

Si une erreur survient à n'importe quel moment, le serveur envoie un paquet `Exception` au lieu de `EndOfStream`, ce qui met fin à la requête.

1. Le client envoie [`Query`](#query) avec un `query_id` unique (généralement un UUID).
2. Le client envoie les éventuelles tables externes, puis le marqueur Data vide. Le paquet Data vide a `table_name = ""`, `num_columns = 0`, `num_rows = 0`. Le serveur ne commence à exécuter la requête qu'après avoir reçu ce marqueur. Une table externe peut être répartie sur plusieurs paquets `Data` portant le même `table_name` ; le premier d'entre eux fixe le schéma de la table, et chaque paquet ultérieur pour ce nom doit contenir les mêmes noms et types de colonnes, dans le même ordre, faute de quoi le serveur rejette la requête avec `INCORRECT_DATA` (voir [Data](#data)).
3. Le client passe à l'état `READING_RESPONSE` et vide son tampon d'écriture.
4. Le client lit les paquets de réponse en boucle et les traite selon leur type :

   | Type de paquet | Action |
   | - | - |
   | `Data` (1) | Décoder le bloc. Le premier Data est l'en-tête de schéma ; les suivants sont des blocs de résultat (à accumuler) ; un bloc vide est un marqueur de limite. `num_rows == 0` ne signifie **pas** la fin de la requête. |
   | `Progress` (3) | Métriques d'exécution. Chaque paquet est un **incrément** par rapport au précédent — à cumuler localement. |
   | `EndOfStream` (5) | Requête terminée. Sortir de la boucle et revenir à l'état `READY`. |
   | `ProfileInfo` (6) | Données de profilage post-exécution. |
   | `Totals` (7) | Bloc des totaux d'agrégation (même format wire que Data). |
   | `Extremes` (8) | Bloc des valeurs min/max (même format wire que Data). |
   | `Log` (10) | Ligne de journal du serveur. |
   | `TableColumns` (11) | Métadonnées des valeurs par défaut des colonnes. |
   | `ProfileEvents` (14) | Compteurs de performance. |
   | `Exception` (2) | Décoder et renvoyer sous forme d'erreur. Sortir de la boucle et revenir à l'état `READY`. |
   | tout autre type | Inattendu pendant la phase de requête. Mettre fin à la connexion. |

À la réception de `EndOfStream` ou d'un `Exception` traité, la connexion revient à l'état `READY`. Une violation de protocole ou une erreur d'E/S y met fin.

> [!NOTE]
>
> Le cas `num_rows == 0` piège souvent les nouvelles implémentations. Un bloc sans ligne est un marqueur de limite ou un en-tête de schéma, et non un signal de fin de flux. Seuls `EndOfStream` ou `Exception` mettent fin à la réponse.

### Phase INSERT

La phase INSERT correspond à la [phase de requête](#query-phase), avec deux échanges supplémentaires. Le client soumet un statement `INSERT`, puis le serveur répond par un **bloc de schéma** décrivant la table cible. Le client envoie ensuite en flux des paquets Data contenant les lignes, suivis du marqueur Data vide. Enfin, le serveur termine par `EndOfStream` ou `Exception`.

À partir de l'état `READY`, la requête SQL est un `INSERT` de la forme `INSERT INTO <table> [(<cols>)] VALUES`, sans littéral `VALUES (...)` intégré, puisque les données des lignes transitent par les paquets Data. Le déroulement est le suivant :

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: Query packet (INSERT body)
    C->>S: External-table Data packets (0 or more)
    Note over C,S: No empty Data marker here —<br/>it would end the row stream before it starts
    opt metadata before schema
        S->>C: TableColumns / Progress / ...
    end
    S->>C: Data packet — schema block (columns, 0 rows)
    Note over C,S: Schema block is the contract:<br/>rows sent next must match these column shapes
    loop one or more blocks
        C->>S: Data packet (rows N)
    end
    C->>S: Data packet — empty block (0 rows), end-of-input terminator
    loop until EndOfStream or Exception
        S->>C: Progress / ProfileInfo / Log / ProfileEvents
    end
    opt async_insert = 1 and negotiated_version ≥ 54484
        S->>C: trailing Progress, then insert ProfileEvents
    end
    S->>C: EndOfStream
```

1. Le client envoie [`Query`](#query) avec `body` défini sur la requête SQL INSERT.
2. Le client envoie les éventuelles tables externes (rare pour un INSERT). Contrairement à la [phase de requête](#query-phase), il n'envoie **pas** de marqueur Data vide à ce stade. Le paquet `Query` de l'`INSERT` est envoyé avec des données en attente : le bloc vide de fin de données est donc reporté à l'étape 5. S'il était envoyé avant le bloc de schéma, le serveur l'interpréterait comme la fin du flux de lignes, terminerait l'INSERT sans aucune ligne, puis analyserait le premier véritable paquet de lignes comme un paquet de premier niveau orphelin.
3. Le client consomme les paquets de métadonnées (TableColumns, Progress, ProfileInfo, Log, ProfileEvents) jusqu'à recevoir le paquet Data de schéma — un bloc avec 0 ligne mais une structure de colonnes complète (noms et types). Le bloc de schéma fait office de contrat : les lignes que le client envoie ensuite doivent respecter la forme de ces colonnes.
4. Le client envoie le ou les blocs de données. Pour chaque bloc, il écrit `VarUInt(ClientPacket::Data = 2)`, puis `String("")` pour le nom vide de table externe, puis le bloc. Les types de colonnes doivent correspondre, position par position, aux colonnes du bloc de schéma.
5. Le client envoie le marqueur de fin d'entrée : un paquet Data contenant un bloc vide (0 colonne, 0 ligne).
6. Le client consomme le flux de réponse jusqu'à `EndOfStream` (succès) ou `Exception` (échec).

**INSERT asynchrone (v54484+).** Lorsque la requête comporte `async_insert = 1`, le serveur place les lignes en file d'attente et les vide par lots. À partir de la version négociée ≥ 54484 (`PROGRESS_IN_ASYNC_INSERT`), une fois le vidage terminé, le serveur émet un paquet [`Progress`](#progress) supplémentaire, immédiatement suivi des `ProfileEvents` de l'insertion, puis de `EndOfStream`. En dessous de 54484, le serveur n'envoie pas ce Progress final. Il s'agit d'un paquet `Progress` ordinaire ; comme le serveur réinitialise le pipeline de requête avant d'y intégrer les compteurs d'écriture, l'incrément ne contient en pratique que le temps écoulé, et les statistiques de lignes et d'octets écrits parviennent au client via les `ProfileEvents` qui l'accompagnent. Un client qui consomme déjà les Progress entrelacés à l'étape 6 n'a qu'à accepter un paquet supplémentaire.

La connexion revient à l'état `READY` à la réception de `EndOfStream` ou d'une `Exception` gérée. Les violations de protocole et les erreurs d'E/S y mettent fin.

## Référence des messages

Les champs sont présentés dans l'ordre de transmission. La colonne `Type` utilise les valeurs suivantes :

* `VarUInt` — entier non signé de longueur variable (voir [VarUInt](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#varuint)).
* `String` — octets préfixés par un VarUInt (voir [String](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#string)).
* `UInt8`, `Int32`, etc. — entiers little-endian de largeur fixe.
* `Bool` — un seul octet, `0x00` ou `0x01`.

La colonne `Role` indique qui utilise chaque champ :

* **client** — défini par les clients externes.
* **interserveur** — pertinent uniquement pour la communication entre serveurs ; les clients externes y écrivent une valeur par défaut.
* **universal** — utilisé par les deux.

Ces tableaux ne décrivent que le corps de chaque paquet, situé après le code du type de paquet.

### ClientHello (type de paquet 0)

Client → Serveur. Premier message envoyé après l'ouverture de la connexion TCP.

| # | Champ | Type | Rôle | Description |
| - | - | - | - | - |
| 1 | client\_name | String | universal | Identifiant du client (par exemple, `"clickhouse-client"`) |
| 2 | version\_major | VarUInt | universal | Version majeure du client |
| 3 | version\_minor | VarUInt | universal | Version mineure du client |
| 4 | protocol\_version | VarUInt | universal | Version maximale du protocole prise en charge par le client |
| 5 | database | String | universal | Nom de la base de données par défaut |
| 6 | user | String | universal | Nom d'utilisateur pour l'authentification. Si la valeur est vide, l'utilisateur de session par défaut du serveur est utilisé (paramètre serveur `default_session_user` ; rejeté par les serveurs antérieurs à la version 26.8). Voir [Authentification](#authentication). |
| 7 | password | String | universal | Mot de passe (en clair) |

### ServerHello (type de paquet 0)

Serveur → Client. Réponse à ClientHello lorsque l'authentification réussit.

| # | Champ | Type | Rôle | Condition | Description |
| - | - | - | - | - | - |
| 1 | server\_name | String | universal | always | Identifiant du serveur |
| 2 | version\_major | VarUInt | universal | always | Version majeure du serveur |
| 3 | version\_minor | VarUInt | universal | always | Version mineure du serveur |
| 4 | protocol\_version | VarUInt | universal | always | Version du protocole du serveur |
| 4a | parallel\_replicas\_protocol\_version | VarUInt | universal | VERSIONED\_PARALLEL\_REPLICAS\_PROTOCOL (v54471) | Version du protocole de coordination des répliques parallèles du serveur. **Position dans le format binaire : immédiatement après `protocol_version`**, avant `timezone`. Valeur actuelle : `8`. |
| 5 | timezone | String | universal | TIMEZONE (v54058) | Fuseau horaire du serveur (p. ex. `"UTC"`) |
| 6 | display\_name | String | universal | DISPLAY\_NAME (v54372) | Nom du serveur lisible par un humain |
| 7 | version\_patch | VarUInt | universal | VERSION\_PATCH (v54401) | Version de correctif du serveur |
| 8 | proto\_send\_chunked\_srv | String | universal | CHUNKED\_PROTOCOL (v54470) | Mode de fragmentation sortante préféré par le serveur. L'une des valeurs `"chunked"`, `"notchunked"`, `"chunked_optional"`, `"notchunked_optional"`. Voir [tramage fragmenté](#chunked-framing). **Se place AVANT `password_complexity_rules` dans le format binaire, bien que sa version de condition soit plus élevée.** |
| 9 | proto\_recv\_chunked\_srv | String | universal | CHUNKED\_PROTOCOL (v54470) | Mode de fragmentation entrante préféré par le serveur. Mêmes valeurs possibles que pour le champ 8. |
| 10 | password\_complexity\_rules | Rule\[] | universal | PASSWORD\_COMPLEXITY\_RULES (v54461) | Politique de mots de passe du serveur. `VarUInt count` suivi de `count × Rule`. Voir ci-dessous. |
| 11 | nonce | UInt64 | interserveur | INTERSERVER\_SECRET\_V2 (v54462) | Nonce aléatoire de 8 octets en LE. Utilisé par le serveur pour son mécanisme de signature des requêtes interserveur. Les clients externes DOIVENT le décoder (afin de maintenir l'alignement du flux) et DEVRAIENT ignorer sa valeur. |
| 12 | server\_settings | Setting\[] | universal | SERVER\_SETTINGS (v54474) | Diffusion des paramètres du serveur dont la valeur diffère de celle par défaut. Format : zéro ou plusieurs triplets `(String key, VarUInt flags, String value)`, terminés par une clé vide. Identique à la [liste des paramètres du paquet Query](#setting). |
| 13 | query\_plan\_serialization\_version | VarUInt | universal | QUERY\_PLAN\_SERIALIZATION (v54477) | Version de sérialisation des plans de requête prise en charge par le serveur. Les clients externes la décodent puis l'ignorent. |
| 14 | cluster\_function\_protocol\_version | VarUInt | universal | VERSIONED\_CLUSTER\_FUNCTION\_PROTOCOL (v54479) | Version du protocole des fonctions de table `*Cluster` du serveur. Valeur actuelle : `8`. Cette valeur conditionne la présence de champs supplémentaires dans la charge utile interserveur des tâches de lecture de cluster (le corps `ReadTaskResponse`, non spécifié par ailleurs) ; la version `7` est réservée à une fonctionnalité d'un dépôt privé (Iceberg compaction), et la version `8` ajoute un champ optionnel `read_source_index`. Les clients externes ne participent pas aux lectures de cluster : ils décodent ce champ puis l'ignorent. |

**Rule** — élément de `password_complexity_rules` :

| # | Champ | Type | Description |
| - | - | - | - |
| 1 | pattern | String | Motif d'expression régulière auquel un mot de passe conforme doit correspondre. |
| 2 | message | String | Explication lisible par un humain, affichée lorsqu'un mot de passe enfreint cette règle. |

Cette liste reflète la politique de mots de passe configurée par l'opérateur du serveur et n'a qu'une valeur indicative : le serveur n'applique pas ces règles pendant la poignée de main. Un client qui permet de modifier ou de définir un mot de passe peut s'appuyer sur ces règles pour signaler les erreurs avant d'envoyer au serveur un mot de passe non conforme.

> [!NOTE]
>
> Pour limiter la consommation de ressources face à un serveur malveillant ou mal configuré, plafonnez la valeur décodée de `count` à 256 entrées, et chaque String `pattern` et `message` à 4096 octets. Un `count` égal à `0` (aucune paire ne suit) est le cas habituel pour les serveurs sur lesquels aucune politique de mots de passe n'est configurée.

### Addendum (sans type de paquet)

Client → Serveur, conditionné par `ADDENDUM` (v54458). Envoyé immédiatement après la fin de la poignée de main. Il ne s'agit pas d'un type de paquet distinct : les champs sont transmis bruts dans le format binaire, sans octet de type de paquet en préfixe.

| # | Champ | Type | Rôle | Condition | Description |
| - | - | - | - | - | - |
| 1 | quota\_key | String | universal | toujours | Clé de quota de ressources pour les quotas à clé côté serveur. Les clients qui n'utilisent pas de quota à clé envoient une chaîne vide. |
| 2 | proto\_send\_chunked | String | universal | CHUNKED\_PROTOCOL (v54470) | Fragmentation sortante négociée par le client : `"chunked"` ou `"notchunked"`. Déterminée en fonction de `proto_recv_chunked_srv` reçu dans ServerHello. |
| 3 | proto\_recv\_chunked | String | universal | CHUNKED\_PROTOCOL (v54470) | Fragmentation entrante négociée par le client. Déterminée en fonction de `proto_send_chunked_srv`. |
| 4 | parallel\_replicas\_protocol\_version | VarUInt | universal | VERSIONED\_PARALLEL\_REPLICAS\_PROTOCOL (v54471) | Version du protocole de coordination des répliques parallèles prise en charge par le client. Les clients externes qui ne participent pas aux requêtes distribuées DEVRAIENT tout de même envoyer une version valide (actuellement `8`) pour que la vérification de compatibilité côté serveur réussisse. |

Le passage au tramage fragmenté n'intervient qu'*après* le vidage de cet Addendum ; l'Addendum lui-même n'est pas tramé.

### Ping (type de paquet 4)

Client → Serveur. Aucun corps — avant le tramage fragmenté, le paquet se compose d'un seul octet `0x04` ; lorsque la fragmentation est négociée, cet octet devient la charge utile, d'un seul octet, d'un fragment (voir [tramage fragmenté](#chunked-framing)).

### Pong (type de paquet 4)

Serveur → Client. Aucun corps — le paquet se résume à un seul octet `0x04` avant le tramage fragmenté ; lorsque la fragmentation est négociée, cet octet constitue la charge utile, longue d'un octet, d'un fragment (voir [tramage fragmenté](#chunked-framing)).

### Exception (type de paquet 2)

Serveur → Client. Envoyé lorsque le serveur rencontre une erreur, quelle que soit la phase.

| # | Champ | Type | Rôle | Description |
| - | - | - | - | - |
| 1 | code | Int32 | universal | Code d'erreur |
| 2 | name | String | universal | Classe de l'exception (par ex. `"DB::Exception"`) |
| 3 | message | String | universal | Message d'erreur lisible par un humain |
| 4 | stack\_trace | String | universal | Trace de pile côté serveur |
| 5 | has\_nested (obsolète) | Bool | universal | Octet de compatibilité obsolète. Toujours écrit avec la valeur `false` par le serveur |

### Query (type de paquet 1)

Client → Serveur.

| # | Champ | Type | Rôle | Condition | Description |
| - | - | - | - | - | - |
| 1 | query\_id | String | universel | toujours | Identifiant unique de la requête (UUID) |
| 2 | client\_info | ClientInfo | universel | CLIENT\_INFO (v54032) | Voir [ClientInfo](#clientinfo) |
| 3 | settings | Setting\[] | universel | toujours | Voir [Setting](#setting). **Toujours présent** (terminé par une clé vide) ; seul l'*encodage* de chaque paramètre dépend de la version — voir la note sur l'encodage dans [Setting](#setting). Un client ne doit pas omettre ce champ lorsque la version négociée est inférieure à `54429`. |
| 3a | external\_roles | String | universel | INTERSERVER\_EXTERNALLY\_GRANTED\_ROLES (v54472) | Liste sérialisée des noms de rôles accordés de manière externe. Liste vide = octet `0x00` (VarUInt 0) encapsulé dans une enveloppe String (`[VarUInt 1][0x00]` dans le format binaire). Les clients externes envoient toujours une liste vide. |
| 4 | auth\_hash | String | interserveur | INTERSERVER\_SECRET (v54441) | Hash d'authentification interserveur — **et non** le secret de cluster brut. Voir [Authentification inter-serveur](#inter-server-authentication) ci-dessous. Les clients externes (ainsi que toute `InitialQuery`) envoient une chaîne vide. |
| 5 | stage | VarUInt | universel | toujours | Étape de traitement de la requête. `0` = FetchColumns, `1` = WithMergeableState, `2` = Complete, `3` = WithMergeableStateAfterAggregation, `4` = WithMergeableStateAfterAggregationAndLimit, `7` = QueryPlan. Les valeurs `3`/`4` apparaissent dans les requêtes distribuées ; `7` accompagne un plan de requête sérialisé. Les clients externes envoient normalement `2`. |
| 6 | compression | VarUInt | universel | toujours | 0 = désactivée, 1 = activée |
| 7 | query\_body | String | universel | toujours | Texte SQL |
| 8 | parameters | Parameter\[] | client | PARAMETERS (v54459) | Voir [Parameter](#parameter). Terminé par une clé vide. |

### ClientInfo (intégré dans Query)

Client → Serveur, intégré dans le corps de Query (champ 2). Conditionné par `CLIENT_INFO` (v54032). (Certains champs de ClientInfo dépendent de versions ultérieures, comme indiqué ci-dessous pour chaque champ.)

| # | Champ | Type | Rôle | Condition | Description |
| - | - | - | - | - | - |
| 1 | query\_kind | UInt8 | universal | toujours | 0 = NoQuery, 1 = InitialQuery, 2 = SecondaryQuery. Les clients externes envoient `1`. |
| 2 | initial\_user | String | universal | toujours | Utilisateur ayant lancé la requête |
| 3 | initial\_query\_id | String | universal | toujours | Identifiant de la requête d’origine |
| 4 | initial\_address | String | universel | toujours | Adresse du socket du client d’origine. Le serveur ne résout jamais cette valeur (aucune résolution de nom d’hôte ni de nom de service). Pour une `SECONDARY_QUERY` (où la valeur est conservée et utilisée, par exemple dans `system.query_log` et pour l’authentification interserveur), la syntaxe acceptée est IPv4 `a.b.c.d:port` ou IPv6 entre crochets `[addr]:port`, l’hôte devant être une adresse IP littérale et le port un nombre décimal compris dans `0..65535` ; les autres formes (par exemple `localhost:9000`, `host:http`, `:9000` ou un path de socket UNIX tel que `/tmp/ch.sock`) sont rejetées avec `INCORRECT_DATA`. Pour une `INITIAL_QUERY`, le serveur remplace ce champ par l’adresse réelle du pair, si bien que toute valeur est acceptée (une valeur qui n’est pas un simple `ip:port` est remplacée par la valeur par défaut `0.0.0.0:0`). Les clients externes doivent envoyer leur propre `ip:port`. |
| 5 | initial\_time | Int64 | client | INITIAL\_QUERY\_START\_TIME (v54449) | Horodatage de début de la requête (en microsecondes). Encodage de taille fixe sur 8 octets, et non en VarUInt |
| 6 | query\_interface | UInt8 | universel | toujours | 1 = TCP, 2 = HTTP |
| 7 | os\_user | String | client | si interface = TCP | Nom d’utilisateur de l’OS |
| 8 | client\_hostname | String | client | si interface = TCP | Nom d’hôte de la machine cliente |
| 9 | client\_name | String | client | si interface = TCP | Nom de l’application cliente |
| 10 | version\_major | VarUInt | universal | si interface = TCP | Version majeure du client |
| 11 | version\_minor | VarUInt | universal | si interface = TCP | Version mineure du client |
| 12 | protocol\_version | VarUInt | universel | si interface = TCP | La version du protocole TCP propre au client d'origine (`DBMS_TCP_PROTOCOL_VERSION`), et **non** la version négociée. La révision du pair détermine uniquement les champs présents ; cette valeur correspond à la version intégrée à la compilation de l'initiateur. Ainsi, lorsqu'un client plus récent communique avec un serveur plus ancien, elle peut être supérieure à la révision négociée ou à celle du serveur. |
| 13 | quota\_key | String | universal | QUOTA\_KEY\_IN\_CLIENT\_INFO (v54060) | Clé de quota de ressources pour les quotas par clé côté serveur. Les clients qui n’utilisent pas de quota par clé envoient une string vide. |
| 14 | distributed\_depth | VarUInt | interserveur | DISTRIBUTED\_DEPTH (v54448) | Profondeur d'imbrication des requêtes distribuées. Les clients externes envoient `0`. |
| 15 | version\_patch | VarUInt | universal | VERSION\_PATCH (v54401), TCP uniquement | Version corrective du client |
| 16 | open\_telemetry | (ci-dessous) | client | OPEN\_TELEMETRY (v54442) | Contexte de trace. Les clients qui n'utilisent pas le traçage envoient `0`. |
| 17 | collaborate\_with\_initiator | VarUInt | interserveur | PARALLEL\_REPLICAS (v54453) | Bool encodé en VarUInt. Les clients externes envoient `0`. |
| 18 | count\_participating\_replicas | VarUInt | interserveur | PARALLEL\_REPLICAS (v54453) | Les clients externes envoient `0`. |
| 19 | number\_of\_current\_replica | VarUInt | interserveur | PARALLEL\_REPLICAS (v54453) | Les clients externes envoient `0`. |
| 20 | script\_query\_number | VarUInt | client | QUERY\_AND\_LINE\_NUMBERS (v54475) | Position du statement (indexée à partir de 1) dans un script comportant plusieurs statements. Les clients externes envoient `0`. |
| 21 | script\_line\_number | VarUInt | client | QUERY\_AND\_LINE\_NUMBERS (v54475) | Numéro de ligne (à partir de 1) dans le script source. Les clients externes envoient `0`. |
| 22 | jwt\_present | UInt8 | interserveur | JWT\_IN\_INTERSERVER (v54476) | `0` = pas de JWT ; `1` = un JWT suit. Les clients externes n'utilisant pas l'authentification JWT envoient `0`. |
| 23 | jwt | String | interserveur | JWT\_IN\_INTERSERVER (v54476), si jwt\_present=1 | Jeton JWT de type bearer, présent uniquement lorsque le champ 22 = `1`. |
| 24 | client\_agent | String | client | CLIENT\_AGENT\_IN\_CLIENT\_INFO (v54485) | Champ final. Identifiant de l'outil ou de l'agent client, détecté automatiquement à partir de l'environnement (par exemple `claude-code`, `cursor`, `gemini-cli` ou la variable d'environnement `AGENT`). Les clients externes pour lesquels aucun agent n'est détecté envoient une chaîne vide. Présent dans le chemin Query ordinaire dès que la version négociée est ≥ 54485 (envoyé sur toutes les interfaces, et pas seulement en TCP). |
| 25 | is\_internal | UInt8 | client | INTERNAL\_QUERY\_FLAG (v54486) | Champ final. `1` pour une requête interne au serveur (non émise par l'utilisateur), transmis aux requêtes distantes afin de les signaler comme internes dans `system.query_log` ; indépendant de `query_kind` (champ 1). Les clients externes envoient `0`. Présent dès que la version négociée est ≥ 54486 (envoyé sur toutes les interfaces, pas seulement TCP). |
| 26 | current\_roles | UInt8 \[+ liste de String] | interserveur | INTERSERVER\_CURRENT\_ROLES (v54488) | Champ final. `[UInt8 present]`, puis, lorsque `present = 1`, une liste `[VarUInt count][String]*count` contenant les noms des rôles actifs (activés) de l'initiateur. Permet à un nœud secondaire de restreindre les politiques de ligne aux rôles de l'initiateur plutôt qu'aux rôles par défaut de l'utilisateur. Renseigné uniquement pour les requêtes interserveur, et seulement lorsque `push_external_roles_in_interserver_queries = 1` ; dans les autres cas, ainsi que pour les clients externes, `present = 0`. L'octet de présence est obligatoire dès que la version négociée est ≥ 54488 (envoyé sur toutes les interfaces, pas seulement TCP). Pour les requêtes interserveur, la liste sérialisée est également intégrée à `auth_hash` (voir [Authentification inter-serveur](#inter-server-authentication)). |

> [!NOTE]
>
> **Disposition dépendante de l'interface (champs 7–12)**
>
> Les champs 7–12 ci-dessus correspondent à la branche **TCP**. Lorsque `query_interface` (champ 6) n'est **pas** TCP, ces champs sont *remplacés* par une disposition wire différente — il ne s'agit pas de simples omissions facultatives : un décodeur doit donc bifurquer en fonction du champ 6.
>
> * `query_interface = 2` (**HTTP**) : ce sont les informations de la requête HTTP transmise par le serveur qui sont écrites à la place — `http_method` (`UInt8`), `http_user_agent` (`String`), puis `forwarded_for` (`String`, conditionné par `X_FORWARDED_FOR_IN_CLIENT_INFO` v54443), `http_referer` (`String`, conditionné par `REFERER_IN_CLIENT_INFO` v54447), et enfin `http_handler_name` (`String`) et `http_request_url` (`String`) (tous deux conditionnés par `HTTP_HANDLER_IN_CLIENT_INFO` v54490, dans cet ordre). Aucun des champs `os_user`/`client_hostname`/`client_name`/`version_*`/`protocol_version` n'est présent. Les deux derniers contiennent le nom du gestionnaire HTTP défini en SQL qui a été sélectionné ainsi que l'URL de la requête, afin qu'ils soient propagés jusqu'aux partitionnés distants (ils sont vides pour une requête HTTP ne passant pas par un gestionnaire).
> * Toute autre interface : aucun des champs TCP (7–12) ni aucun des champs HTTP n'est écrit ; le flux se poursuit directement avec `quota_key`.
>
> À l'issue de cette branche, la disposition redevient commune : `quota_key` (champ 13) et `distributed_depth` (champ 14) suivent pour toutes les interfaces, puis `version_patch` (champ 15) n'est écrit que pour TCP.
>
> Cette branche concerne principalement le trafic interserveur, lorsque le serveur initiateur transmet une requête arrivée à l'origine via HTTP. Un décodeur qui lit systématiquement les champs TCP interprétera mal ces paquets, en prenant `http_method` ou `http_user_agent` pour `quota_key`.

Encodage OpenTelemetry (champ 16) :

```text theme={null}
[UInt8: has_trace]              0 = no trace data follows, 1 = trace data follows
If has_trace == 1:
  [16 bytes: trace_id]          byte-swapped per-8-bytes
  [8 bytes:  span_id]           byte-swapped
  [String:   trace_state]       W3C trace state
  [UInt8:    trace_flags]       W3C trace flags
```

### Authentification inter-serveur

Le champ 4 du paquet Query (`auth_hash`) ne contient **pas**, dans le format binaire, le secret de cluster partagé. Envoyer le secret brut ferait échouer l'authentification tout en le divulguant. Un serveur agissant en tant que client interserveur prouve plutôt qu'il connaît le secret au moyen d'un hash SHA-256 salé :

1. **Passer en mode interserveur.** Le serveur qui se connecte le signale dans `ClientHello` : le champ `user` contient le marqueur interserveur et `password` est vide. Il ajoute ensuite deux autres chaînes de caractères — le nom du cluster et un `salt` de 32 octets nouvellement généré (`encodeSHA256` d'une valeur aléatoire) — immédiatement après les champs `user`/`password`, au sein du même paquet `ClientHello`. Le serveur lit ces deux chaînes **avant** d'envoyer `ServerHello` ; un client doit donc les écrire d'emblée. Attendre d'abord `ServerHello` provoque un interblocage, car le serveur reste bloqué dans l'attente de ces chaînes.
2. **Obtenir le nonce.** `ServerHello` transporte un nonce `UInt64` de 8 octets lorsque `INTERSERVER_SECRET_V2` (v54462) est négocié.
3. **Calculer le hash.** Pour chaque paquet Query qui n'est pas une `InitialQuery`, le client écrit `encodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles + current_roles)` dans le champ 4, soit un digest de 32 octets. (`nonce` est représenté sous forme de chaîne décimale, présente uniquement si la version négociée est ≥ v54462 ; `external_roles` n'est ajouté que lorsque `INTERSERVER_EXTERNALLY_GRANTED_ROLES` (v54472) est négocié ; `current_roles` est la liste sérialisée des noms de rôles — `[VarUInt count][String]*count`, sans l'octet de présence — ajoutée uniquement lorsque `INTERSERVER_CURRENT_ROLES` (v54488) est négocié et que la liste est présente.) Pour une `InitialQuery`, ou lorsqu'aucun secret de cluster n'est configuré, le client écrit à la place une chaîne vide.
4. **Vérifier.** Le serveur lit le champ 4 avec une limite de 32 octets et recalcule la même concaténation à partir de sa propre copie du secret de cluster ; la connexion est rejetée si les digests diffèrent.

Les clients externes (non interserveur) n'entrent jamais dans ce mode et envoient toujours un `auth_hash` vide.

### Setting

Encodé directement dans la liste de paramètres du corps de Query (le paquet [Query](#query), champ 3). La liste est **toujours présente**, quelle que soit la version négociée, et se termine par un Setting dont la clé est vide — un unique `VarUInt 0`, sans indicateurs ni valeur à la suite. Seul l'encodage de chaque paramètre dépend de la version négociée, selon `SETTINGS_SERIALIZED_AS_STRINGS` (v54429).

**v54429+ (`STRINGS_WITH_FLAGS`)** — chaque paramètre est représenté par le triplet suivant :

| # | Champ | Type | Rôle | Description |
| - | - | - | - | - |
| 1 | key | String | universal | Nom du paramètre. Vide = fin de la liste. |
| 2 | flags | VarUInt | universal | Indicateurs binaires de métadonnées ; voir ci-dessous. |
| 3 | value | String | universal | Valeur du paramètre sous forme de string |

Les champs 2 et 3 sont absents lorsque `key` est vide.

**Avant 54429 (`BINARY`)** — chaque paramètre est de la forme `[String key][type-specific binary value]` : le champ `flags` n'est **pas** écrit, et la valeur est encodée sous la forme binaire native du paramètre (par exemple un integer de largeur fixe ou une string préfixée par sa longueur) plutôt que sous forme de string décimale ou textuelle. La liste se termine toujours par une `key` vide. Un client ciblant une version négociée inférieure à `54429` doit lire et écrire cette forme binaire, et non le triplet ci-dessus. (Les paramètres personnalisés définis par l'utilisateur font exception : ils comportent toujours `flags` et une valeur de type string, quel que soit l'encodage.)

Le champ `flags` regroupe :

* `0x01` — **Important** : le paramètre affecte le résultat de la requête et ne doit pas être ignoré silencieusement par les pairs plus anciens.
* `0x02` — **Custom** : un paramètre personnalisé défini par l'utilisateur.
* `0x0c` — un **champ de niveau sur 2 bits**, et non un indicateur indépendant : `0x00` = Production, `0x04` = Obsolete, `0x08` = Experimental, `0x0c` = Beta. Lisez les 2 bits (`flags & 0x0c`) — un test naïf `flags & 0x04` classerait à tort Beta (`0x0c`) comme Obsolete.
* `0x80` — **HotReload** (rechargement de la configuration sans redémarrage ; défini dans l'enum des indicateurs, présent principalement pour les paramètres de coordination).

### Paramètre

Paramètres de requête, pour les requêtes paramétrées telles que `SELECT {x:UInt64}`. Ils sont encodés de la même manière qu'un [Setting](#setting) dont l'indicateur `Custom` (`0x02`) est activé, et se terminent de la même façon par une clé vide.

| # | Champ | Type | Rôle | Description |
| - | - | - | - | - |
| 1 | key | String | client | Nom du paramètre. Vide = fin de la liste. |
| 2 | flags | VarUInt | client | Toujours `0x02` (Custom) |
| 3 | value | String | client | Valeur du paramètre sous forme de string. Voir la note ci-dessous concernant les guillemets. |

> [!NOTE]
>
> La valeur du paramètre correspond à la représentation SQL de la valeur, et non à un littéral brut. Les paramètres de type String doivent être transmis déjà entourés de guillemets simples (par exemple, la valeur de `{name:String}` est `'Alice'`, et non `Alice`) ; sinon, le parseur de valeurs du serveur les rejette.

### Data (type de paquet 1 serveur→client, type de paquet 2 client→serveur)

Dans les deux sens. Transporte les blocs de résultats, les données d'INSERT, les tables externes et les marqueurs de fin de données.

Le format wire est symétrique : dans les deux sens, un préfixe `table_name` précède le bloc. Seul l'octet du type de paquet diffère.

```text theme={null}
[VarUInt: packet_type]     1 (server→client) or 2 (client→server)
[String:  table_name]      External table name; empty in most cases
[Block]                    See the Native Format spec for the Block layout
```

| Champ | Type | Rôle | Description |
| - | - | - | - |
| table\_name | String | universal | Nom de la table externe. La valeur vide (`""`) est le cas le plus courant : table principale, résultat de la requête et flux de lignes d'INSERT. Un `table_name` vide ne constitue **pas** à lui seul le marqueur de fin de données (les paquets de lignes d'INSERT ordinaires portent également `""`). Un même nom non vide peut apparaître dans plusieurs paquets `Data` envoyés par le client au cours d'une même requête : le premier paquet portant ce nom crée la table externe et fixe son schéma (un paquet comportant des colonnes mais avec `num_rows = 0` suffit), et chaque paquet suivant portant ce nom doit déclarer les mêmes noms et types de colonnes, dans le même ordre. Le serveur compare l'en-tête du bloc au schéma enregistré et renvoie une `Exception` (`INCORRECT_DATA`) en cas de divergence. |
| Corps du bloc | — | — | Voir [Structure des blocs et des colonnes](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#block-and-column-structure). |

Le **marqueur de fin de données** est un paquet dont le bloc est vide (`0` colonne et `0` ligne), quelle que soit la valeur de `table_name`. Le serveur ne considère un paquet `Data` client comme marqueur de fin que si le bloc décodé est vide (`block.empty()`) ; un paquet avec `table_name = ""` et un bloc non vide est un paquet de lignes ordinaire, et non un marqueur de fin. Un flux de lignes d'INSERT se compose donc d'une suite de blocs `Data` non vides, clôturée par un unique bloc `Data` vide.

Les variantes de blocs et leur signification sont décrites dans [Variantes de blocs](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#block-variants).

### Progress (type de paquet 3)

Serveur → Client. Envoyé périodiquement pendant l'exécution de la requête. Tous les champs sont de type VarUInt, et chaque paquet contient des **incréments depuis le paquet `Progress` précédent**, et non des totaux cumulés. Avant l'envoi, le serveur lit ses compteurs, les remet atomiquement à zéro, puis calcule `elapsed_ns` comme le temps écoulé depuis le dernier envoi. Un client **doit donc cumuler** localement les paquets successifs pour obtenir des totaux courants : traiter un paquet comme une valeur absolue fait reculer l'affichage de la progression ou fausse le décompte à la baisse dès que plus d'un paquet est reçu.

| # | Champ | Type | Rôle | Condition | Description |
| - | - | - | - | - | - |
| 1 | rows | VarUInt | universal | toujours | Lignes lues depuis le paquet précédent (à ajouter au total courant) |
| 2 | bytes | VarUInt | universal | toujours | Octets lus depuis le paquet précédent (à ajouter au total courant) |
| 3 | total\_rows | VarUInt | universal | toujours | Incrément du nombre total estimé de lignes à lire ; à cumuler (peut valoir 0 dans un paquet donné) |
| 4 | total\_bytes | VarUInt | universal | TOTAL\_BYTES\_IN\_PROGRESS (v54463) | Incrément du nombre total estimé d'octets à lire ; à cumuler. Se situe ENTRE `total_rows` et `wrote_rows` dans le format binaire. |
| 5 | wrote\_rows | VarUInt | universal | WRITE\_CLIENT\_INFO (v54420) | Lignes écrites depuis le paquet précédent (pour INSERT) ; à cumuler |
| 6 | wrote\_bytes | VarUInt | universal | WRITE\_CLIENT\_INFO (v54420) | Octets écrits depuis le paquet précédent (pour INSERT) ; à cumuler |
| 7 | elapsed\_ns | VarUInt | universal | SERVER\_QUERY\_TIME\_IN\_PROGRESS (v54460) | Nanosecondes écoulées depuis le paquet précédent (un delta, et non la durée totale de la requête) ; à cumuler |

### ProfileInfo (type de paquet 6)

Serveur → Client. Envoyé une seule fois par requête, vers la fin de l'exécution.

| # | Champ | Type | Rôle | Condition | Description |
| - | - | - | - | - | - |
| 1 | rows | VarUInt | universal | toujours | Nombre total de lignes traitées |
| 2 | blocks | VarUInt | universal | toujours | Nombre total de blocs traités |
| 3 | bytes | VarUInt | universal | toujours | Nombre total d'octets traités |
| 4 | applied\_limit | Bool | universal | toujours | Indique si une clause LIMIT a été appliquée |
| 5 | rows\_before\_limit | VarUInt | universal | toujours | Nombre de lignes avant LIMIT |
| 6 | *obsolete* | Bool | universal | toujours | Octet de compatibilité obsolète. Le serveur y écrit toujours `true` et le client l'ignore à la lecture ; il ne s'agit **pas** d'un indicateur signalant que `rows_before_limit` a été calculé. L'état pertinent de la limite est fourni par le champ 4 (`applied_limit`) combiné au champ 5. À lire, puis à ignorer. |
| 7 | applied\_aggregation | Bool | universal | ROWS\_BEFORE\_AGGREGATION (v54469) | Indique si GROUP BY a été appliqué |
| 8 | rows\_before\_aggregation | VarUInt | universal | ROWS\_BEFORE\_AGGREGATION (v54469) | Nombre de lignes avant l'agrégation |

### Totals (type de paquet 7)

Serveur → Client. Envoyé pour les requêtes contenant `WITH TOTALS`. Le format wire est identique à celui de [Data](#data) : une string `table_name` (toujours vide) suivie d'un bloc. Seul l'octet de type de paquet diffère.

```text theme={null}
[VarUInt: 7]                packet type
[String:  table_name]       always empty
[Block]                     see the Native Format spec
```

### Extremes (type de paquet 8)

Serveur → Client. Envoyé lorsque le paramètre `extremes` est activé. Le format wire est identique à celui de [Data](#data). Le bloc contient exactement 2 lignes : la ligne 0 contient le minimum de chaque colonne et la ligne 1, le maximum.

```text theme={null}
[VarUInt: 8]                packet type
[String:  table_name]       always empty
[Block]                     num_rows = 2
```

### Log (type de paquet 10)

Serveur → Client. Envoyé lorsque la requête dispose d’une file d’attente de logs active (paramètre `send_logs_level` ; voir [streaming des logs](#log-streaming)).

Même enveloppe et même format de corps que [Data](#data). Le bloc a une valeur fixe `num_columns = 8` et un schéma prédéfini. Chaque ligne de log correspond à une ligne sur l’ensemble des 8 colonnes, et un même paquet Log peut contenir de nombreuses lignes.

```text theme={null}
[VarUInt: 10]               packet type
[String:  table_name]       always empty
[Block]                     num_columns = 8, num_rows = number of log lines
```

Les 8 colonnes, dans cet ordre précis :

| # | Nom | Type | Description |
| - | - | - | - |
| 1 | event\_time | DateTime | Timestamp de l’événement (secondes depuis l’epoch) |
| 2 | event\_time\_microseconds | UInt32 | Composant microsecondes |
| 3 | host\_name | String | Nom d’hôte du serveur qui émet le journal |
| 4 | query\_id | String | Identifiant de la requête associée au journal |
| 5 | thread\_id | UInt64 | Identifiant du thread de l’OS |
| 6 | priority | Int8 | Niveau de journalisation (priorité Poco : 1 = Fatal, … 8 = Trace, 9 = Test) |
| 7 | source | String | Nom du logger |
| 8 | text | String | Texte du message de journal |

### ProfileEvents (type de paquet 14)

Serveur → Client. Transmet les compteurs de performance de chaque requête.

Enveloppe et format de corps identiques à ceux de [Data](#data). Le bloc comporte un nombre fixe de colonnes (`num_columns = 6`) et un schéma prédéfini. Chaque événement correspond à une ligne.

```text theme={null}
[VarUInt: 14]               packet type
[String:  table_name]       always empty
[Block]                     num_columns = 6, num_rows = number of events
```

Les 6 colonnes :

| # | Nom | Type | Description |
| - | - | - | - |
| 1 | host\_name | String | Nom d’hôte du serveur |
| 2 | current\_time | DateTime | Timestamp de l’événement |
| 3 | thread\_id | UInt64 | ID du thread |
| 4 | type | Enum8 | Type d’événement : 1 = Increment (compteur), 2 = Gauge. Il est stocké sur un octet signé. |
| 5 | name | String | Nom de l’événement (par ex. `"Query"`, `"NetworkReceiveBytes"`) |
| 6 | value | Int64 | Valeur du compteur ou de la jauge |

> [!NOTE]
>
> Le type des éléments de la colonne `value` varie d'un paquet à l'autre : les serveurs plus anciens émettent du `UInt64`, les plus récents du `Int64`. Lisez la string de type de la colonne dans l'en-tête du bloc plutôt que de présupposer une largeur donnée.

### TableColumns (type de paquet 11)

Serveur → Client, conditionné par `COLUMN_DEFAULTS_METADATA` (v54410). Le serveur l'envoie avant le bloc de schéma de l'INSERT afin de transmettre les métadonnées des valeurs par défaut des colonnes, mais uniquement lorsque la version négociée est ≥ 54410 **et** que le paramètre `input_format_defaults_for_omitted_fields` est activé. En dessous de 54410, ce paquet n'est jamais envoyé ; un client plus ancien ne doit donc **pas** l'attendre, car le bloc `Data` de schéma arrive directement. Un client v54410+ doit être capable de gérer les deux cas : un `TableColumns` facultatif, suivi du bloc de schéma.

| # | Champ | Type | Rôle | Description |
| - | - | - | - | - |
| 1 | external\_table | String | universal | Nom de la table externe. Vide = table principale. |
| 2 | columns\_description | String | universal | Définitions textuelles des colonnes, par exemple `"id Int32, name String DEFAULT ''"`. Texte libre, à analyser comme une string. |

> [!NOTE]
>
> **Corps compressé à partir de v54481**
>
> À partir de la version négociée 54481 (`COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS`), le serveur écrit **les deux** champs via le même chemin de sortie, éventuellement compressé. Ainsi, lorsque la requête a `compression = true`, l'intégralité du corps de `TableColumns` (`external_table` + `columns_description`) se trouve dans la [trame de compression](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#compression-frame), et le client le lit via le flux décompressé correspondant. Lorsque la requête n'utilise pas de compression, le corps est transmis non compressé dans le format binaire, exactement comme l'indique le tableau ci-dessus. Ce point est important pour les réponses de schéma d'`INSERT` : un client qui adapte la gestion de la compression pour `Log` et `ProfileEvents`, mais pas pour `TableColumns`, interprétera mal la réponse lorsque la compression de la requête est activée.

### TimezoneUpdate (type de paquet 17)

Serveur → Client, conditionné par `TIMEZONE_UPDATES` (v54464). Envoyé à un seul endroit : l'initialiseur de la fonction de table `input` (une requête de la forme `INSERT INTO <table> SELECT ... FROM input('<structure>')`, qui reçoit en flux des lignes envoyées par le client). Juste après avoir envoyé le bloc `Data` contenant le schéma d'entrée (voir la [phase INSERT](#insert-phase)), le serveur émet `TimezoneUpdate` avec la valeur actuelle de `session_timezone` dans le contexte de la requête, afin que le client analyse les lignes qu'il s'apprête à envoyer selon le même fuseau horaire. Le serveur n'émet **pas** ce paquet lors de modifications arbitraires via `SET session_timezone` en cours de requête, ni pour indiquer au client comment formater les blocs de résultats ultérieurs.

| # | Champ | Type | Rôle | Description |
| - | - | - | - | - |
| 1 | timezone | String | universal | Le nouveau fuseau horaire par défaut de la session (par exemple, `"UTC"`, `"Europe/Berlin"`). |

Le paquet arrive une seule fois, immédiatement après le bloc de schéma d'entrée et avant que le client ne commence à envoyer les blocs de lignes. Un décodeur qui ignore `TimezoneUpdate` DOIT tout de même consommer la `String` qui suit afin de préserver l'alignement du wire.

### Authentification SSH par challenge-réponse (types de paquet 11, 12, 18)

Conditionnée par `SSH_AUTHENTICATION` (v54466), et uniquement sur activation explicite. Une connexion entre dans le flux SSH lorsque ClientHello envoie `user = " SSH KEY AUTHENTICATION " + <real_user>` (avec les espaces de début et de fin) et `password = ""`. Le serveur lit le préfixe, le supprime pour retrouver l'utilisateur réel et bascule en mode challenge-réponse.

| Paquet | Code | Direction | Corps |
| - | - | - | - |
| SSHChallengeRequest | 11 | Client → Serveur | (aucun corps) |
| SSHChallenge | 18 | Serveur → Client | `String challenge` — octets aléatoires ; l'un des composants de la string à signer (voir ci-dessous) |
| SSHChallengeResponse | 12 | Client → Serveur | `String signature` — signature SSH calculée sur la concaténation définie ci-dessous, **et non** sur le challenge brut |

Ce flux remplace l'authentification par mot de passe, et l'échange challenge-réponse a lieu **avant** ServerHello : le serveur diffère sa réponse Hello jusqu'à ce que l'authentification réussisse.

1. Le client envoie ClientHello avec le préfixe marqueur SSH et un mot de passe vide.

2. Le client envoie `SSHChallengeRequest` (paquet 11). Le serveur n'a **pas encore** envoyé ServerHello : il traite d'abord l'authentification et reste bloqué à cette étape en attendant ce paquet.

3. Le serveur répond par un `SSHChallenge` contenant des octets aléatoires (paquet 18).

4. Le client construit la string à signer et signe **celle-ci**, et non le challenge brut, puis envoie `SSHChallengeResponse` (paquet 12) avec la signature. Le message signé est la concaténation octet par octet, sans séparateur, de quatre parties dans cet ordre exact :

   ```text theme={null}
   to_sign = decimal(protocol_version) + default_database + user + challenge
   ```

   | Partie | Source |
   | - | - |
   | `decimal(protocol_version)` | La version de protocole du client sous forme de **string ASCII décimale** (par ex. `"54466"`) — le numéro de version sous forme de string, et non un VarUInt ou un entier à largeur fixe. Pour la validation, le serveur utilise la même version de protocole que celle reçue dans `ClientHello`. |
   | `default_database` | Le champ `database` de `ClientHello` (string vide s'il est absent). |
   | `user` | Le nom d'utilisateur réel **sans le préfixe marqueur `" SSH KEY AUTHENTICATION "`** — le même nom que celui que le serveur retrouve après suppression du préfixe. |
   | `challenge` | Les octets bruts de `challenge` issus du paquet `SSHChallenge`. |

5. Le serveur vérifie la signature à l'aide de la clé publique enregistrée pour l'utilisateur, en reconstruisant la même string `decimal(protocol_version) + default_database + user + challenge`. En cas de succès, il envoie `ServerHello` (la même réponse que dans le flux par mot de passe) et le handshake se poursuit normalement (Addendum, etc.) ; en cas d'échec, il renvoie une `Exception` et ferme la connexion. L'authentification échouera pour tout client qui ne signe que les octets bruts du challenge.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: ClientHello (SSH marker user, empty password)
    C->>S: SSHChallengeRequest (11)
    Note over C,S: Server has NOT sent ServerHello yet —<br/>it authenticates first and blocks here
    S->>C: SSHChallenge (18) — random bytes to sign
    C->>S: SSHChallengeResponse (12) — signature over version+db+user+challenge
    alt signature verifies
        S->>C: ServerHello — handshake continues normally
    else verification fails
        S->>C: Exception — connection terminates
    end
```

> [!NOTE]
>
> Il s'agit de l'inverse du handshake par mot de passe, dans lequel ServerHello suit immédiatement ClientHello. Avec l'authentification SSH auth, ServerHello n'est envoyé qu'une fois la signature vérifiée ; le challenge-réponse SSH s'intercale donc dans le handshake avant la réception de tout ServerHello.

Les clients externes qui n'utilisent pas SSH auth ne voient jamais les paquets 11, 12 ou 18 : ceux-ci ne transitent jamais sur le wire, sauf si l'utilisateur active explicitement ce mode via le préfixe du nom d'utilisateur.

### MergeTreeAllRangesAnnouncementResponse (type de paquet 14)

Client → Serveur, interserveur uniquement. Conditionné par `parallel_replicas_protocol_version ≥ 8` (voir [VERSIONED\_PARALLEL\_REPLICAS\_PROTOCOL](#feature-table)). Les clients externes n'envoient jamais ce paquet.

Lorsque la version de parallel-replicas négociée est `≥ 8`, le cycle requête/réponse de l'initiateur pour un [`MergeTreeAllRangesAnnouncement`](#packet-type-reference) envoyé par un follower (type de paquet `15`, sens serveur→client) est modifié comme suit :

1. Un follower ouvre son pipeline de lecture et envoie `MergeTreeAllRangesAnnouncement` à l'initiateur.
2. **Uniquement lorsque le `mode` de l'annonce est différent de `Default`** (`WithOrder = 1` ou `ReverseOrder = 2`, tous deux utilisés pour les lectures parallèles ordonnées), l'initiateur répond avec `MergeTreeAllRangesAnnouncementResponse`. Pour `mode = Default = 0`, l'initiateur ne répond pas et le follower n'attend pas : en mode `Default`, les plages sont distribuées avec chaque `MergeTreeReadTaskRequest`, si bien que la liste des parties de données n'est jamais nécessaire en amont.
3. Le follower attend la réponse de manière bloquante (lorsqu'elle est attendue) avant d'émettre son premier [`MergeTreeReadTaskRequest`](#packet-type-reference) (paquet serveur `16`, envoyé follower→initiateur ; l'initiateur répond avec `MergeTreeReadTaskResponse`, paquet client `10`), et s'appuie sur la liste des parties de données renvoyée pour restreindre la construction des sources aux seules parties de données dont son flux `#split_i` est propriétaire.

En dessous de la version `8`, l'annonce fonctionne en mode fire-and-forget quel que soit le mode, et le follower construit des sources sur toutes les parties de données connues localement (comportement legacy).

#### Corps

| # | Champ | Type | Description |
| - | - | - | - |
| 1 | version | Int64 (little-endian) | Version du protocole parallel-replicas de l'émetteur. Égale à `DBMS_PARALLEL_REPLICAS_PROTOCOL_VERSION` (actuellement `8`) lorsque la révision TCP du destinataire est `≥ DBMS_MIN_REVISION_WITH_VERSIONED_PARALLEL_REPLICAS_PROTOCOL` (`54471`) ; sinon, se rabat sur `DBMS_MIN_SUPPORTED_PARALLEL_REPLICAS_PROTOCOL_VERSION` (`3`). Le récepteur rejette toute valeur inférieure à `DBMS_MIN_SUPPORTED_PARALLEL_REPLICAS_PROTOCOL_VERSION`. |
| 2 | parts | [RangesInDataPartsDescription](#rangesindatapartsdescription) | Ensemble de référence des parties de données que le coordinateur a enregistrées pour le flux visé par l'annonce. Une liste vide signifie que ce flux n'existe pas sur le coordinateur (par exemple, lorsque le follower a annoncé plus de splits que l'initiateur n'en a créé) ; le pool du follower associé à ce flux se marque alors immédiatement comme terminé. |
| 3 | stream\_id | String | Reprend le `stream_id` de l'annonce à laquelle répond ce message (nom de la table suivi du suffixe `#split_i` lorsqu'une topologie de split est en place). |

#### Corps de RangesInDataPartsDescription

| # | Champ | Type | Description |
| - | - | - | - |
| 1 | count | VarUInt | Nombre de descripteurs de partie de données qui suivent. Le décodeur rejette comme mal formées les valeurs supérieures à `100'000'000'000`. |
| 2 | parts | [RangesInDataPartDescription](#rangesindatapartdescription) répété `count` fois | Les descripteurs, dans l'ordre dans lequel le coordinateur les a enregistrés. |

#### Corps RangesInDataPartDescription

| # | Champ | Type | Gate | Description |
| - | - | - | - | - |
| 1 | info | [MergeTreePartInfo](#mergetreepartinfo) | universal | Identité de la partie de données (partition, plage de blocs, niveau, mutation). |
| 2 | ranges | [MarkRanges](#markranges) | universal | Plages de marques au sein de `info` que ce flux peut traiter. Une liste vide signifie que la partie de données est enregistrée, mais qu'aucun travail ne lui est actuellement assigné. |
| 3 | rows | VarUInt | universal | Nombre total de lignes couvertes par `ranges`. |
| 4 | projection\_name | String | `DBMS_PARALLEL_REPLICAS_MIN_VERSION_WITH_PROJECTION` (PR v5) | Vide pour les lignes de la partie de données principale ; sinon, nom de la projection. |
| 5 | min\_marks\_per\_task | VarUInt | `DBMS_PARALLEL_REPLICAS_MIN_VERSION_WITH_MIN_MARKS_PER_TASK` (PR v6) | Nombre minimal de marques que le pool du follower doit regrouper dans une même tâche de lecture pour cette partie de données. |

#### Corps de MergeTreePartInfo

| # | Champ | Type | Description |
| - | - | - | - |
| 1 | version | Int64 (little-endian) | Toujours `DBMS_MERGE_TREE_PART_INFO_VERSION` (`1`). Le décodeur rejette toute autre valeur. |
| 2 | partition\_id | String | Identifiant de partition (par ex. `"all"` pour les tables non partitionnées, ou la valeur sous forme de chaîne de l'expression tuple de la clé de partitionnement). |
| 3 | min\_block | Int64 (little-endian) | Premier numéro de bloc de la plage de blocs de la partie de données. |
| 4 | max\_block | Int64 (little-endian) | Dernier numéro de bloc de la plage de blocs de la partie de données (inclus). |
| 5 | level | UInt32 (little-endian) | Niveau de fusion. |
| 6 | mutation | Int64 (little-endian) | Version de la mutation ayant produit cette partie de données (`0` si elle n'a subi aucune mutation). |
| 7 | use\_legacy\_max\_level | Bool (text) | Encodé sur un seul octet ASCII (`'1'` ou `'0'`) — indicateur de compatibilité historique pour le format du nom de la partie de données. |

#### Corps MarkRanges

| # | Champ | Type | Description |
| - | - | - | - |
| 1 | size | UInt64 (little-endian) | Nombre de paires de plages de marques qui suivent. Remarque : entier little-endian à largeur fixe, **et non** VarUInt. |
| 2 | ranges | `size` répétitions de `(UInt64 begin, UInt64 end)`, chacun en little-endian | Intervalles de marques semi-ouverts `[begin, end)`. |

## Référence des types de paquets

### Client → Serveur

| Code | Nom | Format du corps | Description |
| - | - | - | - |
| 0 | Hello | [ClientHello](#clienthello) | Initiation du handshake |
| 1 | Query | [Query](#query) | Demande d'exécution d'une requête |
| 2 | Data | [Data](#data) | Bloc de données (données d'INSERT, tables externes, marqueur de fin de données) |
| 3 | Cancel | (pas de corps) | Annulation de la requête en cours |
| 4 | Ping | [Ping](#ping) | Vérification de disponibilité (liveness) |
| 5 | TablesStatusRequest | non spécifié | Vérification du statut des tables |
| 6 | KeepAlive | non spécifié | Maintien de la connexion (keepalive) |
| 7 | Scalar | non spécifié | Bloc de données scalaires |
| 8 | IgnoredPartUUIDs | non spécifié | Parties de données à exclure de la requête |
| 9 | ReadTaskResponse | non spécifié | Réponse de lecture pour un cluster S3 |
| 10 | MergeTreeReadTaskResponse | non spécifié | Réponse à une tâche de lecture parallèle |
| 11 | SSHChallengeRequest | [Authentification SSH](#ssh-authentication) | Demande de challenge d'authentification SSH |
| 12 | SSHChallengeResponse | [Authentification SSH](#ssh-authentication) | Réponse au challenge d'authentification SSH |
| 13 | QueryPlan | [QueryPlan](#queryplan) | Plan de requête |
| 14 | MergeTreeAllRangesAnnouncementResponse | [MergeTreeAllRangesAnnouncementResponse](#mergetreeallrangesannouncementresponse) | Réponse de l'initiateur à un [`MergeTreeAllRangesAnnouncement`](#packet-type-reference) envoyé par un follower (disponible uniquement si `parallel_replicas_protocol_version ≥ 8` — voir [VERSIONED\_PARALLEL\_REPLICAS\_PROTOCOL](#feature-table)). Interserveur uniquement — jamais envoyé par les clients externes. |

### Serveur → Client

| Code | Nom | Format du corps | Description |
| - | - | - | - |
| 0 | Hello | [ServerHello](#serverhello) | Réponse au handshake |
| 1 | Data | [Data](#data) | Bloc de données de résultat |
| 2 | Exception | [Exception](#exception) | Erreur |
| 3 | Progress | [Progress](#progress) | Progression de l'exécution de la requête |
| 4 | Pong | [Pong](#pong) | Réponse de vérification de disponibilité (liveness) |
| 5 | EndOfStream | (pas de corps) | Requête terminée |
| 6 | ProfileInfo | [ProfileInfo](#profileinfo) | Données de profilage après exécution |
| 7 | Totals | [Totals](#totals) | Ligne GROUP BY WITH TOTALS |
| 8 | Extremes | [Extremes](#extremes) | Valeurs min/max (bloc de 2 lignes) |
| 9 | TablesStatusResponse | non spécifié | Réponse sur le statut des tables |
| 10 | Log | [Log](#log) | Lignes de log de l'exécution de la requête |
| 11 | TableColumns | [TableColumns](#tablecolumns) | Descriptions des colonnes pour les valeurs par défaut |
| 12 | PartUUIDs | non spécifié | Identifiants uniques des parties de données |
| 13 | ReadTaskRequest | non spécifié | Demande de tâche de lecture du cluster |
| 14 | ProfileEvents | [ProfileEvents](#profileevents) | Compteurs de performance |
| 15 | MergeTreeAllRangesAnnouncement | non spécifié | Initialisation de la lecture parallèle |
| 16 | MergeTreeReadTaskRequest | non spécifié | Attribution des tâches de lecture parallèle |
| 17 | TimezoneUpdate | [TimezoneUpdate](#timezoneupdate) | Mise à jour du fuseau horaire du serveur |
| 18 | SSHChallenge | [Authentification SSH](#ssh-authentication) | Challenge d'authentification SSH |

### QueryPlan

`QueryPlan` est un paquet client réservé aux communications interserveurs. Il suit un paquet [`Query`](#query) dont le `stage` vaut `QueryPlan` (`7`) et contient un unique flux de plan sérialisé.

| # | Champ | Type | Condition | Description |
| - | - | - | - | - |
| 1 | serialization\_version | VarUInt | universal | Version de sérialisation du plan de requête, inférieure ou égale à la valeur `query_plan_serialization_version` annoncée par le destinataire dans `ServerHello`. |
| 2 | max\_threads | VarUInt | version de sérialisation ≥ `10` | Limite de threads au niveau du plan. `0` signifie qu'aucune limite propre au plan n'est appliquée. |
| 3 | concurrency\_control | Bool | version de sérialisation ≥ `10` | Indique si le plan participe au contrôle de la concurrence. |
| 4 | plan | flux de plan de requête versionné | universal | Arbre du plan sérialisé, paramètres des étapes, en-têtes et sets préparés. Sa disposition interne est régie par la version de sérialisation du plan de requête. |

Avant la version `10`, les champs 2 et 3 sont absents. Un initiateur dont l'une des limites d'exécution diffère de la valeur par défaut ne doit pas envoyer cette ancienne disposition : il transmet à la place la requête SQL d'origine, ce qui permet à l'ancien destinataire de reconstituer les limites à partir des paramètres de la requête.

## Configuration

Cette section présente les paramètres ajustables qui régissent les connexions du protocole natif :

* [Paramètres de la couche transport](#transport-layer-settings) — options de socket TCP et délais d'expiration, qui agissent sur le comportement de la connexion TCP elle-même.
* [Paramètres de la couche applicative](#application-layer-settings) — paramètres ajustables propres à chaque requête, transmis dans la [liste des paramètres du paquet Query](#setting), qui déterminent ce que le serveur envoie dans le format binaire ou la manière dont les données sont découpées en trames.
* [Paramètres hors périmètre](#settings-out-of-scope) — paramètres souvent confondus avec les paramètres du protocole, mais qui contrôlent en réalité l'exécution SQL ou le stockage.

Les valeurs par défaut indiquées ci-dessous correspondent à une version récente du serveur ; elles peuvent varier selon les versions et les déploiements.

### Paramètres de la couche transport

#### Options de socket

| Option | Valeur par défaut | Côté | Description |
| - | - | - | - |
| `TCP_NODELAY` | activé | les deux | Algorithme de Nagle désactivé. Les petits paquets sont envoyés immédiatement. |
| `SO_KEEPALIVE` | activé (client), valeur par défaut de l'OS (serveur) | asymétrique | Sondes TCP keepalive au niveau du noyau. Le client active explicitement cette option lorsque `tcp_keep_alive_timeout > 0`. Le serveur hérite de la valeur par défaut de l'OS. |
| `SO_RCVBUF` / `SO_SNDBUF` | valeurs par défaut de l'OS | — | Tailles des tampons de socket. Non ajustées par le protocole. |

#### Délais d'expiration

| Paramètre | Valeur par défaut | Unité | Côté | Description |
| - | - | - | - | - |
| `connect_timeout` | 10 | secondes | client | Délai d'expiration pour l'établissement de la connexion TCP initiale. |
| `handshake_timeout_ms` | 10000 | millisecondes | client | Délai d'expiration pour la réception de ServerHello pendant le handshake. |
| `send_timeout` | 300 | secondes | les deux | Si aucun octet ne peut être écrit dans cet intervalle, la connexion lève une exception. |
| `receive_timeout` | 300 | secondes | les deux | Si aucun octet ne peut être lu dans cet intervalle, la connexion lève une exception. |
| `tcp_keep_alive_timeout` | 290 | secondes | client | Durée d'inactivité avant que l'OS n'envoie la première sonde TCP keepalive. |
| `receive_data_timeout_ms` | 2000 | millisecondes | client | Délai d'expiration pour la réception du premier paquet Data en provenance d'une réplique. |
| `connect_timeout_with_failover_ms` | 1000 | millisecondes | client | Délai d'expiration de connexion par tentative lors du parcours des répliques. |
| `connect_timeout_with_failover_secure_ms` | 1000 | millisecondes | client | Délai d'expiration de connexion par tentative lors du parcours des répliques via TLS. |
| `hedged_connection_timeout_ms` | 50 | millisecondes | client | Délai d'expiration de connexion par tentative pour les requêtes redondantes (hedged requests). |
| `poll_interval` | 10 | secondes | serveur | Granularité de la boucle du serveur qui vérifie les connexions inactives et l'arrêt. |

Les délais d'expiration s'imbriquent comme suit :

```text theme={null}
tcp_keep_alive_timeout (290s)
      < receive_timeout (300s)
      < idle_connection_timeout (3600s)
      < tcp_close_connection_after_queries_seconds (0 = unlimited by default)
```

Le keepalive de l'OS se déclenche en premier et peut détecter silencieusement les pairs injoignables au niveau du kernel. Le délai d'expiration de réception (timeout) de l'application constitue la ligne de défense suivante. Enfin, le délai d'inactivité (idle timeout) intervient en dernier recours pour libérer les connexions restées longtemps inutilisées.

#### Limites de connexion

| Paramètre | Par défaut | Unité | Côté | Description |
| - | - | - | - | - |
| `max_connections` | 4096 | nombre | serveur | Nombre maximal de connexions TCP simultanées. |
| `idle_connection_timeout` | 3600 | secondes | serveur | Durée maximale pendant laquelle une connexion inactive peut rester ouverte. |
| `tcp_close_connection_after_queries_num` | 0 (illimité) | nombre | serveur | Nombre maximal de requêtes par connexion avant sa fermeture forcée. |
| `tcp_close_connection_after_queries_seconds` | 0 (illimité) | secondes | serveur | Durée de vie totale maximale d'une connexion, quelle que soit son activité. |

Une connexion qui envoie régulièrement des requêtes peut rester ouverte indéfiniment : seules les connexions inactives sont fermées au bout d'une heure, et aucune durée de vie maximale n'est définie par défaut.

### Paramètres de la couche applicative

Ces paramètres sont transmis avec chaque requête dans la [liste des paramètres du paquet Query](#setting). Ils modifient ce que le serveur envoie dans le format binaire, ou la façon dont ces données sont découpées en trames.

#### Compression

| Paramètre | Par défaut | Unité | Description |
| - | - | - | - |
| `network_compression_method` | `"ZSTD"` | string | Codec de compression utilisé lorsque l'indicateur `compression` du paquet Query est activé. Valeurs : `"LZ4"`, `"LZ4HC"`, `"ZSTD"`, `"NONE"`. |
| `network_zstd_compression_level` | 3 | 1–15 | Niveau de compression ZSTD lorsque `network_compression_method == "ZSTD"`. |

L'indicateur `compression` du [paquet Query](#query) (champ 6) active ou désactive la compression ; ces paramètres déterminent quel codec est utilisé lorsqu'elle est activée.

#### Streaming des logs

| Paramètre | Valeur par défaut | Unité | Description |
| - | - | - | - |
| `send_logs_level` | `"fatal"` | string | Niveau de journalisation minimal. Valeurs : `"none"`, `"fatal"`, `"error"`, `"warning"`, `"information"`, `"debug"`, `"trace"`, `"test"`. |
| `send_logs_source_regexp` | `""` | string | Filtre regex appliqué à la source du logger. Vide = toutes les sources sont acceptées. |

Lorsque `send_logs_level` est défini sur une valeur autre que `"none"`, le serveur émet des paquets [Log](#log) pendant l'exécution de la requête.

#### Rapport de progression

| Paramètre | Valeur par défaut | Unité | Description |
| - | - | - | - |
| `interactive_delay` | 100000 | microsecondes | Intervalle minimal visé entre deux paquets Progress consécutifs. |

Il s'agit d'un minimum visé, et non d'un maximum strict : le serveur peut envoyer des paquets Progress moins souvent lorsque la requête ne progresse pas assez vite.

#### Enveloppe de résultat

| Paramètre | Par défaut | Unité | Description |
| - | - | - | - |
| `extremes` | false | bool | Si la valeur est true, le serveur envoie un paquet [Extremes](#extremes) contenant les valeurs min/max de chaque colonne. |
| `max_result_rows` | 0 (illimité) | nombre | Nombre maximal de lignes transmises. Le comportement est déterminé par `result_overflow_mode`. |
| `max_result_bytes` | 0 (illimité) | octets non compressés | Volume maximal d'octets non compressés. Le comportement est déterminé par `result_overflow_mode`. |
| `result_overflow_mode` | `"throw"` | string | `"throw"` termine le flux par une Exception ; `"break"` envoie des résultats partiels suivis d'un EndOfStream. |

#### INSERT asynchrone

| Paramètre | Valeur par défaut | Unité | Description |
| - | - | - | - |
| `async_insert` | true | bool | Si true, les données INSERT sont placées dans une file d’attente côté serveur et regroupées par lots. |
| `wait_for_async_insert` | true | bool | Si true (avec `async_insert` activé), le serveur ne renvoie sa réponse qu’une fois les données en file d’attente écrites sur disque. |
| `wait_for_async_insert_timeout` | 120 | secondes | Durée maximale pendant laquelle le serveur attend le vidage avant de répondre. |

#### Traçage distribué

| Paramètre | Valeur par défaut | Unité | Description |
| - | - | - | - |
| `opentelemetry_start_trace_probability` | 0.0 | probabilité de 0 à 1 | Probabilité, côté serveur, d'associer un contexte OpenTelemetry à la télémétrie des réponses. |

### Paramètres hors périmètre

Ces paramètres sont parfois pris à tort pour des paramètres de niveau protocole, mais ils contrôlent l'exécution SQL, le stockage ou l'utilisation du CPU, et non le comportement wire. Une implémentation du protocole n'a pas besoin de les traiter de manière particulière.

* `max_threads` — parallélisme lors de l'exécution des requêtes.
* `max_memory_usage` — plafond de mémoire par requête.
* `max_block_size`, `preferred_block_size_bytes` — dimensionnement interne des blocs lors du traitement des requêtes ; les blocs au format wire en sont indépendants.
* `compile_expressions` — compilation JIT ; concerne uniquement le CPU.
* `async_insert_max_data_size` — tampon de file d'attente côté serveur.
* Tous les paramètres `input_format_*` et `output_format_*`, **à l'exception** de la famille `input_format_native_*` / `output_format_native_*` — les paramètres autres que `native` sélectionnent ou ajustent d'autres formats (par exemple via HTTP) et ne modifient pas les blocs `Data` du protocole natif.

Les paramètres `*_native_*` font exception : ils modifient les octets contenus dans les blocs `Data` transmis en TCP natif, et une implémentation du protocole doit donc les prendre en compte. `output_format_native_encode_types_in_binary_format` remplace, dans le champ `type` de la colonne, la string textuelle par un encodage binaire du type (sauf pour une requête secondaire, voir [disposition wire des colonnes](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#column-wire-layout)) ; `output_format_native_write_json_as_string` émet les colonnes `JSON` sous forme de `String` ; et `output_format_native_use_flattened_dynamic_and_json_serialization` sélectionne la disposition FLATTENED pour `Dynamic`/`JSON`. Comme ils portent sur le corps du bloc et non sur l'enveloppe du paquet, ils sont décrits dans la spécification du [Native Format](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md) — voir [disposition wire des colonnes](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#column-wire-layout) et [types versionnés](https://clickhouse.com/docs/fr/resources/develop-contribute/native-protocol/format.md#versioned-types).

## Glossaire

**Cancel** — un paquet initié par le client (type 3) qui interrompt une requête en cours d'exécution. Non détaillé sur cette page.

**Marqueur de fin de données client** — un paquet Data vide (0 colonne, 0 ligne) que le client envoie pour clore un flux d'entrée. Sa position varie selon le type de requête :

* **Requête normale (`SELECT`, etc.) :** envoyé après le paquet Query et les éventuels paquets Data de tables externes pour signaler qu'il n'y a « plus de données externes ». Le serveur commence alors l'exécution.
* **`INSERT` :** le client n'envoie **pas** de marqueur avant le schéma. Le serveur envoie d'abord le bloc de schéma, le client transmet ses blocs Data de lignes, et c'est seulement ensuite qu'il envoie le paquet Data vide pour terminer le flux de lignes. Un marqueur vide envoyé avant le bloc de schéma serait interprété comme une fin immédiate des lignes, et les données seraient perdues.

**Feature** — une modification du format wire introduite dans une version spécifique du protocole. Active lorsque la version négociée est supérieure ou égale à la version de la feature. Voir [versionnement et feature gates](#versioning-and-feature-gates).

**Interserveur** — une étiquette de rôle désignant un champ qui n'a de sens que dans les requêtes distribuées entre serveurs. Les clients externes y écrivent une valeur par défaut (généralement une string vide, 0 ou false).

**Version négociée** — `min(client_version, server_version)`, calculée lors du handshake. Détermine quelles features sont actives pendant toute la durée de vie de la connexion.

**Paquet** — un message wire : un code de type de paquet VarUInt suivi d'un corps dont le format dépend du type. Voir [enveloppe de paquet](#packet-envelope).

**Code de type de paquet** — le VarUInt placé en tête d'un paquet, qui identifie son format. Les valeurs 0 à 18 sont actuellement attribuées. Voir la [référence des types de paquets](#packet-type-reference).

**Flux de réponse** — la séquence de paquets émis par le serveur pendant une requête. De longueur indéterminée, il se termine par exactement un `EndOfStream` (succès) ou un `Exception` (échec). Voir la [phase de requête](#query-phase).

**Bloc de schéma** — le bloc d'en-tête (un bloc avec des colonnes mais 0 ligne) que le serveur envoie pendant la phase INSERT pour annoncer la structure des colonnes attendue avant que le client n'envoie les données.

**Liste de paramètres** — une séquence de tuples `(key, flags, value)` dans le corps de Query, terminée par une clé vide. Transporte la configuration de la couche applicative propre à chaque requête. Voir [Setting](#setting).

**Stage** — un champ VarUInt du paquet [Query](#query) (champ 5) qui détermine jusqu'où le serveur exécute la requête. Les clients externes envoient généralement `2` (Complete) ; les requêtes distribuées et les plans de requête sérialisés utilisent les valeurs supérieures. Voir le champ 5 de [Query](#query) pour l'ensemble complet des valeurs wire.

**Terminateur** — un paquet qui met fin à un flux. La réponse à Query se termine par `EndOfStream` (succès) ou `Exception` (échec). Le flux d'entrée du client se termine par le marqueur Data vide.
