data, totals et extremes correspond exactement à ce que le format de sortie aurait produit sans tramage. Les paquets auxiliaires (progression, logs, événements de profil, exceptions) sont représentés au format JSON.
Le tramage peut également rendre un format de sortie plus expressif : c’est la seule exception délibérée à la règle ci-dessus. La famille de formats JSONCompactEachRow omet les totaux et les extrêmes dans sa sortie brute, car leurs lignes seraient indiscernables des lignes de données ordinaires. Avec un format de tramage, le type de paquet permet de les distinguer ; ces formats émettent donc les lignes de totaux et d’extrêmes (dans leur syntaxe de ligne habituelle) dans les paquets totals et extremes. Pour ces formats, la concaténation des payloads des seuls paquets data correspond exactement à ce que le format de sortie aurait produit sans tramage, tandis que les paquets totals et extremes contiennent des lignes supplémentaires absentes de la sortie sans tramage. Par conséquent, un client qui reconstruit la sortie sans tramage à partir d’un tel flux ne doit concaténer que les payloads data.
Le format de tramage est sélectionné par le paramètre défini au niveau de la requête framing_output_format. Il s’applique actuellement au protocole HTTP et est ignoré par les autres interfaces.
Les logs du serveur sont inclus sous forme de paquets si le paramètre send_logs_level est défini. Les événements de profil sont inclus si le paramètre send_profile_events est activé (par défaut). Les paquets de progression et d’événements de profil sont envoyés au maximum une fois par intervalle de interactive_delay microsecondes.
Un flux abouti se termine par un paquet progress final contenant les compteurs finaux (result_rows, result_bytes, memory_usage), comme le paquet de progression final du protocole natif. Ces compteurs ne sont connus qu’après la fin de la requête ; aucun paquet progress antérieur ne les contient donc. Le paquet progress final est écrit après les paquets log et profile_events de fin émis par la journalisation de fin de requête (par exemple, l’entrée de journal « utilisation maximale de la mémoire ») : il s’agit donc bien du dernier paquet du flux. En cas d’échec, le paquet exception est le dernier paquet à la place, et le paquet progress contenant les compteurs finaux n’est pas écrit du tout : il marque la réussite du flux, même lorsque l’échec survient après la fin de la requête elle-même et que les compteurs finaux étaient déjà connus (par exemple, en cas d’échec lors de l’écriture du journal des requêtes).
Comme cette fin de flux est écrite après l’enregistrement de l’entrée QueryFinish dans system.query_log, les événements de profil liés à l’envoi réseau de la requête (NetworkSendBytes, NetworkSendElapsedMicroseconds) n’incluent ni l’envoi des paquets de fin ni la fermeture de la réponse, ni, lorsque la réponse est mise en mémoire tampon (http_response_buffer_size ou wait_end_of_query), l’envoi du corps de réponse mis en mémoire tampon, qui n’est transmis qu’après la fin de la requête. Cela correspond au protocole natif, qui envoie également ses logs et événements de profil de fin après l’entrée du journal des requêtes.
Tout ce qu’une requête active uniquement via sa propre clause SETTINGS — un format de tramage, send_logs_level ou send_profile_events — n’est connu qu’après l’analyse syntaxique de la requête ; les logs et événements de profil correspondants ne sont donc capturés qu’à partir de l’exécution de la requête. Les logs et événements de profil des phases d’analyse syntaxique, de planification et d’analyse ne sont capturés que lorsque le paramètre provient de la session ou de l’URL. En particulier, une requête qui échoue pendant l’analyse (avant l’exécution du pipeline), par exemple en raison d’une référence à une table inconnue, et qui active send_logs_level uniquement dans sa clause SETTINGS, ne fournit que le paquet exception, et non les logs de la phase d’analyse. Définissez send_logs_level dans la session ou l’URL pour les capturer.
La même réserve concernant la découverte tardive s’applique à send_logs_source_regexp : la file d’attente des logs filtre les entrées par source au moment où chacune est capturée. Une regexp définie uniquement dans la clause SETTINGS de la requête ne prend donc effet qu’à partir de l’exécution de celle-ci. Les paquets log des phases d’analyse syntaxique, de planification et d’analyse sont filtrés selon la valeur du paramètre au niveau de la session ou de l’URL — ils ne sont pas filtrés si ce paramètre n’y est pas défini — et peuvent donc inclure des sources qui ne correspondent pas à la regexp au niveau de la requête. Inversement, les entrées éliminées par une regexp de session ou d’URL plus restrictive sont perdues et ne sont pas récupérées par une regexp plus large au niveau de la requête. Définissez send_logs_source_regexp au niveau de la session ou de l’URL afin de filtrer l’ensemble du cycle de vie de la requête.
Si une exception se produit pendant l’exécution de la requête, elle est envoyée sous forme de paquet exception (le dernier paquet du flux), quel que soit le paramètre http_write_exception_in_output_format, afin que le client puisse toujours analyser la réponse comme un flux de paquets. Une fois l’exception enregistrée, le format de sortie n’ajoute plus aucun octet au payload : une requête qui échoue avant de produire une sortie ne fournit aucun paquet data (pas même le squelette de document vide du format), et une requête qui échoue en cours de flux laisse le payload concaténé tronqué au point de défaillance, sans suffixe du format — le payload d’une requête ayant échoué ne doit pas ressembler à un document complet.
Il existe une exception à cette règle : si l’écriture d’un paquet échoue en cours de route (par exemple, si la connexion est interrompue après que certains octets du paquet ont déjà atteint le client), le tramage échoue de manière sécurisée et le flux est arrêté sans paquet exception final. Aucun nouvel essai n’est effectué pour un paquet partiellement écrit, car le réémettre ajouterait un doublon après les octets tronqués et corromprait le flux. Dans cette situation, le client observe une réponse tronquée et une connexion HTTP interrompue plutôt qu’un paquet terminal bien formé. La même règle s’applique à une défaillance lors de la fermeture du flux de réponse lui-même (vidage des résultats mis en mémoire tampon, finalisation de la compression HTTP, fermeture du socket) : à ce stade, une partie ou la totalité du flux de réussite a déjà été transmise dans le format binaire, de sorte que rien n’y est ajouté — ni paquet exception ni bloc d’erreur HTTP générique — et le client observe une réponse tronquée et une connexion interrompue. Elle s’applique également lorsque la transmission de l’exception elle-même échoue : si l’écriture du paquet exception terminal échoue (par exemple, lors de l’envoi des logs de fin) après qu’une partie du flux de paquets a été produite — qu’elle ait déjà été transmise ou qu’elle se trouve encore dans les tampons de réponse côté serveur (http_response_buffer_size) — le flux est également arrêté sans rien y ajouter, de sorte qu’un corps d’erreur HTTP brut n’est jamais mélangé à un flux de paquets partiel. Une défaillance lors de l’écriture des champs de type chaîne des paquets auxiliaires log, profile_events et exception compte également comme un paquet partiellement écrit, y compris si elle survient lors de l’écriture des derniers octets d’une telle chaîne : le flux se termine alors avec ce paquet tronqué et ne comporte aucun terminateur — ni paquet exception ni paquet progress des compteurs finaux — de sorte qu’un client nécessitant un terminateur détecte la défaillance même lorsque la requête elle-même a réussi.
Un format de tramage est également appliqué aux requêtes qui ne produisent aucun flux de résultats : un INSERT réussi, une requête DDL ou toute autre requête sans sortie. Une telle réponse ne contient aucun paquet data, mais définit tout de même le Content-Type de la réponse sur le format de tramage et transmet les paquets progress, log et profile_events, conformément au protocole natif. Le flux se termine par un paquet progress final contenant les compteurs finaux (par exemple, result_rows et result_bytes avec le nombre de lignes écrites pour un INSERT). Comme aucun payload n’est formaté, le format de sortie n’est pas pertinent pour ces requêtes et n’affecte pas le flux encadré.
Formats de tramage disponibles
None
JSONEachRowWithProgress.
EventStream
Content-Type de la réponse sur text/event-stream; charset=UTF-8; payload=base64. Chaque paquet est envoyé comme un événement nommé d’après son type : data, totals, extremes, progress, log, profile_events, exception. Les paquets de progression et les autres paquets auxiliaires sont envoyés au format JSON.
Les événements envoyés par le serveur constituent un protocole texte qui traite les sauts de ligne (y compris les retours chariot, \r) comme des délimiteurs de champs. Les octets produits par le format de sortie ne sont donc pas intégrés tels quels : un bloc de données formatées est encodé en base64 dans un unique champ data:, qui se décode en payload entièrement formaté, avec tous ses sauts de ligne. C’est ce qu’indique le paramètre payload=base64 du Content-Type. La concaténation des payloads décodés des paquets data, totals et extremes correspond exactement, octet pour octet, à ce que le format de sortie aurait produit sans tramage, quel que soit le format de sortie : texte, binaire (Native, RowBinary) ou transmission brute (RawBLOB, TSVRaw).
Les paquets JSON auxiliaires (progress, log, profile_events, exception) ne sont jamais encodés : ils sont écrits dans un unique champ data: contenant du JSON, sans aucun saut de ligne.
Les formats de sortie *WithProgress (JSONEachRowWithProgress, JSONCompactEachRowWithProgress) écrivent la progression sous forme de lignes intégrées faisant partie de leur propre sortie. Un format de tramage transmet plutôt la progression sous forme de paquets progress distincts ; il n’est donc pas compatible avec ces formats de sortie et les rejette. Utilisez le format de sortie de base (par exemple JSONEachRow) avec le tramage, ou le tramage None avec un format *WithProgress.
EventStream s’intègre au protocole HTTP et lève une exception lorsqu’il ne s’applique pas.
JSONEachPacketBase64 et JSONEachPacketString
application/x-ndjson) contenant les informations relatives au paquet. Les octets produits par le format de sortie sont placés dans le champ data : encodés en base64 dans JSONEachPacketBase64 (adapté aux formats de sortie binaires) ou sous forme de chaîne JSON dans JSONEachPacketString.
Les deux variantes encodent le champ data différemment. Le Content-Type de la réponse permet donc de les distinguer, comme pour EventStream : JSONEachPacketBase64 définit application/x-ndjson; charset=UTF-8; payload=base64, tandis que JSONEachPacketString définit application/x-ndjson; payload=string. Un client peut ainsi déterminer, à partir des seules métadonnées de la réponse, si le champ data doit être décodé de base64. charset=UTF-8 n’est garanti que par JSONEachPacketBase64, car seul l’encodage base64 garantit que l’intégralité du flux est en UTF-8 valide, quels que soient les octets du payload — voir ci-dessous.
Comme JSONEachPacketString place les octets du payload dans une chaîne JSON, il est destiné aux formats de sortie produisant du texte UTF-8 valide. Les colonnes String et FixedString peuvent contenir des octets arbitraires ; les formats de sortie texte tels que JSONEachRow, TSV ou CSV peuvent donc émettre de l’UTF-8 non valide pour ces valeurs, tout comme JSONEachRow de ClickHouse lorsque output_format_json_validate_utf8 = 0 est défini par défaut. Dans ce cas, la chaîne JSON résultante, et donc l’ensemble du flux NDJSON, n’est pas nécessairement en UTF-8 valide. JSONEachPacketString ne valide ni ne réencode le payload ; utilisez JSONEachPacketBase64 pour transporter des octets arbitraires sans aucune modification.
Les formats de sortie qui produisent assurément des octets non UTF-8 sont d’emblée rejetés par JSONEachPacketString avec une erreur, avant l’exécution de la requête : les formats binaires (Native, RowBinary), les formats de transmission brute (RawBLOB, TSVRaw), les formats qui écrivent dans leur sortie un nom de colonne, un nom de type de données ou un nom d’élément Tuple non UTF-8 issu de l’en-tête de la requête, ainsi que les configurations dont les littéraux définis par les paramètres sont écrits tels quels par les sérialisations et ne sont pas en UTF-8 valide : les paramètres format_csv_delimiter, format_tsv_null_representation / format_csv_null_representation et bool_true_representation / bool_false_representation.
JSONEachPacketBase64, le même paquet de data se présente ainsi :
Types de paquets
Contrairement aux payloads
data, totals et extremes (voir les notes ci-dessus sur l’exactitude au niveau des octets), les champs texte des paquets auxiliaires (query_id, text et source de log, name de profile_events et le message d’exception) n’offrent aucun mécanisme d’échappement en base64, et certains d’entre eux (par exemple query_id, issu de la requête) peuvent contenir des octets arbitraires. Ces champs sont systématiquement assainis en UTF-8 valide, les séquences non valides étant remplacées par le caractère de remplacement (U+FFFD), de sorte que les paquets auxiliaires constituent toujours du JSON valide.
Le traitement simultané de plusieurs requêtes n’est pas encore implémenté, mais la conception le permet : chaque paquet peut être étendu avec des informations sur l’index de la requête parmi plusieurs requêtes.